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)) - } - } - } - } - }