diff --git a/src/main/java/com/github/shadowsocks/Exec.java b/src/main/java/com/github/shadowsocks/Exec.java
deleted file mode 100755
index d3a32625..00000000
--- a/src/main/java/com/github/shadowsocks/Exec.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.github.shadowsocks;
-
-import java.io.FileDescriptor;
-
-/**
- * Utility methods for creating and managing a subprocess.
- *
- * Note: The native methods access a package-private java.io.FileDescriptor
- * field to get and set the raw Linux file descriptor. This might break if the
- * implementation of java.io.FileDescriptor is changed.
- */
-
-public class Exec {
- static {
- java.lang.System.loadLibrary("exec");
- }
-
- /** Close a given file descriptor. */
- public static native void close(FileDescriptor fd);
-
- /**
- * Create a subprocess. Differs from java.lang.ProcessBuilder in that a pty
- * is used to communicate with the subprocess.
- *
- * Callers are responsible for calling Exec.close() on the returned file
- * descriptor.
- *
- * @param rdt Whether redirect stdout and stderr
- * @param cmd The command to execute.
- * @param args An array of arguments to the command.
- * @param envVars An array of strings of the form "VAR=value" to be added to the
- * environment of the process.
- * @param scripts The scripts to execute.
- * @param processId A one-element array to which the process ID of the started
- * process will be written.
- * @return File descriptor
- */
- public static native FileDescriptor createSubprocess(int rdt, String cmd, String[] args,
- String[] envVars, String scripts, int[] processId);
-
- /** Send SIGHUP to a process group. */
- public static native void hangupProcessGroup(int processId);
-
- /**
- * Causes the calling thread to wait for the process associated with the
- * receiver to finish executing.
- *
- * @return The exit value of the Process being waited on
- */
- public static native int waitFor(int processId);
-}
diff --git a/src/main/java/eu/chainfire/libsuperuser/Application.java b/src/main/java/eu/chainfire/libsuperuser/Application.java
new file mode 100644
index 00000000..55bbf320
--- /dev/null
+++ b/src/main/java/eu/chainfire/libsuperuser/Application.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2012-2013 Jorrit "Chainfire" Jongma
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package eu.chainfire.libsuperuser;
+
+import android.content.Context;
+import android.os.Handler;
+import android.widget.Toast;
+
+/**
+ * Base application class to extend from, solving some issues with
+ * toasts and AsyncTasks you are likely to run into
+ */
+public class Application extends android.app.Application {
+ /**
+ * Shows a toast message
+ *
+ * @param context Any context belonging to this application
+ * @param message The message to show
+ */
+ public static void toast(Context context, String message) {
+ // this is a static method so it is easier to call,
+ // as the context checking and casting is done for you
+
+ if (context == null) return;
+
+ if (!(context instanceof Application)) {
+ context = context.getApplicationContext();
+ }
+
+ if (context instanceof Application) {
+ final Context c = context;
+ final String m = message;
+
+ ((Application)context).runInApplicationThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(c, m, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+ }
+
+ private static Handler mApplicationHandler = new Handler();
+
+ /**
+ * Run a runnable in the main application thread
+ *
+ * @param r Runnable to run
+ */
+ public void runInApplicationThread(Runnable r) {
+ mApplicationHandler.post(r);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ try {
+ // workaround bug in AsyncTask, can show up (for example) when you toast from a service
+ // this makes sure AsyncTask's internal handler is created from the right (main) thread
+ Class.forName("android.os.AsyncTask");
+ } catch (ClassNotFoundException e) {
+ }
+ }
+}
diff --git a/src/main/java/eu/chainfire/libsuperuser/Debug.java b/src/main/java/eu/chainfire/libsuperuser/Debug.java
new file mode 100644
index 00000000..4fda587a
--- /dev/null
+++ b/src/main/java/eu/chainfire/libsuperuser/Debug.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2012-2013 Jorrit "Chainfire" Jongma
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package eu.chainfire.libsuperuser;
+
+import android.os.Looper;
+import android.util.Log;
+import com.github.shadowsocks.BuildConfig;
+
+/**
+ * Utility class for logging and debug features that (by default) does nothing when not in debug mode
+ */
+public class Debug {
+
+ // ----- DEBUGGING -----
+
+ private static boolean debug = BuildConfig.DEBUG;
+
+ /**
+ * Enable or disable debug mode
+ *
+ * By default, debug mode is enabled for development
+ * builds and disabled for exported APKs - see
+ * BuildConfig.DEBUG
+ *
+ * @param enabled Enable debug mode ?
+ */
+ public static void setDebug(boolean enable) {
+ debug = enable;
+ }
+
+ /**
+ * Is debug mode enabled ?
+ *
+ * @return Debug mode enabled
+ */
+ public static boolean getDebug() {
+ return debug;
+ }
+
+ // ----- LOGGING -----
+
+ public interface OnLogListener {
+ public void onLog(int type, String typeIndicator, String message);
+ }
+
+ public static final String TAG = "libsuperuser";
+
+ public static final int LOG_GENERAL = 0x0001;
+ public static final int LOG_COMMAND = 0x0002;
+ public static final int LOG_OUTPUT = 0x0004;
+
+ public static final int LOG_NONE = 0x0000;
+ public static final int LOG_ALL = 0xFFFF;
+
+ private static int logTypes = LOG_ALL;
+
+ private static OnLogListener logListener = null;
+
+ /**
+ * Log a message (internal)
+ *
+ * Current debug and enabled logtypes decide what gets logged -
+ * even if a custom callback is registered
+ *
+ * @param type Type of message to log
+ * @param typeIndicator String indicator for message type
+ * @param message The message to log
+ */
+ private static void logCommon(int type, String typeIndicator, String message) {
+ if (debug && ((logTypes & type) == type)) {
+ if (logListener != null) {
+ logListener.onLog(type, typeIndicator, message);
+ } else {
+ Log.d(TAG, "[" + TAG + "][" + typeIndicator + "]" + (!message.startsWith("[") && !message.startsWith(" ") ? " " : "") + message);
+ }
+ }
+ }
+
+ /**
+ * Log a "general" message
+ *
+ * These messages are infrequent and mostly occur at startup/shutdown or on error
+ *
+ * @param message The message to log
+ */
+ public static void log(String message) {
+ logCommon(LOG_GENERAL, "G", message);
+ }
+
+ /**
+ * Log a "per-command" message
+ *
+ * This could produce a lot of output if the client runs many commands in the session
+ *
+ * @param message The message to log
+ */
+ public static void logCommand(String message) {
+ logCommon(LOG_COMMAND, "C", message);
+ }
+
+ /**
+ * Log a line of stdout/stderr output
+ *
+ * This could produce a lot of output if the shell commands are noisy
+ *
+ * @param message The message to log
+ */
+ public static void logOutput(String message) {
+ logCommon(LOG_OUTPUT, "O", message);
+ }
+
+ /**
+ * Enable or disable logging specific types of message
+ *
+ * You may | (or) LOG_* constants together. Note that
+ * debug mode must also be enabled for actual logging to
+ * occur.
+ *
+ * @param type LOG_* constants
+ * @param enabled Enable or disable
+ */
+ public static void setLogTypeEnabled(int type, boolean enable) {
+ if (enable) {
+ logTypes |= type;
+ } else {
+ logTypes &= ~type;
+ }
+ }
+
+ /**
+ * Is logging for specific types of messages enabled ?
+ *
+ * You may | (or) LOG_* constants together, to learn if
+ * all passed message types are enabled for logging. Note
+ * that debug mode must also be enabled for actual logging
+ * to occur.
+ *
+ * @param type LOG_* constants
+ */
+ public static boolean getLogTypeEnabled(int type) {
+ return ((logTypes & type) == type);
+ }
+
+ /**
+ * Is logging for specific types of messages enabled ?
+ *
+ * You may | (or) LOG_* constants together, to learn if
+ * all message types are enabled for logging. Takes
+ * debug mode into account for the result.
+ *
+ * @param type LOG_* constants
+ */
+ public static boolean getLogTypeEnabledEffective(int type) {
+ return getDebug() && getLogTypeEnabled(type);
+ }
+
+ /**
+ * Register a custom log handler
+ *
+ * Replaces the log method (write to logcat) with your own
+ * handler. Whether your handler gets called is still dependent
+ * on debug mode and message types being enabled for logging.
+ *
+ * @param onLogListener Custom log listener or NULL to revert to default
+ */
+ public static void setOnLogListener(OnLogListener onLogListener) {
+ logListener = onLogListener;
+ }
+
+ /**
+ * Get the currently registered custom log handler
+ *
+ * @return Current custom log handler or NULL if none is present
+ */
+ public static OnLogListener getOnLogListener() {
+ return logListener;
+ }
+
+ // ----- SANITY CHECKS -----
+
+ private static boolean sanityChecks = true;
+
+ /**
+ * Enable or disable sanity checks
+ *
+ * Enables or disables the library crashing when su is called
+ * from the main thread.
+ *
+ * @param enabled Enable or disable
+ */
+ public static void setSanityChecksEnabled(boolean enable) {
+ sanityChecks = enable;
+ }
+
+ /**
+ * Are sanity checks enabled ?
+ *
+ * Note that debug mode must also be enabled for actual
+ * sanity checks to occur.
+ *
+ * @return True if enabled
+ */
+ public static boolean getSanityChecksEnabled() {
+ return sanityChecks;
+ }
+
+ /**
+ * Are sanity checks enabled ?
+ *
+ * Takes debug mode into account for the result.
+ *
+ * @return True if enabled
+ */
+ public static boolean getSanityChecksEnabledEffective() {
+ return getDebug() && getSanityChecksEnabled();
+ }
+
+ /**
+ * Are we running on the main thread ?
+ *
+ * @return Running on main thread ?
+ */
+ public static boolean onMainThread() {
+ return ((Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper()));
+ }
+
+}
diff --git a/src/main/java/eu/chainfire/libsuperuser/Shell.java b/src/main/java/eu/chainfire/libsuperuser/Shell.java
new file mode 100644
index 00000000..496ba0f6
--- /dev/null
+++ b/src/main/java/eu/chainfire/libsuperuser/Shell.java
@@ -0,0 +1,1342 @@
+/*
+ * Copyright (C) 2012-2013 Jorrit "Chainfire" Jongma
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package eu.chainfire.libsuperuser;
+
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import eu.chainfire.libsuperuser.StreamGobbler.OnLineListener;
+
+/**
+ * Class providing functionality to execute commands in a (root) shell
+ */
+public class Shell {
+ /**
+ * Runs commands using the supplied shell, and returns the output, or null in
+ * case of errors.
+ *
+ * This method is deprecated and only provided for backwards compatibility.
+ * Use {@link #run(String, String[], String[], boolean)} instead, and see that
+ * same method for usage notes.
+ *
+ * @param shell The shell to use for executing the commands
+ * @param commands The commands to execute
+ * @param wantSTDERR Return STDERR in the output ?
+ * @return Output of the commands, or null in case of an error
+ */
+ @Deprecated
+ public static List run(String shell, String[] commands, boolean wantSTDERR) {
+ return run(shell, commands, null, wantSTDERR);
+ }
+
+ /**
+ * Runs commands using the supplied shell, and returns the output, or null in
+ * case of errors.
+ *
+ * Note that due to compatibility with older Android versions,
+ * wantSTDERR is not implemented using redirectErrorStream, but rather appended
+ * to the output. STDOUT and STDERR are thus not guaranteed to be in the correct
+ * order in the output.
+ *
+ * Note as well that this code will intentionally crash when run in debug mode
+ * from the main thread of the application. You should always execute shell
+ * commands from a background thread.
+ *
+ * When in debug mode, the code will also excessively log the commands passed to
+ * and the output returned from the shell.
+ *
+ * Though this function uses background threads to gobble STDOUT and STDERR so
+ * a deadlock does not occur if the shell produces massive output, the output is
+ * still stored in a List<String>, and as such doing something like 'ls -lR /'
+ * will probably have you run out of memory.
+ *
+ * @param shell The shell to use for executing the commands
+ * @param commands The commands to execute
+ * @param environment List of all environment variables (in 'key=value' format) or null for defaults
+ * @param wantSTDERR Return STDERR in the output ?
+ * @return Output of the commands, or null in case of an error
+ */
+ public static List run(String shell, String[] commands, String[] environment, boolean wantSTDERR) {
+ String shellUpper = shell.toUpperCase(Locale.ENGLISH);
+
+ if (Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) {
+ // check if we're running in the main thread, and if so, crash if we're in debug mode,
+ // to let the developer know attention is needed here.
+
+ Debug.log(ShellOnMainThreadException.EXCEPTION_COMMAND);
+ throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_COMMAND);
+ }
+ Debug.logCommand(String.format("[%s%%] START", shellUpper));
+
+ List res = Collections.synchronizedList(new ArrayList());
+
+ try {
+ // Combine passed environment with system environment
+ if (environment != null) {
+ Map newEnvironment = new HashMap();
+ newEnvironment.putAll(System.getenv());
+ int split;
+ for (String entry : environment) {
+ if ((split = entry.indexOf("=")) >= 0) {
+ newEnvironment.put(entry.substring(0, split), entry.substring(split + 1));
+ }
+ }
+ int i = 0;
+ environment = new String[newEnvironment.size()];
+ for (Map.Entry entry : newEnvironment.entrySet()) {
+ environment[i] = entry.getKey() + "=" + entry.getValue();
+ i++;
+ }
+ }
+
+ // setup our process, retrieve STDIN stream, and STDOUT/STDERR gobblers
+ Process process = Runtime.getRuntime().exec(shell, environment);
+ DataOutputStream STDIN = new DataOutputStream(process.getOutputStream());
+ StreamGobbler STDOUT = new StreamGobbler(shellUpper + "-", process.getInputStream(), res);
+ StreamGobbler STDERR = new StreamGobbler(shellUpper + "*", process.getErrorStream(), wantSTDERR ? res : null);
+
+ // start gobbling and write our commands to the shell
+ STDOUT.start();
+ STDERR.start();
+ for (String write : commands) {
+ Debug.logCommand(String.format("[%s+] %s", shellUpper, write));
+ STDIN.write((write + "\n").getBytes("UTF-8"));
+ STDIN.flush();
+ }
+ STDIN.write("exit\n".getBytes("UTF-8"));
+ STDIN.flush();
+
+ // wait for our process to finish, while we gobble away in the background
+ process.waitFor();
+
+ // make sure our threads are done gobbling, our streams are closed, and the process is
+ // destroyed - while the latter two shouldn't be needed in theory, and may even produce
+ // warnings, in "normal" Java they are required for guaranteed cleanup of resources, so
+ // lets be safe and do this on Android as well
+ try {
+ STDIN.close();
+ } catch (IOException e) {
+ }
+ STDOUT.join();
+ STDERR.join();
+ process.destroy();
+
+ // in case of su, 255 usually indicates access denied
+ if (SU.isSU(shell) && (process.exitValue() == 255)) {
+ res = null;
+ }
+ } catch (IOException e) {
+ // shell probably not found
+ res = null;
+ } catch (InterruptedException e) {
+ // this should really be re-thrown
+ res = null;
+ }
+
+ Debug.logCommand(String.format("[%s%%] END", shell.toUpperCase(Locale.ENGLISH)));
+ return res;
+ }
+
+ protected static String[] availableTestCommands = new String[] {
+ "echo -BOC-",
+ "id"
+ };
+
+ /**
+ * See if the shell is alive, and if so, check the UID
+ *
+ * @param ret Standard output from running availableTestCommands
+ * @param checkForRoot true if we are expecting this shell to be running as root
+ * @return true on success, false on error
+ */
+ protected static boolean parseAvailableResult(List ret, boolean checkForRoot) {
+ if (ret == null) return false;
+
+ // this is only one of many ways this can be done
+ boolean echo_seen = false;
+
+ for (String line : ret) {
+ if (line.contains("uid=")) {
+ // id command is working, let's see if we are actually root
+ return !checkForRoot || line.contains("uid=0");
+ } else if (line.contains("-BOC-")) {
+ // if we end up here, at least the su command starts some kind of shell,
+ // let's hope it has root privileges - no way to know without additional
+ // native binaries
+ echo_seen = true;
+ }
+ }
+
+ return echo_seen;
+ }
+
+ /**
+ * This class provides utility functions to easily execute commands using SH
+ */
+ public static class SH {
+ /**
+ * Runs command and return output
+ *
+ * @param command The command to run
+ * @return Output of the command, or null in case of an error
+ */
+ public static List run(String command) {
+ return Shell.run("sh", new String[] { command }, null, false);
+ }
+
+ /**
+ * Runs commands and return output
+ *
+ * @param commands The commands to run
+ * @return Output of the commands, or null in case of an error
+ */
+ public static List run(List commands) {
+ return Shell.run("sh", commands.toArray(new String[commands.size()]), null, false);
+ }
+
+ /**
+ * Runs commands and return output
+ *
+ * @param commands The commands to run
+ * @return Output of the commands, or null in case of an error
+ */
+ public static List run(String[] commands) {
+ return Shell.run("sh", commands, null, false);
+ }
+ }
+
+ /**
+ * This class provides utility functions to easily execute commands using SU
+ * (root shell), as well as detecting whether or not root is available, and
+ * if so which version.
+ */
+ public static class SU {
+ /**
+ * Runs command as root (if available) and return output
+ *
+ * @param command The command to run
+ * @return Output of the command, or null if root isn't available or in case of an error
+ */
+ public static List run(String command) {
+ return Shell.run("su", new String[] { command }, null, false);
+ }
+
+ /**
+ * Runs commands as root (if available) and return output
+ *
+ * @param commands The commands to run
+ * @return Output of the commands, or null if root isn't available or in case of an error
+ */
+ public static List run(List commands) {
+ return Shell.run("su", commands.toArray(new String[commands.size()]), null, false);
+ }
+
+ /**
+ * Runs commands as root (if available) and return output
+ *
+ * @param commands The commands to run
+ * @return Output of the commands, or null if root isn't available or in case of an error
+ */
+ public static List run(String[] commands) {
+ return Shell.run("su", commands, null, false);
+ }
+
+ /**
+ * Detects whether or not superuser access is available, by checking the output
+ * of the "id" command if available, checking if a shell runs at all otherwise
+ *
+ * @return True if superuser access available
+ */
+ public static boolean available() {
+ // this is only one of many ways this can be done
+
+ List ret = run(Shell.availableTestCommands);
+ return Shell.parseAvailableResult(ret, true);
+ }
+
+ /**
+ * Detects the version of the su binary installed (if any), if supported by the binary.
+ * Most binaries support two different version numbers, the public version that is
+ * displayed to users, and an internal version number that is used for version number
+ * comparisons. Returns null if su not available or retrieving the version isn't supported.
+ *
+ * Note that su binary version and GUI (APK) version can be completely different.
+ *
+ * @param internal Request human-readable version or application internal version
+ * @return String containing the su version or null
+ */
+ public static String version(boolean internal) {
+ List ret = Shell.run(
+ internal ? "su -V" : "su -v",
+ new String[] { },
+ null,
+ false
+ );
+ if (ret == null) return null;
+
+ for (String line : ret) {
+ if (!internal) {
+ if (line.contains(".")) return line;
+ } else {
+ try {
+ if (Integer.parseInt(line) > 0) return line;
+ } catch(NumberFormatException e) {
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Attempts to deduce if the shell command refers to a su shell
+ *
+ * @param shell Shell command to run
+ * @return Shell command appears to be su
+ */
+ public static boolean isSU(String shell) {
+ // Strip parameters
+ int pos = shell.indexOf(' ');
+ if (pos >= 0) {
+ shell = shell.substring(0, pos);
+ }
+
+ // Strip path
+ pos = shell.lastIndexOf('/');
+ if (pos >= 0) {
+ shell = shell.substring(pos + 1);
+ }
+
+ return shell.equals("su");
+ }
+
+ /**
+ * Constructs a shell command to start a su shell using the supplied
+ * uid and SELinux context. This is can be an expensive operation,
+ * consider caching the result.
+ *
+ * @param uid Uid to use (0 == root)
+ * @param context (SELinux) context name to use or null
+ * @return Shell command
+ */
+ public static String shell(int uid, String context) {
+ // su[ --context ][ ]
+ String shell = "su";
+
+ // First known firmware with SELinux built-in was a 4.2 (17) leak
+ if ((context != null) && (android.os.Build.VERSION.SDK_INT >= 17)) {
+ Boolean enforcing = null;
+
+ // Detect enforcing through sysfs, not always present
+ if (enforcing == null) {
+ File f = new File("/sys/fs/selinux/enforce");
+ if (f.exists()) {
+ try {
+ InputStream is = new FileInputStream("/sys/fs/selinux/enforce");
+ try {
+ enforcing = (is.read() == '1');
+ } finally {
+ is.close();
+ }
+ } catch (Exception e) {
+ }
+ }
+ }
+
+ // 4.4+ builds are enforcing by default, take the gamble
+ if (enforcing == null) {
+ enforcing = (android.os.Build.VERSION.SDK_INT >= 19);
+ }
+
+ // Switching to a context in permissive mode is not generally
+ // useful aside from audit testing, but we want to avoid
+ // switching the context due to the increased chance of issues.
+ if (enforcing) {
+ String display = version(false);
+ String internal = version(true);
+
+ // We only know the format for SuperSU v1.90+ right now
+ if (
+ (display != null) &&
+ (internal != null) &&
+ (display.endsWith("SUPERSU")) &&
+ (Integer.valueOf(internal) >= 190)
+ ) {
+ shell = String.format(Locale.ENGLISH, "%s --context %s", shell, context);
+ }
+ }
+ }
+
+ // Most su binaries support the "su " format, but in case
+ // they don't, lets skip it for the default 0 (root) case
+ if (uid > 0) {
+ shell = String.format(Locale.ENGLISH, "%s %d", shell, uid);
+ }
+
+ return shell;
+ }
+ }
+
+ /**
+ * Command result callback, notifies the recipient of the completion of a command
+ * block, including the (last) exit code, and the full output
+ */
+ public interface OnCommandResultListener {
+ /**
+ * Command result callback
+ *
+ * Depending on how and on which thread the shell was created, this callback
+ * may be executed on one of the gobbler threads. In that case, it is important
+ * the callback returns as quickly as possible, as delays in this callback may
+ * pause the native process or even result in a deadlock
+ *
+ * See {@link Shell.Interactive} for threading details
+ *
+ * @param commandCode Value previously supplied to addCommand
+ * @param exitCode Exit code of the last command in the block
+ * @param output All output generated by the command block
+ */
+ public void onCommandResult(int commandCode, int exitCode, List output);
+
+ // for any onCommandResult callback
+ public static final int WATCHDOG_EXIT = -1;
+ public static final int SHELL_DIED = -2;
+
+ // for Interactive.open() callbacks only
+ public static final int SHELL_EXEC_FAILED = -3;
+ public static final int SHELL_WRONG_UID = -4;
+ public static final int SHELL_RUNNING = 0;
+ }
+
+ /**
+ * Internal class to store command block properties
+ */
+ private static class Command {
+ private static int commandCounter = 0;
+
+ private final String[] commands;
+ private final int code;
+ private final OnCommandResultListener onCommandResultListener;
+ private final String marker;
+
+ public Command(String[] commands, int code, OnCommandResultListener onCommandResultListener) {
+ this.commands = commands;
+ this.code = code;
+ this.onCommandResultListener = onCommandResultListener;
+ this.marker = UUID.randomUUID().toString() + String.format("-%08x", ++commandCounter);
+ }
+ }
+
+ /**
+ * Builder class for {@link Shell.Interactive}
+ */
+ public static class Builder {
+ private Handler handler = null;
+ private boolean autoHandler = true;
+ private String shell = "sh";
+ private boolean wantSTDERR = false;
+ private List commands = new LinkedList();
+ private Map environment = new HashMap();
+ private OnLineListener onSTDOUTLineListener = null;
+ private OnLineListener onSTDERRLineListener = null;
+ private int watchdogTimeout = 0;
+
+ /**
+ * Set a custom handler that will be used to post all callbacks to
+ *
+ * See {@link Shell.Interactive} for further details on threading and handlers
+ *
+ * @param handler Handler to use
+ * @return This Builder object for method chaining
+ */
+ public Builder setHandler(Handler handler) { this.handler = handler; return this; }
+
+ /**
+ * Automatically create a handler if possible ? Default to true
+ *
+ * See {@link Shell.Interactive} for further details on threading and handlers
+ *
+ * @param autoHandler Auto-create handler ?
+ * @return This Builder object for method chaining
+ */
+ public Builder setAutoHandler(boolean autoHandler) { this.autoHandler = autoHandler; return this; }
+
+ /**
+ * Set shell binary to use. Usually "sh" or "su", do not use a full path
+ * unless you have a good reason to
+ *
+ * @param shell Shell to use
+ * @return This Builder object for method chaining
+ */
+ public Builder setShell(String shell) { this.shell = shell; return this; }
+
+ /**
+ * Convenience function to set "sh" as used shell
+ *
+ * @return This Builder object for method chaining
+ */
+ public Builder useSH() { return setShell("sh"); }
+
+ /**
+ * Convenience function to set "su" as used shell
+ *
+ * @return This Builder object for method chaining
+ */
+ public Builder useSU() { return setShell("su"); }
+
+ /**
+ * Set if error output should be appended to command block result output
+ *
+ * @param wantSTDERR Want error output ?
+ * @return This Builder object for method chaining
+ */
+ public Builder setWantSTDERR(boolean wantSTDERR) { this.wantSTDERR = wantSTDERR; return this; }
+
+ /**
+ * Add or update an environment variable
+ *
+ * @param key Key of the environment variable
+ * @param value Value of the environment variable
+ * @return This Builder object for method chaining
+ */
+ public Builder addEnvironment(String key, String value) { environment.put(key, value); return this; }
+
+ /**
+ * Add or update environment variables
+ *
+ * @param addEnvironment Map of environment variables
+ * @return This Builder object for method chaining
+ */
+ public Builder addEnvironment(Map addEnvironment) { environment.putAll(addEnvironment); return this; }
+
+ /**
+ * Add a command to execute
+ *
+ * @param command Command to execute
+ * @return This Builder object for method chaining
+ */
+ public Builder addCommand(String command) { return addCommand(command, 0, null); }
+
+ /**
+ * Add a command to execute, with a callback to be called on completion
+ *
+ * The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details
+ *
+ * @param command Command to execute
+ * @param code User-defined value passed back to the callback
+ * @param onCommandResultListener Callback to be called on completion
+ * @return This Builder object for method chaining
+ */
+ public Builder addCommand(String command, int code, OnCommandResultListener onCommandResultListener) { return addCommand(new String[] { command }, code, onCommandResultListener); }
+
+ /**
+ * Add commands to execute
+ *
+ * @param commands Commands to execute
+ * @return This Builder object for method chaining
+ */
+ public Builder addCommand(List commands) { return addCommand(commands, 0, null); }
+
+ /**
+ * Add commands to execute, with a callback to be called on completion (of all commands)
+ *
+ * The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details
+ *
+ * @param commands Commands to execute
+ * @param code User-defined value passed back to the callback
+ * @param onCommandResultListener Callback to be called on completion (of all commands)
+ * @return This Builder object for method chaining
+ */
+ public Builder addCommand(List commands, int code, OnCommandResultListener onCommandResultListener) { return addCommand(commands.toArray(new String[commands.size()]), code, onCommandResultListener); }
+
+ /**
+ * Add commands to execute
+ *
+ * @param commands Commands to execute
+ * @return This Builder object for method chaining
+ */
+ public Builder addCommand(String[] commands) { return addCommand(commands, 0, null); }
+
+ /**
+ * Add commands to execute, with a callback to be called on completion (of all commands)
+ *
+ * The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details
+ *
+ * @param commands Commands to execute
+ * @param code User-defined value passed back to the callback
+ * @param onCommandResultListener Callback to be called on completion (of all commands)
+ * @return This Builder object for method chaining
+ */
+ public Builder addCommand(String[] commands, int code, OnCommandResultListener onCommandResultListener) { this.commands.add(new Command(commands, code, onCommandResultListener)); return this; }
+
+ /**
+ * Set a callback called for every line output to STDOUT by the shell
+ *
+ * The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details
+ *
+ * @param onLineListener Callback to be called for each line
+ * @return This Builder object for method chaining
+ */
+ public Builder setOnSTDOUTLineListener(OnLineListener onLineListener) { this.onSTDOUTLineListener = onLineListener; return this; }
+
+ /**
+ * Set a callback called for every line output to STDERR by the shell
+ *
+ * The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details
+ *
+ * @param onLineListener Callback to be called for each line
+ * @return This Builder object for method chaining
+ */
+ public Builder setOnSTDERRLineListener(OnLineListener onLineListener) { this.onSTDERRLineListener = onLineListener; return this; }
+
+ /**
+ * Enable command timeout callback
+ *
+ * This will invoke the onCommandResult() callback with exitCode WATCHDOG_EXIT if a command takes longer than watchdogTimeout
+ * seconds to complete.
+ *
+ * If a watchdog timeout occurs, it generally means that the Interactive session is out of sync with the shell process. The
+ * caller should close the current session and open a new one.
+ *
+ * @param watchdogTimeout Timeout, in seconds; 0 to disable
+ * @return This Builder object for method chaining
+ */
+ public Builder setWatchdogTimeout(int watchdogTimeout) { this.watchdogTimeout = watchdogTimeout; return this; }
+
+ /**
+ * Enable/disable reduced logcat output
+ *
+ * Note that this is a global setting
+ *
+ * @param useMinimal true for reduced output, false for full output
+ * @return This Builder object for method chaining
+ */
+ public Builder setMinimalLogging(boolean useMinimal) {
+ Debug.setLogTypeEnabled(Debug.LOG_COMMAND | Debug.LOG_OUTPUT, !useMinimal);
+ return this;
+ }
+
+ /**
+ * Construct a {@link Shell.Interactive} instance, and start the shell
+ */
+ public Interactive open() { return new Interactive(this, null); }
+
+ /**
+ * Construct a {@link Shell.Interactive} instance, try to start the shell, and
+ * call onCommandResultListener to report success or failure
+ *
+ * @param onCommandResultListener Callback to return shell open status
+ */
+ public Interactive open(OnCommandResultListener onCommandResultListener) {
+ return new Interactive(this, onCommandResultListener);
+ }
+ }
+
+ /**
+ * An interactive shell - initially created with {@link Shell.Builder} - that
+ * executes blocks of commands you supply in the background, optionally calling
+ * callbacks as each block completes.
+ *
+ * STDERR output can be supplied as well, but due to compatibility with older
+ * Android versions, wantSTDERR is not implemented using redirectErrorStream,
+ * but rather appended to the output. STDOUT and STDERR are thus not guaranteed to
+ * be in the correct order in the output.
+ *
+ * Note as well that the close() and waitForIdle() methods will intentionally
+ * crash when run in debug mode from the main thread of the application. Any blocking
+ * call should be run from a background thread.
+ *
+ * When in debug mode, the code will also excessively log the commands passed to
+ * and the output returned from the shell.
+ *
+ * Though this function uses background threads to gobble STDOUT and STDERR so
+ * a deadlock does not occur if the shell produces massive output, the output is
+ * still stored in a List<String>, and as such doing something like 'ls -lR /'
+ * will probably have you run out of memory when using a
+ * {@link Shell.OnCommandResultListener}. A work-around is to not supply this callback,
+ * but using (only) {@link Shell.Builder#setOnSTDOUTLineListener(OnLineListener)}. This
+ * way, an internal buffer will not be created and wasting your memory.
+ *
+ * Callbacks, threads and handlers
+ *
+ * On which thread the callbacks execute is dependent on your initialization. You can
+ * supply a custom Handler using {@link Shell.Builder#setHandler(Handler)} if needed.
+ * If you do not supply a custom Handler - unless you set {@link Shell.Builder#setAutoHandler(boolean)}
+ * to false - a Handler will be auto-created if the thread used for instantiation
+ * of the object has a Looper.
+ *
+ * If no Handler was supplied and it was also not auto-created, all callbacks will
+ * be called from either the STDOUT or STDERR gobbler threads. These are important
+ * threads that should be blocked as little as possible, as blocking them may in rare
+ * cases pause the native process or even create a deadlock.
+ *
+ * The main thread must certainly have a Looper, thus if you call {@link Shell.Builder#open()}
+ * from the main thread, a handler will (by default) be auto-created, and all the callbacks
+ * will be called on the main thread. While this is often convenient and easy to code with,
+ * you should be aware that if your callbacks are 'expensive' to execute, this may negatively
+ * impact UI performance.
+ *
+ * Background threads usually do not have a Looper, so calling {@link Shell.Builder#open()}
+ * from such a background thread will (by default) result in all the callbacks being executed
+ * in one of the gobbler threads. You will have to make sure the code you execute in these callbacks
+ * is thread-safe.
+ */
+ public static class Interactive {
+ private final Handler handler;
+ private final boolean autoHandler;
+ private final String shell;
+ private final boolean wantSTDERR;
+ private final List commands;
+ private final Map environment;
+ private final OnLineListener onSTDOUTLineListener;
+ private final OnLineListener onSTDERRLineListener;
+ private int watchdogTimeout;
+
+ private Process process = null;
+ private DataOutputStream STDIN = null;
+ private StreamGobbler STDOUT = null;
+ private StreamGobbler STDERR = null;
+ private ScheduledThreadPoolExecutor watchdog = null;
+
+ private volatile boolean running = false;
+ private volatile boolean idle = true; // read/write only synchronized
+ private volatile boolean closed = true;
+ private volatile int callbacks = 0;
+ private volatile int watchdogCount;
+
+ private Object idleSync = new Object();
+ private Object callbackSync = new Object();
+
+ private volatile int lastExitCode = 0;
+ private volatile String lastMarkerSTDOUT = null;
+ private volatile String lastMarkerSTDERR = null;
+ private volatile Command command = null;
+ private volatile List buffer = null;
+
+ /**
+ * The only way to create an instance: Shell.Builder::open()
+ *
+ * @param builder Builder class to take values from
+ */
+ private Interactive(final Builder builder, final OnCommandResultListener onCommandResultListener) {
+ autoHandler = builder.autoHandler;
+ shell = builder.shell;
+ wantSTDERR = builder.wantSTDERR;
+ commands = builder.commands;
+ environment = builder.environment;
+ onSTDOUTLineListener = builder.onSTDOUTLineListener;
+ onSTDERRLineListener = builder.onSTDERRLineListener;
+ watchdogTimeout = builder.watchdogTimeout;
+
+ // If a looper is available, we offload the callbacks from the gobbling threads
+ // to whichever thread created us. Would normally do this in open(),
+ // but then we could not declare handler as final
+ if ((Looper.myLooper() != null) && (builder.handler == null) && autoHandler) {
+ handler = new Handler();
+ } else {
+ handler = builder.handler;
+ }
+
+ boolean ret = open();
+ if (onCommandResultListener == null) {
+ return;
+ } else if (ret == false) {
+ onCommandResultListener.onCommandResult(0, OnCommandResultListener.SHELL_EXEC_FAILED, null);
+ return;
+ }
+
+ // Allow up to 60 seconds for SuperSU/Superuser dialog, then enable the user-specified
+ // timeout for all subsequent operations
+ watchdogTimeout = 60;
+ addCommand(Shell.availableTestCommands, 0, new OnCommandResultListener() {
+ public void onCommandResult(int commandCode, int exitCode, List output) {
+ if (exitCode == OnCommandResultListener.SHELL_RUNNING &&
+ Shell.parseAvailableResult(output, Shell.SU.isSU(shell)) != true) {
+ // shell is up, but it's brain-damaged
+ exitCode = OnCommandResultListener.SHELL_WRONG_UID;
+ }
+ watchdogTimeout = builder.watchdogTimeout;
+ onCommandResultListener.onCommandResult(0, exitCode, output);
+ }
+ });
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ if (!closed && Debug.getSanityChecksEnabledEffective()) {
+ // waste of resources
+ Debug.log(ShellNotClosedException.EXCEPTION_NOT_CLOSED);
+ throw new ShellNotClosedException();
+ }
+ super.finalize();
+ }
+
+ /**
+ * Add a command to execute
+ *
+ * @param command Command to execute
+ */
+ public void addCommand(String command) { addCommand(command, 0, null); }
+
+ /**
+ * Add a command to execute, with a callback to be called on completion
+ *
+ * The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details
+ *
+ * @param command Command to execute
+ * @param code User-defined value passed back to the callback
+ * @param onCommandResultListener Callback to be called on completion
+ */
+ public void addCommand(String command, int code, OnCommandResultListener onCommandResultListener) { addCommand(new String[] { command }, code, onCommandResultListener); }
+
+ /**
+ * Add commands to execute
+ *
+ * @param commands Commands to execute
+ */
+ public void addCommand(List commands) { addCommand(commands, 0, null); }
+
+ /**
+ * Add commands to execute, with a callback to be called on completion (of all commands)
+ *
+ * The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details
+ *
+ * @param commands Commands to execute
+ * @param code User-defined value passed back to the callback
+ * @param onCommandResultListener Callback to be called on completion (of all commands)
+ */
+ public void addCommand(List commands, int code, OnCommandResultListener onCommandResultListener) { addCommand(commands.toArray(new String[commands.size()]), code, onCommandResultListener); }
+
+ /**
+ * Add commands to execute
+ *
+ * @param commands Commands to execute
+ */
+ public void addCommand(String[] commands) { addCommand(commands, 0, null); }
+
+ /**
+ * Add commands to execute, with a callback to be called on completion (of all commands)
+ *
+ * The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details
+ *
+ * @param commands Commands to execute
+ * @param code User-defined value passed back to the callback
+ * @param onCommandResultListener Callback to be called on completion (of all commands)
+ */
+ public synchronized void addCommand(String[] commands, int code, OnCommandResultListener onCommandResultListener) {
+ this.commands.add(new Command(commands, code, onCommandResultListener));
+ runNextCommand();
+ }
+
+ /**
+ * Run the next command if any and if ready, signals idle state if no commands left
+ */
+ private void runNextCommand() {
+ runNextCommand(true);
+ }
+
+ /**
+ * Called from a ScheduledThreadPoolExecutor timer thread every second when there is an outstanding command
+ */
+ private synchronized void handleWatchdog() {
+ final int exitCode;
+
+ if (watchdog == null) return;
+ if (watchdogTimeout == 0) return;
+
+ if (!isRunning()) {
+ exitCode = OnCommandResultListener.SHELL_DIED;
+ Debug.log(String.format("[%s%%] SHELL_DIED", shell.toUpperCase(Locale.ENGLISH)));
+ } else if (watchdogCount++ < watchdogTimeout) {
+ return;
+ } else {
+ exitCode = OnCommandResultListener.WATCHDOG_EXIT;
+ Debug.log(String.format("[%s%%] WATCHDOG_EXIT", shell.toUpperCase(Locale.ENGLISH)));
+ }
+
+ if (handler != null) {
+ postCallback(command, exitCode, buffer);
+ }
+
+ // prevent multiple callbacks for the same command
+ command = null;
+ buffer = null;
+ idle = true;
+
+ watchdog.shutdown();
+ watchdog = null;
+ kill();
+ }
+
+ /**
+ * Start the periodic timer when a command is submitted
+ */
+ private void startWatchdog() {
+ if (watchdogTimeout == 0) {
+ return;
+ }
+ watchdogCount = 0;
+ watchdog = new ScheduledThreadPoolExecutor(1);
+ watchdog.scheduleAtFixedRate(new Runnable() {
+ @Override
+ public void run() {
+ handleWatchdog();
+ }
+ }, 1, 1, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Disable the watchdog timer upon command completion
+ */
+ private void stopWatchdog() {
+ if (watchdog != null) {
+ watchdog.shutdownNow();
+ watchdog = null;
+ }
+ }
+
+ /**
+ * Run the next command if any and if ready
+ *
+ * @param notifyIdle signals idle state if no commands left ?
+ */
+ private void runNextCommand(boolean notifyIdle) {
+ // must always be called from a synchronized method
+
+ boolean running = isRunning();
+ if (!running) idle = true;
+
+ if (running && idle && (commands.size() > 0)) {
+ Command command = commands.get(0);
+ commands.remove(0);
+
+ buffer = null;
+ lastExitCode = 0;
+ lastMarkerSTDOUT = null;
+ lastMarkerSTDERR = null;
+
+ if (command.commands.length > 0) {
+ try {
+ if (command.onCommandResultListener != null) {
+ // no reason to store the output if we don't have an OnCommandResultListener
+ // user should catch the output with an OnLineListener in this case
+ buffer = Collections.synchronizedList(new ArrayList());
+ }
+
+ idle = false;
+ this.command = command;
+ startWatchdog();
+ for (String write : command.commands) {
+ Debug.logCommand(String.format("[%s+] %s", shell.toUpperCase(Locale.ENGLISH), write));
+ STDIN.write((write + "\n").getBytes("UTF-8"));
+ }
+ STDIN.write(("echo " + command.marker + " $?\n").getBytes("UTF-8"));
+ STDIN.write(("echo " + command.marker + " >&2\n").getBytes("UTF-8"));
+ STDIN.flush();
+ } catch (IOException e) {
+ }
+ } else {
+ runNextCommand(false);
+ }
+ } else if (!running) {
+ // our shell died for unknown reasons - abort all submissions
+ while (commands.size() > 0) {
+ postCallback(commands.remove(0), OnCommandResultListener.SHELL_DIED, null);
+ }
+ }
+
+ if (idle && notifyIdle) {
+ synchronized(idleSync) {
+ idleSync.notifyAll();
+ }
+ }
+ }
+
+ /**
+ * Processes a STDOUT/STDERR line containing an end/exitCode marker
+ */
+ private synchronized void processMarker() {
+ if (command.marker.equals(lastMarkerSTDOUT) && (command.marker.equals(lastMarkerSTDERR))) {
+ if (buffer != null) {
+ postCallback(command, lastExitCode, buffer);
+ }
+
+ stopWatchdog();
+ command = null;
+ buffer = null;
+ idle = true;
+ runNextCommand();
+ }
+ }
+
+ /**
+ * Process a normal STDOUT/STDERR line
+ *
+ * @param line Line to process
+ * @param listener Callback to call or null
+ */
+ private synchronized void processLine(String line, OnLineListener listener) {
+ if (listener != null) {
+ if (handler != null) {
+ final String fLine = line;
+ final OnLineListener fListener = listener;
+
+ startCallback();
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ fListener.onLine(fLine);
+ } finally {
+ endCallback();
+ }
+ }
+ });
+ } else {
+ listener.onLine(line);
+ }
+ }
+ }
+
+ /**
+ * Add line to internal buffer
+ *
+ * @param line Line to add
+ */
+ private synchronized void addBuffer(String line) {
+ if (buffer != null) {
+ buffer.add(line);
+ }
+ }
+
+ /**
+ * Increase callback counter
+ */
+ private void startCallback() {
+ synchronized (callbackSync) {
+ callbacks++;
+ }
+ }
+
+ /**
+ * Schedule a callback to run on the appropriate thread
+ */
+ private void postCallback(final Command fCommand, final int fExitCode, final List fOutput) {
+ if (fCommand.onCommandResultListener == null) {
+ return;
+ }
+ if (handler == null) {
+ fCommand.onCommandResultListener.onCommandResult(fCommand.code, fExitCode, fOutput);
+ return;
+ }
+ startCallback();
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ fCommand.onCommandResultListener.onCommandResult(fCommand.code, fExitCode, fOutput);
+ } finally {
+ endCallback();
+ }
+ }
+ });
+ }
+
+ /**
+ * Decrease callback counter, signals callback complete state when dropped to 0
+ */
+ private void endCallback() {
+ synchronized (callbackSync) {
+ callbacks--;
+ if (callbacks == 0) {
+ callbackSync.notifyAll();
+ }
+ }
+ }
+
+ /**
+ * Internal call that launches the shell, starts gobbling, and starts executing commands.
+ * See {@link Shell.Interactive}
+ *
+ * @return Opened successfully ?
+ */
+ private synchronized boolean open() {
+ Debug.log(String.format("[%s%%] START", shell.toUpperCase(Locale.ENGLISH)));
+
+ try {
+ // setup our process, retrieve STDIN stream, and STDOUT/STDERR gobblers
+ if (environment.size() == 0) {
+ process = Runtime.getRuntime().exec(shell);
+ } else {
+ Map newEnvironment = new HashMap();
+ newEnvironment.putAll(System.getenv());
+ newEnvironment.putAll(environment);
+ int i = 0;
+ String[] env = new String[newEnvironment.size()];
+ for (Map.Entry entry : newEnvironment.entrySet()) {
+ env[i] = entry.getKey() + "=" + entry.getValue();
+ i++;
+ }
+ process = Runtime.getRuntime().exec(shell, env);
+ }
+
+ STDIN = new DataOutputStream(process.getOutputStream());
+ STDOUT = new StreamGobbler(shell.toUpperCase(Locale.ENGLISH) + "-", process.getInputStream(), new OnLineListener() {
+ @Override
+ public void onLine(String line) {
+ synchronized (Interactive.this) {
+ if (command == null) {
+ return;
+ }
+ if (line.startsWith(command.marker)) {
+ try {
+ lastExitCode = Integer.valueOf(line.substring(command.marker.length() + 1), 10);
+ } catch (Exception e) {
+ }
+ lastMarkerSTDOUT = command.marker;
+ processMarker();
+ } else {
+ addBuffer(line);
+ processLine(line, onSTDOUTLineListener);
+ }
+ }
+ }
+ });
+ STDERR = new StreamGobbler(shell.toUpperCase(Locale.ENGLISH) + "*", process.getErrorStream(), new OnLineListener() {
+ @Override
+ public void onLine(String line) {
+ synchronized (Interactive.this) {
+ if (command == null) {
+ return;
+ }
+ if (line.startsWith(command.marker)) {
+ lastMarkerSTDERR = command.marker;
+ processMarker();
+ } else {
+ if (wantSTDERR) addBuffer(line);
+ processLine(line, onSTDERRLineListener);
+ }
+ }
+ }
+ });
+
+ // start gobbling and write our commands to the shell
+ STDOUT.start();
+ STDERR.start();
+
+ running = true;
+ closed = false;
+
+ runNextCommand();
+
+ return true;
+ } catch (IOException e) {
+ // shell probably not found
+ return false;
+ }
+ }
+
+ /**
+ * Close shell and clean up all resources. Call this when you are done with the shell.
+ * If the shell is not idle (all commands completed) you should not call this method
+ * from the main UI thread because it may block for a long time. This method will
+ * intentionally crash your app (if in debug mode) if you try to do this anyway.
+ */
+ public void close() {
+ boolean _idle = isIdle(); // idle must be checked synchronized
+
+ synchronized (this) {
+ if (!running) return;
+ running = false;
+ closed = true;
+ }
+
+ // This method should not be called from the main thread unless the shell is idle
+ // and can be cleaned up with (minimal) waiting. Only throw in debug mode.
+ if (!_idle && Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) {
+ Debug.log(ShellOnMainThreadException.EXCEPTION_NOT_IDLE);
+ throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_NOT_IDLE);
+ }
+
+ if (!_idle) waitForIdle();
+
+ try {
+ STDIN.write(("exit\n").getBytes("UTF-8"));
+ STDIN.flush();
+
+ // wait for our process to finish, while we gobble away in the background
+ process.waitFor();
+
+ // make sure our threads are done gobbling, our streams are closed, and the process is
+ // destroyed - while the latter two shouldn't be needed in theory, and may even produce
+ // warnings, in "normal" Java they are required for guaranteed cleanup of resources, so
+ // lets be safe and do this on Android as well
+ try {
+ STDIN.close();
+ } catch (IOException e) {
+ }
+ STDOUT.join();
+ STDERR.join();
+ stopWatchdog();
+ process.destroy();
+ } catch (IOException e) {
+ // shell probably not found
+ } catch (InterruptedException e) {
+ // this should really be re-thrown
+ }
+
+ Debug.log(String.format("[%s%%] END", shell.toUpperCase(Locale.ENGLISH)));
+ }
+
+ /**
+ * Try to clean up as much as possible from a shell that's gotten itself wedged.
+ * Hopefully the StreamGobblers will croak on their own when the other side of
+ * the pipe is closed.
+ */
+ public synchronized void kill() {
+ running = false;
+ closed = true;
+
+ try {
+ STDIN.close();
+ } catch (IOException e) {
+ }
+ try {
+ process.destroy();
+ } catch (Exception e) {
+ }
+ }
+
+ /**
+ * Is our shell still running ?
+ *
+ * @return Shell running ?
+ */
+ public boolean isRunning() {
+ try {
+ // if this throws, we're still running
+ process.exitValue();
+ return false;
+ } catch (IllegalThreadStateException e) {
+ }
+ return true;
+ }
+
+ /**
+ * Have all commands completed executing ?
+ *
+ * @return Shell idle ?
+ */
+ public synchronized boolean isIdle() {
+ if (!isRunning()) {
+ idle = true;
+ synchronized(idleSync) {
+ idleSync.notifyAll();
+ }
+ }
+ return idle;
+ }
+
+ /**
+ * Wait for idle state. As this is a blocking call, you should not call it from the main UI thread.
+ * If you do so and debug mode is enabled, this method will intentionally crash your app.
+ *
+ * If not interrupted, this method will not return until all commands have finished executing.
+ * Note that this does not necessarily mean that all the callbacks have fired yet.
+ *
+ * If no Handler is used, all callbacks will have been executed when this method returns. If
+ * a Handler is used, and this method is called from a different thread than associated with the
+ * Handler's Looper, all callbacks will have been executed when this method returns as well.
+ * If however a Handler is used but this method is called from the same thread as associated
+ * with the Handler's Looper, there is no way to know.
+ *
+ * In practice this means that in most simple cases all callbacks will have completed when this
+ * method returns, but if you actually depend on this behavior, you should make certain this is
+ * indeed the case.
+ *
+ * See {@link Shell.Interactive} for further details on threading and handlers
+ *
+ * @return True if wait complete, false if wait interrupted
+ */
+ public boolean waitForIdle() {
+ if (Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) {
+ Debug.log(ShellOnMainThreadException.EXCEPTION_WAIT_IDLE);
+ throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_WAIT_IDLE);
+ }
+
+ if (isRunning()) {
+ synchronized (idleSync) {
+ while (!idle) {
+ try {
+ idleSync.wait();
+ } catch (InterruptedException e) {
+ return false;
+ }
+ }
+ }
+
+ if (
+ (handler != null) &&
+ (handler.getLooper() != null) &&
+ (handler.getLooper() != Looper.myLooper())
+ ) {
+ // If the callbacks are posted to a different thread than this one, we can wait until
+ // all callbacks have called before returning. If we don't use a Handler at all,
+ // the callbacks are already called before we get here. If we do use a Handler but
+ // we use the same Looper, waiting here would actually block the callbacks from being
+ // called
+
+ synchronized (callbackSync) {
+ while (callbacks > 0) {
+ try {
+ callbackSync.wait();
+ } catch (InterruptedException e) {
+ return false;
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Are we using a Handler to post callbacks ?
+ *
+ * @return Handler used ?
+ */
+ public boolean hasHandler() {
+ return (handler != null);
+ }
+ }
+}
diff --git a/src/main/java/eu/chainfire/libsuperuser/ShellNotClosedException.java b/src/main/java/eu/chainfire/libsuperuser/ShellNotClosedException.java
new file mode 100644
index 00000000..fe31c0f3
--- /dev/null
+++ b/src/main/java/eu/chainfire/libsuperuser/ShellNotClosedException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2012-2013 Jorrit "Chainfire" Jongma
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package eu.chainfire.libsuperuser;
+
+/**
+ * Exception class used to notify developer that a shell was not close()d
+ */
+@SuppressWarnings("serial")
+public class ShellNotClosedException extends RuntimeException {
+ public static final String EXCEPTION_NOT_CLOSED = "Application did not close() interactive shell";
+
+ public ShellNotClosedException() {
+ super(EXCEPTION_NOT_CLOSED);
+ }
+}
diff --git a/src/main/java/eu/chainfire/libsuperuser/ShellOnMainThreadException.java b/src/main/java/eu/chainfire/libsuperuser/ShellOnMainThreadException.java
new file mode 100644
index 00000000..acb7764d
--- /dev/null
+++ b/src/main/java/eu/chainfire/libsuperuser/ShellOnMainThreadException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2012-2013 Jorrit "Chainfire" Jongma
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package eu.chainfire.libsuperuser;
+
+/**
+ * Exception class used to crash application when shell commands are executed
+ * from the main thread, and we are in debug mode.
+ */
+@SuppressWarnings("serial")
+public class ShellOnMainThreadException extends RuntimeException {
+ public static final String EXCEPTION_COMMAND = "Application attempted to run a shell command from the main thread";
+ public static final String EXCEPTION_NOT_IDLE = "Application attempted to wait for a non-idle shell to close on the main thread";
+ public static final String EXCEPTION_WAIT_IDLE = "Application attempted to wait for a shell to become idle on the main thread";
+
+ public ShellOnMainThreadException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/eu/chainfire/libsuperuser/StreamGobbler.java b/src/main/java/eu/chainfire/libsuperuser/StreamGobbler.java
new file mode 100644
index 00000000..04c4fe77
--- /dev/null
+++ b/src/main/java/eu/chainfire/libsuperuser/StreamGobbler.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2012-2013 Jorrit "Chainfire" Jongma
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package eu.chainfire.libsuperuser;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.List;
+
+/**
+ * Thread utility class continuously reading from an InputStream
+ */
+public class StreamGobbler extends Thread {
+ /**
+ * Line callback interface
+ */
+ public interface OnLineListener {
+ /**
+ * Line callback
+ *
+ * This callback should process the line as quickly as possible.
+ * Delays in this callback may pause the native process or even
+ * result in a deadlock
+ *
+ * @param line String that was gobbled
+ */
+ public void onLine(String line);
+ }
+
+ private String shell = null;
+ private BufferedReader reader = null;
+ private List writer = null;
+ private OnLineListener listener = null;
+
+ /**
+ * StreamGobbler constructor
+ *
+ * We use this class because shell STDOUT and STDERR should be read as quickly as
+ * possible to prevent a deadlock from occurring, or Process.waitFor() never
+ * returning (as the buffer is full, pausing the native process)
+ *
+ * @param shell Name of the shell
+ * @param inputStream InputStream to read from
+ * @param outputList List to write to, or null
+ */
+ public StreamGobbler(String shell, InputStream inputStream, List outputList) {
+ this.shell = shell;
+ reader = new BufferedReader(new InputStreamReader(inputStream));
+ writer = outputList;
+ }
+
+ /**
+ * StreamGobbler constructor
+ *
+ * We use this class because shell STDOUT and STDERR should be read as quickly as
+ * possible to prevent a deadlock from occurring, or Process.waitFor() never
+ * returning (as the buffer is full, pausing the native process)
+ *
+ * @param shell Name of the shell
+ * @param inputStream InputStream to read from
+ * @param onLineListener OnLineListener callback
+ */
+ public StreamGobbler(String shell, InputStream inputStream, OnLineListener onLineListener) {
+ this.shell = shell;
+ reader = new BufferedReader(new InputStreamReader(inputStream));
+ listener = onLineListener;
+ }
+
+ @Override
+ public void run() {
+ // keep reading the InputStream until it ends (or an error occurs)
+ try {
+ String line = null;
+ while ((line = reader.readLine()) != null) {
+ Debug.logOutput(String.format("[%s] %s", shell, line));
+ if (writer != null) writer.add(line);
+ if (listener != null) listener.onLine(line);
+ }
+ } catch (IOException e) {
+ }
+
+ // make sure our stream is closed and resources will be freed
+ try {
+ reader.close();
+ } catch (IOException e) {
+ }
+ }
+}
diff --git a/src/main/jni/Android.mk b/src/main/jni/Android.mk
index eaeb435e..1006cd5b 100755
--- a/src/main/jni/Android.mk
+++ b/src/main/jni/Android.mk
@@ -130,21 +130,6 @@ LOCAL_LDLIBS := -llog
include $(BUILD_EXECUTABLE)
-########################################################
-## termExec
-########################################################
-
-include $(CLEAR_VARS)
-
-LOCAL_MODULE:= exec
-
-LOCAL_SRC_FILES:= \
- termExec.cpp
-
-LOCAL_LDLIBS := -ldl -llog
-
-include $(BUILD_SHARED_LIBRARY)
-
########################################################
## system
########################################################
diff --git a/src/main/jni/termExec.cpp b/src/main/jni/termExec.cpp
deleted file mode 100644
index 55623137..00000000
--- a/src/main/jni/termExec.cpp
+++ /dev/null
@@ -1,428 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#define LOG_TAG "Shadowsocks"
-
-#include "jni.h"
-#include
-
-#define LOGI(...) do { __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__); } while(0)
-#define LOGW(...) do { __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__); } while(0)
-#define LOGE(...) do { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__); } while(0)
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-static jclass class_fileDescriptor;
-static jfieldID field_fileDescriptor_descriptor;
-static jmethodID method_fileDescriptor_init;
-
-typedef unsigned short char16_t;
-
-class String8 {
-public:
- String8() {
- mString = 0;
- }
-
- ~String8() {
- if (mString) {
- free(mString);
- }
- }
-
- void set(const char16_t* o, size_t numChars) {
- if (mString) {
- free(mString);
- }
- mString = (char*) malloc(numChars + 1);
- if (!mString) {
- return;
- }
- for (size_t i = 0; i < numChars; i++) {
- mString[i] = (char) o[i];
- }
- mString[numChars] = '\0';
- }
-
- const char* string() {
- return mString;
- }
-private:
- char* mString;
-};
-
-static int throwOutOfMemoryError(JNIEnv *env, const char *message)
-{
- jclass exClass;
- const char *className = "java/lang/OutOfMemoryError";
-
- exClass = env->FindClass(className);
- return env->ThrowNew(exClass, message);
-}
-
-static int create_subprocess(const int rdt, const char *cmd, char *const argv[],
- char *const envp[], const char* scripts, int* pProcessId)
-{
- pid_t pid;
- int pfds[2];
- int pfds2[2];
-
- pipe(pfds);
-
- if (rdt) {
- pipe(pfds2);
- }
-
- pid = fork();
-
- if(pid < 0) {
- LOGE("- fork failed: %s -\n", strerror(errno));
- return -1;
- }
-
- if(pid == 0){
-
- signal(SIGPIPE, SIG_IGN);
-
- if (envp) {
- for (; *envp; ++envp) {
- putenv(*envp);
- }
- }
-
- dup2(pfds[0], 0);
- close(pfds[1]);
-
- if (rdt) {
- close(1);
- close(2);
- dup2(pfds2[1], 1);
- dup2(pfds2[1], 2);
- close(pfds2[0]);
- }
-
- execv(cmd, argv);
-
- fflush(NULL);
-
- exit(0);
-
- } else {
-
- signal(SIGPIPE, SIG_IGN);
-
- *pProcessId = (int) pid;
-
- dup2(pfds[1], 1);
- close(pfds[0]);
-
- write(pfds[1], scripts, strlen(scripts)+1);
- if (rdt) {
- close(pfds2[1]);
- return pfds2[0];
- } else {
- return -1;
- }
- }
-}
-
-
-static jobject android_os_Exec_createSubProcess(JNIEnv *env, jobject clazz,
- jint rdt, jstring cmd, jobjectArray args, jobjectArray envVars, jstring scripts,
- jintArray processIdArray)
-{
- const jchar* str = cmd ? env->GetStringCritical(cmd, 0) : 0;
- String8 cmd_8;
- if (str) {
- cmd_8.set(str, env->GetStringLength(cmd));
- env->ReleaseStringCritical(cmd, str);
- }
-
- const jchar* str_scripts = scripts ? env->GetStringCritical(scripts, 0) : 0;
- String8 scripts_8;
- if (str_scripts) {
- scripts_8.set(str_scripts, env->GetStringLength(scripts));
- env->ReleaseStringCritical(scripts, str_scripts);
- }
-
- jsize size = args ? env->GetArrayLength(args) : 0;
- char **argv = NULL;
- String8 tmp_8;
- if (size > 0) {
- argv = (char **)malloc((size+1)*sizeof(char *));
- if (!argv) {
- throwOutOfMemoryError(env, "Couldn't allocate argv array");
- return NULL;
- }
- for (int i = 0; i < size; ++i) {
- jstring arg = reinterpret_cast(env->GetObjectArrayElement(args, i));
- str = env->GetStringCritical(arg, 0);
- if (!str) {
- throwOutOfMemoryError(env, "Couldn't get argument from array");
- return NULL;
- }
- tmp_8.set(str, env->GetStringLength(arg));
- env->ReleaseStringCritical(arg, str);
- argv[i] = strdup(tmp_8.string());
- }
- argv[size] = NULL;
- }
-
- size = envVars ? env->GetArrayLength(envVars) : 0;
- char **envp = NULL;
- if (size > 0) {
- envp = (char **)malloc((size+1)*sizeof(char *));
- if (!envp) {
- throwOutOfMemoryError(env, "Couldn't allocate envp array");
- return NULL;
- }
- for (int i = 0; i < size; ++i) {
- jstring var = reinterpret_cast(env->GetObjectArrayElement(envVars, i));
- str = env->GetStringCritical(var, 0);
- if (!str) {
- throwOutOfMemoryError(env, "Couldn't get env var from array");
- return NULL;
- }
- tmp_8.set(str, env->GetStringLength(var));
- env->ReleaseStringCritical(var, str);
- envp[i] = strdup(tmp_8.string());
- }
- envp[size] = NULL;
- }
-
- int procId;
- int ptm = create_subprocess(rdt, cmd_8.string(), argv, envp, scripts_8.string(),
- &procId);
-
- if (argv) {
- for (char **tmp = argv; *tmp; ++tmp) {
- free(*tmp);
- }
- free(argv);
- }
- if (envp) {
- for (char **tmp = envp; *tmp; ++tmp) {
- free(*tmp);
- }
- free(envp);
- }
-
- if (processIdArray) {
- int procIdLen = env->GetArrayLength(processIdArray);
- if (procIdLen > 0) {
- jboolean isCopy;
-
- int* pProcId = (int*) env->GetPrimitiveArrayCritical(processIdArray, &isCopy);
- if (pProcId) {
- *pProcId = procId;
- env->ReleasePrimitiveArrayCritical(processIdArray, pProcId, 0);
- }
- }
- }
-
- jobject result = env->NewObject(class_fileDescriptor, method_fileDescriptor_init);
-
- if (!result) {
- LOGE("Couldn't create a FileDescriptor.");
- }
- else {
- env->SetIntField(result, field_fileDescriptor_descriptor, ptm);
- }
-
- return result;
-}
-
-
-static int android_os_Exec_waitFor(JNIEnv *env, jobject clazz,
- jint procId) {
- int status;
- waitpid(procId, &status, 0);
- int result = 0;
- if (WIFEXITED(status)) {
- result = WEXITSTATUS(status);
- }
- return result;
-}
-
-static void android_os_Exec_close(JNIEnv *env, jobject clazz, jobject fileDescriptor)
-{
- int fd;
-
- fd = env->GetIntField(fileDescriptor, field_fileDescriptor_descriptor);
-
- if (env->ExceptionOccurred() != NULL) {
- return;
- }
-
- close(fd);
-}
-
-static void android_os_Exec_hangupProcessGroup(JNIEnv *env, jobject clazz,
- jint procId) {
- kill(-procId, SIGHUP);
-}
-
-
-static int register_FileDescriptor(JNIEnv *env)
-{
- jclass localRef_class_fileDescriptor = env->FindClass("java/io/FileDescriptor");
-
- if (localRef_class_fileDescriptor == NULL) {
- LOGE("Can't find class java/io/FileDescriptor");
- return -1;
- }
-
- class_fileDescriptor = (jclass) env->NewGlobalRef(localRef_class_fileDescriptor);
-
- env->DeleteLocalRef(localRef_class_fileDescriptor);
-
- if (class_fileDescriptor == NULL) {
- LOGE("Can't get global ref to class java/io/FileDescriptor");
- return -1;
- }
-
- field_fileDescriptor_descriptor = env->GetFieldID(class_fileDescriptor, "descriptor", "I");
-
- if (field_fileDescriptor_descriptor == NULL) {
- LOGE("Can't find FileDescriptor.descriptor");
- return -1;
- }
-
- method_fileDescriptor_init = env->GetMethodID(class_fileDescriptor, "", "()V");
- if (method_fileDescriptor_init == NULL) {
- LOGE("Can't find FileDescriptor.init");
- return -1;
- }
- return 0;
-}
-
-
-static const char *classPathName = "com/github/shadowsocks/Exec";
-
-static JNINativeMethod method_table[] = {
- { "createSubprocess", "(ILjava/lang/String;[Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;[I)Ljava/io/FileDescriptor;",
- (void*) android_os_Exec_createSubProcess },
- { "waitFor", "(I)I",
- (void*) android_os_Exec_waitFor},
- { "close", "(Ljava/io/FileDescriptor;)V",
- (void*) android_os_Exec_close},
- { "hangupProcessGroup", "(I)V",
- (void*) android_os_Exec_hangupProcessGroup}
-};
-
-/*
- * Register several native methods for one class.
- */
-static int registerNativeMethods(JNIEnv* env, const char* className,
- JNINativeMethod* gMethods, int numMethods)
-{
- jclass clazz;
-
- clazz = env->FindClass(className);
- if (clazz == NULL) {
- LOGE("Native registration unable to find class '%s'", className);
- return JNI_FALSE;
- }
- if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
- LOGE("RegisterNatives failed for '%s'", className);
- return JNI_FALSE;
- }
-
- return JNI_TRUE;
-}
-
-/*
- * Register native methods for all classes we know about.
- *
- * returns JNI_TRUE on success.
- */
-static int registerNatives(JNIEnv* env)
-{
- if (!registerNativeMethods(env, classPathName, method_table,
- sizeof(method_table) / sizeof(method_table[0]))) {
- return JNI_FALSE;
- }
-
- return JNI_TRUE;
-}
-
-
-// ----------------------------------------------------------------------------
-
-/*
- * This is called by the VM when the shared library is first loaded.
- */
-
-typedef union {
- JNIEnv* env;
- void* venv;
-} UnionJNIEnvToVoid;
-
-jint JNI_OnLoad(JavaVM* vm, void* reserved) {
- UnionJNIEnvToVoid uenv;
- uenv.venv = NULL;
- jint result = -1;
- JNIEnv* env = NULL;
-
- LOGI("JNI_OnLoad");
-
- if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) {
- LOGE("ERROR: GetEnv failed");
- goto bail;
- }
- env = uenv.env;
-
- if ((result = register_FileDescriptor(env)) < 0) {
- LOGE("ERROR: registerFileDescriptor failed");
- goto bail;
- }
-
- if (registerNatives(env) != JNI_TRUE) {
- LOGE("ERROR: registerNatives failed");
- goto bail;
- }
-
- result = JNI_VERSION_1_4;
-
-bail:
- return result;
-}
diff --git a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala
index 929ec817..f443475f 100644
--- a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala
+++ b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala
@@ -49,7 +49,6 @@ import android.view._
import android.widget._
import com.google.analytics.tracking.android.{MapBuilder, EasyTracker}
import de.keyboardsurfer.android.widget.crouton.{Crouton, Style, Configuration}
-import java.io._
import java.util.Hashtable
import net.saik0.android.unifiedpreference.UnifiedPreferenceFragment
import net.saik0.android.unifiedpreference.UnifiedSherlockPreferenceActivity
@@ -63,7 +62,7 @@ import com.google.ads.{AdRequest, AdSize, AdView}
import net.simonvt.menudrawer.MenuDrawer
import com.github.shadowsocks.database._
-import scala.collection.mutable.ListBuffer
+import scala.collection.mutable.{ArrayBuffer, ListBuffer}
import com.github.shadowsocks.database.Profile
import com.nostra13.universalimageloader.core.download.BaseImageDownloader
import com.github.shadowsocks.preferences.{ProfileEditTextPreference, PasswordEditTextPreference, SummaryEditTextPreference}
@@ -72,6 +71,11 @@ import com.github.shadowsocks.database.Item
import com.github.shadowsocks.database.Category
import com.google.zxing.integration.android.IntentIntegrator
import com.github.shadowsocks.aidl.{IShadowsocksServiceCallback, IShadowsocksService}
+import java.io._
+import scala.Some
+import com.github.shadowsocks.database.Item
+import com.github.shadowsocks.database.Category
+import com.github.shadowsocks.utils.Console
class ProfileIconDownloader(context: Context, connectTimeout: Int, readTimeout: Int)
extends BaseImageDownloader(context, connectTimeout, readTimeout) {
@@ -118,13 +122,17 @@ object Typefaces {
object Shadowsocks {
// Constants
+ val TAG = "Shadowsocks"
+ val REQUEST_CONNECT = 1
+
val PREFS_NAME = "Shadowsocks"
val PROXY_PREFS = Array(Key.profileName, Key.proxy, Key.remotePort, Key.localPort, Key.sitekey,
Key.encMethod)
val FEATRUE_PREFS = Array(Key.isGFWList, Key.isGlobalProxy, Key.proxyedApps, Key.isTrafficStat,
Key.isUdpDns, Key.isAutoConnect)
- val TAG = "Shadowsocks"
- val REQUEST_CONNECT = 1
+
+ val EXECUTABLES = Array(Executable.IPTABLES, Executable.PDNSD, Executable.REDSOCKS,
+ Executable.SS_LOCAL, Executable.SS_TUNNEL, Executable.TUN2SOCKS)
// Helper functions
def updateListPreference(pref: Preference, value: String) {
@@ -415,26 +423,28 @@ class Shadowsocks
}
private def crash_recovery() {
- val sb = new StringBuilder
+ val ab = new ArrayBuffer[String]
- sb.append("kill -9 `cat /data/data/com.github.shadowsocks/pdnsd.pid`").append("\n")
- sb.append("kill -9 `cat /data/data/com.github.shadowsocks/ss-local.pid`").append("\n")
- sb.append("kill -9 `cat /data/data/com.github.shadowsocks/ss-tunnel.pid`").append("\n")
- sb.append("kill -9 `cat /data/data/com.github.shadowsocks/tun2socks.pid`").append("\n")
- sb.append("killall -9 pdnsd").append("\n")
- sb.append("killall -9 ss-local").append("\n")
- sb.append("killall -9 ss-tunnel").append("\n")
- sb.append("killall -9 tun2socks").append("\n")
- sb.append("rm /data/data/com.github.shadowsocks/pdnsd.conf").append("\n")
- sb.append("rm /data/data/com.github.shadowsocks/pdnsd.cache").append("\n")
- Utils.runCommand(sb.toString())
+ ab.append("kill -9 `cat /data/data/com.github.shadowsocks/pdnsd.pid`")
+ ab.append("kill -9 `cat /data/data/com.github.shadowsocks/ss-local.pid`")
+ ab.append("kill -9 `cat /data/data/com.github.shadowsocks/ss-tunnel.pid`")
+ ab.append("kill -9 `cat /data/data/com.github.shadowsocks/tun2socks.pid`")
+ ab.append("killall -9 pdnsd")
+ ab.append("killall -9 ss-local")
+ ab.append("killall -9 ss-tunnel")
+ ab.append("killall -9 tun2socks")
+ ab.append("rm /data/data/com.github.shadowsocks/pdnsd.conf")
+ ab.append("rm /data/data/com.github.shadowsocks/pdnsd.cache")
- sb.clear()
- sb.append("kill -9 `cat /data/data/com.github.shadowsocks/redsocks.pid`").append("\n")
- sb.append("killall -9 redsocks").append("\n")
- sb.append("rm /data/data/com.github.shadowsocks/redsocks.conf").append("\n")
- sb.append(Utils.getIptables).append(" -t nat -F OUTPUT").append("\n")
- Utils.runRootCommand(sb.toString())
+ Console.runCommand(ab.toArray)
+
+ ab.clear()
+ ab.append("kill -9 `cat /data/data/com.github.shadowsocks/redsocks.pid`")
+ ab.append("killall -9 redsocks")
+ ab.append("rm /data/data/com.github.shadowsocks/redsocks.conf")
+ ab.append(Utils.getIptables + " -t nat -F OUTPUT")
+
+ Console.runRootCommand(ab.toArray)
}
private def getVersionName: String = {
@@ -587,7 +597,7 @@ class Shadowsocks
// Bind to the service
spawn {
- val isRoot = Utils.getRoot
+ val isRoot = Console.isRoot
handler.post(new Runnable {
override def run() {
status.edit.putBoolean(Key.isRoot, isRoot).commit()
@@ -908,12 +918,12 @@ class Shadowsocks
def reset() {
crash_recovery()
copyAssets(System.getABI)
- Utils.runCommand("chmod 755 /data/data/com.github.shadowsocks/iptables\n"
- + "chmod 755 /data/data/com.github.shadowsocks/redsocks\n"
- + "chmod 755 /data/data/com.github.shadowsocks/pdnsd\n"
- + "chmod 755 /data/data/com.github.shadowsocks/ss-local\n"
- + "chmod 755 /data/data/com.github.shadowsocks/ss-tunnel\n"
- + "chmod 755 /data/data/com.github.shadowsocks/tun2socks\n")
+
+ val ab = new ArrayBuffer[String]
+ for (executable <- Shadowsocks.EXECUTABLES) {
+ ab.append("chmod 755 " + Path.BASE + executable)
+ }
+ Console.runCommand(ab.toArray)
}
private def recovery() {
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
index e102c22e..4d26e05c 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
@@ -50,7 +50,6 @@ import android.os._
import android.support.v4.app.NotificationCompat
import android.util.Log
import com.google.analytics.tracking.android.{Fields, MapBuilder, EasyTracker}
-import java.io._
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import org.apache.http.conn.util.InetAddressUtils
@@ -62,6 +61,8 @@ import com.github.shadowsocks.utils._
import scala.Some
import android.graphics.Color
import com.github.shadowsocks.aidl.Config
+import scala.collection.mutable.ArrayBuffer
+import java.io.File
case class TrafficStat(tx: Long, rx: Long, timestamp: Long)
@@ -107,7 +108,7 @@ class ShadowsocksNatService extends Service with BaseService {
Path.BASE + "ss-local.pid")
.format(config.proxy, config.remotePort, config.localPort, config.sitekey, config.encMethod)
if (BuildConfig.DEBUG) Log.d(TAG, cmd)
- Utils.runCommand(cmd)
+ System.exec(cmd)
}
def startDnsDaemon() {
@@ -123,8 +124,8 @@ class ShadowsocksNatService extends Service with BaseService {
})
Path.BASE + "pdnsd -c " + Path.BASE + "pdnsd.conf"
}
-
- Utils.runCommand(cmd)
+ if (BuildConfig.DEBUG) Log.d(TAG, cmd)
+ System.exec(cmd)
}
def getVersionName: String = {
@@ -146,7 +147,7 @@ class ShadowsocksNatService extends Service with BaseService {
ConfigUtils.printToFile(new File(Path.BASE + "redsocks.conf"))(p => {
p.println(conf)
})
- Utils.runRootCommand(cmd)
+ Console.runRootCommand(cmd)
}
/** Called when the activity is first created. */
@@ -267,24 +268,25 @@ class ShadowsocksNatService extends Service with BaseService {
}
def killProcesses() {
- Utils.runRootCommand(Utils.getIptables + " -t nat -F OUTPUT")
+ Console.runRootCommand(Utils.getIptables + " -t nat -F OUTPUT")
- val sb = new StringBuilder
+ val ab = new ArrayBuffer[String]
- sb ++= "kill -9 `cat " ++= Path.BASE ++= "redsocks.pid`" ++= "\n"
- sb ++= "killall -9 redsocks" ++= "\n"
- Utils.runRootCommand(sb.toString())
+ ab.append("kill -9 `cat " + Path.BASE +"redsocks.pid`")
+ ab.append("killall -9 redsocks")
- sb.clear()
+ Console.runRootCommand(ab.toArray)
- sb ++= "kill -9 `cat " ++= Path.BASE ++= "pdnsd.pid`" ++= "\n"
- sb ++= "killall -9 pdnsd" ++= "\n"
- sb ++= "kill -9 `cat " ++= Path.BASE ++= "ss-local.pid`" ++= "\n"
- sb ++= "killall -9 ss-local" ++= "\n"
- sb ++= "kill -9 `cat " ++= Path.BASE ++= "ss-tunnel.pid`" ++= "\n"
- sb ++= "killall -9 ss-tunnel" ++= "\n"
+ ab.clear()
- Utils.runCommand(sb.toString())
+ ab.append("kill -9 `cat " + Path.BASE + "ss-local.pid`")
+ ab.append("killall -9 ss-local")
+ ab.append("kill -9 `cat " + Path.BASE + "ss-tunnel.pid`")
+ ab.append("killall -9 ss-tunnel")
+ ab.append("kill -9 `cat " + Path.BASE + "pdnsd.pid`")
+ ab.append("killall -9 pdnsd")
+
+ Console.runRootCommand(ab.toArray)
}
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
@@ -292,8 +294,7 @@ class ShadowsocksNatService extends Service with BaseService {
}
def flushDNS() {
- Utils.runRootCommand("ndc resolver flushdefaultif\n"
- + "ndc resolver flushif wlan0\n")
+ Console.runRootCommand(Array("ndc resolver flushdefaultif", "ndc resolver flushif wlan0"))
}
def setupIptables: Boolean = {
@@ -354,9 +355,9 @@ class ShadowsocksNatService extends Service with BaseService {
}
}
val init_rules: String = init_sb.toString()
- Utils.runRootCommand(init_rules, 30 * 1000)
+ Console.runRootCommand(init_rules)
val redt_rules: String = http_sb.toString()
- Utils.runRootCommand(redt_rules)
+ Console.runRootCommand(redt_rules)
true
}
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala
index 0d901516..7efe4254 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala
@@ -73,7 +73,7 @@ class ShadowsocksRunnerActivity extends Activity {
def isVpnEnabled: Boolean = {
if (vpnEnabled < 0) {
vpnEnabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH
- && !Utils.getRoot) {
+ && !Console.isRoot) {
1
} else {
0
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
index 2903d00a..adda7930 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
@@ -47,7 +47,6 @@ import android.os._
import android.support.v4.app.NotificationCompat
import android.util.Log
import com.google.analytics.tracking.android.{Fields, MapBuilder, EasyTracker}
-import java.io._
import android.net.VpnService
import org.apache.http.conn.util.InetAddressUtils
import android.os.Message
@@ -57,6 +56,8 @@ import java.net.InetAddress
import com.github.shadowsocks.utils._
import scala.Some
import com.github.shadowsocks.aidl.{IShadowsocksService, Config}
+import scala.collection.mutable.ArrayBuffer
+import java.io.File
class ShadowsocksVpnService extends VpnService with BaseService {
@@ -96,7 +97,8 @@ class ShadowsocksVpnService extends VpnService with BaseService {
})
Path.BASE + "pdnsd -c " + Path.BASE + "pdnsd.conf"
}
- Utils.runCommand(cmd)
+ if (BuildConfig.DEBUG) Log.d(TAG, cmd)
+ System.exec(cmd)
}
def getVersionName: String = {
@@ -248,18 +250,18 @@ class ShadowsocksVpnService extends VpnService with BaseService {
}
def killProcesses() {
- val sb = new StringBuilder
+ val ab = new ArrayBuffer[String]
- sb ++= "kill -9 `cat " ++= Path.BASE ++= "ss-local.pid`" ++= "\n"
- sb ++= "killall -9 ss-local" ++= "\n"
- sb ++= "kill -9 `cat " ++= Path.BASE ++= "ss-tunnel.pid`" ++= "\n"
- sb ++= "killall -9 ss-tunnel" ++= "\n"
- sb ++= "kill -9 `cat " ++= Path.BASE ++= "tun2socks.pid`" ++= "\n"
- sb ++= "killall -9 tun2socks" ++= "\n"
- sb ++= "kill -9 `cat " ++= Path.BASE ++= "pdnsd.pid`" ++= "\n"
- sb ++= "killall -9 pdnsd" ++= "\n"
+ ab.append("kill -9 `cat " + Path.BASE + "ss-local.pid`")
+ ab.append("killall -9 ss-local")
+ ab.append("kill -9 `cat " + Path.BASE + "ss-tunnel.pid`")
+ ab.append("killall -9 ss-tunnel")
+ ab.append("kill -9 `cat " + Path.BASE + "tun2socks.pid`")
+ ab.append("killall -9 tun2socks")
+ ab.append("kill -9 `cat " + Path.BASE + "pdnsd.pid`")
+ ab.append("killall -9 pdnsd")
- Utils.runCommand(sb.toString())
+ Console.runCommand(ab.toArray)
}
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
diff --git a/src/main/scala/com/github/shadowsocks/utils/Console.scala b/src/main/scala/com/github/shadowsocks/utils/Console.scala
new file mode 100644
index 00000000..82d2c087
--- /dev/null
+++ b/src/main/scala/com/github/shadowsocks/utils/Console.scala
@@ -0,0 +1,77 @@
+package com.github.shadowsocks.utils
+
+import eu.chainfire.libsuperuser.Shell
+import eu.chainfire.libsuperuser.Shell.{OnCommandResultListener, SU, Builder}
+import java.util
+import android.util.Log
+
+object Console {
+
+ private var shell: Shell.Interactive = null
+ private var rootShell: Shell.Interactive = null
+
+ private def openShell() {
+ if (shell == null) {
+ val builder = new Builder()
+ shell = builder
+ .useSH()
+ .setWatchdogTimeout(10)
+ .open(new OnCommandResultListener {
+ override def onCommandResult(commandCode: Int, exitCode: Int,
+ output: util.List[String]) {
+ if (exitCode < 0) shell = null
+ }
+ })
+ }
+ }
+
+ private def openRootShell() {
+ if (rootShell == null) {
+ val builder = new Builder()
+ rootShell = builder
+ .setShell(SU.shell(0, "u:r:untrusted_app:s0"))
+ .setWantSTDERR(true)
+ .setWatchdogTimeout(10)
+ .open()
+ }
+ }
+
+ def runCommand(command: String) {
+ runCommand(Array(command))
+ }
+
+ def runCommand(commands: Array[String]) {
+ if (shell == null) {
+ openShell()
+ }
+ shell.addCommand(commands)
+ shell.waitForIdle()
+ }
+
+ def runRootCommand(command: String): String = runRootCommand(Array(command))
+
+ def runRootCommand(commands: Array[String]): String = {
+ if (!isRoot) {
+ return null
+ }
+ if (rootShell == null) {
+ openRootShell()
+ }
+ val sb = new StringBuilder
+ rootShell.addCommand(commands, 0, new OnCommandResultListener {
+ override def onCommandResult(commandCode: Int, exitCode: Int,
+ output: util.List[String]) {
+ if (exitCode < 0) {
+ rootShell = null
+ } else {
+ import scala.collection.JavaConversions._
+ output.foreach(line => sb.append(line).append('\n'))
+ }
+ }
+ })
+ if (rootShell.waitForIdle()) sb.toString()
+ else null
+ }
+
+ def isRoot: Boolean = SU.available()
+}
diff --git a/src/main/scala/com/github/shadowsocks/utils/Constants.scala b/src/main/scala/com/github/shadowsocks/utils/Constants.scala
index 11ce486c..39a08f76 100644
--- a/src/main/scala/com/github/shadowsocks/utils/Constants.scala
+++ b/src/main/scala/com/github/shadowsocks/utils/Constants.scala
@@ -42,6 +42,15 @@ package com.github.shadowsocks.utils
import android.content.{Intent, SharedPreferences}
import com.github.shadowsocks.aidl.Config
+object Executable {
+ val REDSOCKS = "redsocks"
+ val PDNSD = "pdnsd"
+ val SS_LOCAL = "ss-local"
+ val SS_TUNNEL = "ss-tunnel"
+ val IPTABLES = "iptables"
+ val TUN2SOCKS = "tun2socks"
+}
+
object Msg {
val CONNECT_FINISH = 1
val CONNECT_SUCCESS = 2
diff --git a/src/main/scala/com/github/shadowsocks/utils/Utils.scala b/src/main/scala/com/github/shadowsocks/utils/Utils.scala
index 6dfeef56..7dfbf537 100644
--- a/src/main/scala/com/github/shadowsocks/utils/Utils.scala
+++ b/src/main/scala/com/github/shadowsocks/utils/Utils.scala
@@ -134,8 +134,7 @@ object Utils {
private def toggleAboveApiLevel17() {
// Android 4.2 and above
- Utils.runRootCommand("ndc resolver flushdefaultif\n"
- + "ndc resolver flushif wlan0\n")
+ Console.runRootCommand(Array("ndc resolver flushdefaultif", "ndc resolver flushif wlan0"))
//Utils.runRootCommand("settings put global airplane_mode_on 1\n"
// + "am broadcast -a android.intent.action.AIRPLANE_MODE --ez state true\n"
@@ -207,22 +206,19 @@ object Utils {
def resolve(host: String, enableIPv6: Boolean): Option[String] = {
if (enableIPv6 && Utils.isIPv6Support) {
resolve(host, Type.AAAA) match {
- case Some(addr) => {
+ case Some(addr) =>
return Some(addr)
- }
case None =>
}
}
resolve(host, Type.A) match {
- case Some(addr) => {
+ case Some(addr) =>
return Some(addr)
- }
case None =>
}
resolve(host) match {
- case Some(addr) => {
+ case Some(addr) =>
return Some(addr)
- }
case None =>
}
None
@@ -248,9 +244,8 @@ object Utils {
}
}
} catch {
- case ex: Exception => {
+ case ex: Exception =>
Log.e(TAG, "Failed to get interfaces' addresses.", ex)
- }
}
None
}
@@ -276,9 +271,8 @@ object Utils {
}
}
} catch {
- case ex: Exception => {
+ case ex: Exception =>
Log.e(TAG, "Failed to get interfaces' addresses.", ex)
- }
}
false
}
@@ -289,23 +283,20 @@ object Utils {
*/
def checkIptables() {
- if (!Utils.getRoot) {
+ if (!Console.isRoot) {
iptables = DEFAULT_IPTABLES
return
}
iptables = DEFAULT_IPTABLES
- var lines: String = null
var compatible: Boolean = false
var version: Boolean = false
- val sb = new StringBuilder
- val command = iptables + " --version\n" + iptables + " -L -t nat -n\n" + "exit\n"
- val exitcode = runScript(command, sb, 10 * 1000, asroot = true)
- if (exitcode == TIME_OUT) return
+ val command = Array(iptables + " --version", iptables + " -L -t nat -n")
+ val lines = Console.runRootCommand(command)
+ if (lines == null) return
- lines = sb.toString()
if (lines.contains("OUTPUT")) {
compatible = true
}
@@ -346,9 +337,8 @@ object Utils {
val appInfo: ApplicationInfo = pm.getApplicationInfo(packages(0), 0)
return pm.getApplicationIcon(appInfo)
} catch {
- case e: PackageManager.NameNotFoundException => {
+ case e: PackageManager.NameNotFoundException =>
Log.e(c.getPackageName, "No package found matching with the uid " + uid)
- }
}
}
} else {
@@ -367,25 +357,16 @@ object Utils {
iptables
}
- def getShell: String = {
- if (shell == null) {
- shell = DEFAULT_SHELL
- if (!new File(shell).exists) shell = "sh"
- }
- shell
- }
-
def initHasRedirectSupported() {
- if (!Utils.getRoot) return
+ if (!Console.isRoot) return
hasRedirectSupport = 1
val sb = new StringBuilder
val command = Utils.getIptables + " -t nat -A OUTPUT -p udp --dport 54 -j REDIRECT --to 8154"
- val exitcode: Int = runScript(command, sb, 10 * 1000, asroot = true)
- val lines = sb.toString()
+ val lines = Console.runRootCommand(command)
- Utils.runRootCommand(command.replace("-A", "-D"))
- if (exitcode == TIME_OUT) return
+ Console.runRootCommand(command.replace("-A", "-D"))
+ if (lines == null) return
if (lines.contains("No chain/target/match")) {
hasRedirectSupport = 0
}
@@ -394,196 +375,11 @@ object Utils {
def isInitialized: Boolean = {
initialized match {
case true => true
- case _ => {
+ case _ =>
initialized = true
false
- }
}
}
-
- def getRoot: Boolean = {
- if (isRoot != -1) return isRoot == 1
- if (new File(DEFAULT_ROOT).exists) {
- root_shell = DEFAULT_ROOT
- } else if (new File(ALTERNATIVE_ROOT).exists) {
- root_shell = ALTERNATIVE_ROOT
- } else {
- root_shell = "su"
- }
- val sb = new StringBuilder
- val command: String = "id\n"
- val exitcode: Int = runScript(command, sb, 10 * 1000, asroot = true)
- if (exitcode == TIME_OUT) {
- return false
- }
- val lines = sb.toString()
- if (lines.contains("uid=0")) {
- isRoot = 1
- } else {
- if (rootTries >= 1) isRoot = 0
- rootTries += 1
- }
- isRoot == 1
- }
-
- def runCommand(command: String): Boolean = {
- runCommand(command, 10 * 1000)
- }
-
- def runCommand(command: String, timeout: Int): Boolean = {
- if (BuildConfig.DEBUG) Log.d(TAG, command)
- runScript(command, null, timeout, asroot = false)
- true
- }
-
- def runRootCommand(command: String): Boolean = {
- runRootCommand(command, 10 * 1000)
- }
-
- def runRootCommand(command: String, timeout: Int): Boolean = {
- if (!Utils.getRoot) {
- Log.e(TAG, "Cannot get ROOT permission: " + root_shell)
- return false
- }
- if (BuildConfig.DEBUG) Log.d(TAG, command)
- runScript(command, null, timeout, asroot = true)
- true
- }
-
- def runScript(script: String, result: StringBuilder, timeout: Long, asroot: Boolean): Int = {
- val runner: Utils.ScriptRunner = new Utils.ScriptRunner(script, result, asroot)
- runner.start()
- try {
- if (timeout > 0) {
- runner.join(timeout)
- } else {
- runner.join()
- }
- if (runner.isAlive) {
- runner.destroy()
- runner.join(1000)
- return TIME_OUT
- }
- } catch {
- case ex: InterruptedException => {
- return TIME_OUT
- }
- }
- runner.exitcode
- }
-
- /**
- * Internal thread used to execute scripts (as root or not).
- * Creates a new script runner.
- *
- * @param scripts script to run
- * @param result result output
- * @param asroot if true, executes the script as root
- */
- class ScriptRunner(val scripts: String, val result: StringBuilder, val asroot: Boolean)
- extends Thread {
-
- var exitcode: Int = -1
- val pid: Array[Int] = new Array[Int](1)
- var pipe: FileDescriptor = null
-
- override def destroy() {
- if (pid(0) != -1) {
- Exec.hangupProcessGroup(pid(0))
- pid(0) = -1
- }
- if (pipe != null) {
- Exec.close(pipe)
- pipe = null
- }
- }
-
- def createSubprocess(processId: Array[Int], cmd: String): FileDescriptor = {
- val args = parse(cmd)
- val arg0 = args(0)
- Exec
- .createSubprocess(if (result != null) 1 else 0, arg0, args, null, scripts + "\nexit\n",
- processId)
- }
-
- def parse(cmd: String): Array[String] = {
- val PLAIN = 0
- val INQUOTE = 2
- val SKIP = 3
-
- var state = PLAIN
- val result: ArrayBuffer[String] = new ArrayBuffer[String]
- val builder = new StringBuilder()
-
- cmd foreach {
- ch => {
- state match {
- case PLAIN => {
- ch match {
- case c if Character.isWhitespace(c) => {
- result += builder.toString
- builder.clear()
- }
- case '"' => state = INQUOTE
- case _ => builder += ch
- }
- }
- case INQUOTE => {
- ch match {
- case '\\' => state = SKIP
- case '"' => state = PLAIN
- case _ => builder += ch
- }
- }
- case SKIP => {
- builder += ch
- state = INQUOTE
- }
- }
- }
- }
-
- if (builder.length > 0) {
- result += builder.toString
- }
- result.toArray
- }
-
- override def run() {
- pid(0) = -1
- try {
- if (this.asroot) {
- pipe = createSubprocess(pid, root_shell)
- } else {
- pipe = createSubprocess(pid, getShell)
- }
- if (pid(0) != -1) {
- exitcode = Exec.waitFor(pid(0))
- }
- if (result == null || pipe == null) return
- val stdout: InputStream = new FileInputStream(pipe)
- val buf = new Array[Byte](8192)
- var read: Int = 0
- while (stdout.available > 0) {
- read = stdout.read(buf)
- result.append(new String(buf, 0, read))
- }
- } catch {
- case ex: Exception => {
- Log.e(TAG, "Cannot execute command", ex)
- if (result != null) result.append("\n").append(ex)
- }
- } finally {
- if (pipe != null) {
- Exec.close(pipe)
- }
- if (pid(0) != -1) {
- Exec.hangupProcessGroup(pid(0))
- }
- }
- }
- }
-
}