/* * 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 com.mcxiaoke.shell; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.util.Log; import com.mcxiaoke.apptoolkit.AppContext; import com.mcxiaoke.shell.utils.Remounter; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.UUID; /** * Class providing functionality to execute commands in a (root) shell */ public class Shell { public static final String SHELL_SU = "su"; public static final String SHELL_SH = "sh"; public static final String SHELL_BASH = "bash"; private static final String CMD_CP = "cp"; private static final String CMD_MV = "mv"; private static final String CMD_CHOWN = "chown"; private static final String BUSYBOX = "busybox"; private static final String CMD_SU = "su"; private static final String MOUNT = "mount"; private static final String CMD_FIND = "find"; /** * class for shell command */ public static final class ShellCommand { // input public List<String> commands = new ArrayList<String>(); public List<String> environments = new ArrayList<String>(); public boolean needRoot; public boolean needStandError; // output public int exitValue; public Exception exception; public List<String> output = new ArrayList<String>(); public ShellCommand(String cmd, boolean root) { this(Arrays.asList(new String[]{cmd}), root); } public ShellCommand(String[] cmds, boolean root) { this(Arrays.asList(cmds), root); } public ShellCommand(List<String> cmds, boolean root) { if (cmds != null) { this.commands.addAll(cmds); } this.needRoot = root; this.needStandError = true; } public void addCommand(String cmd) { this.commands.add(cmd); } public void addEnvironment(String env) { this.environments.add(env); } public void addOutput(String out) { this.output.add(out); } } private static final String TAG = Shell.class.getSimpleName(); private static boolean sDebug = AppContext.isDebug(); public static boolean isDebug() { return sDebug; } public static void enable() { sDebug = true; } public static void disable() { sDebug = false; } public static void setDebug(boolean debug) { sDebug = debug; } public static void log(String message) { if (sDebug) { Log.v(TAG, message); } } public static void log(String... messages) { if (sDebug) { if (messages != null && messages.length > 0) { for (String message : messages) { Log.v(TAG, message); } } } } public static void error(Throwable throwable) { if (sDebug) { Log.e(TAG, "error: " + throwable); } } public static void error(String message) { if (sDebug) { Log.e(TAG, "message: " + message); } } public static void error(Throwable throwable, String message) { if (sDebug) { Log.e(TAG, "error: " + throwable + " message: " + message); } } public static final String[] SUPERUSER_PACKAGE = new String[]{ "eu.chainfire.supersu", "eu.chainfire.supersu.pro", "com.noshufou.android.su", "com.miui.uac", "com.lbe.security.shuame", "com.lbe.security.miui", "com.m0narx.su"}; /** * The set of su location I know by now. */ public static final String[] SU_BINARY_PATH = { "/system/bin", "/system/sbin", "/system/xbin", "/vendor/bin", "/sbin", }; /** * Check if command need patch. * * @return */ public static boolean isNeedLibPath() { return android.os.Build.VERSION.SDK_INT == 17; } /** * <p>Runs commands using the supplied shell, and returns the output, or null in * case of errors.</p> * <p/> * <p>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.</p> * <p/> * <p>Note as well that this code will intentionally crash when runCommand in debug mode * from the main thread of the application. You should always execute shell * commands from a background thread.</p> * <p/> * <p>When in debug mode, the code will also excessively log the commands passed to * and the output returned from the shell.</p> * <p/> * <p>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 <em>'ls -lR /'</em> * will probably have you runCommand out of memory.</p> */ private static ShellCommand runCommand(ShellCommand cmd) { if (cmd == null || cmd.commands == null || cmd.commands.isEmpty()) { return null; } String shell = cmd.needRoot ? SHELL_SU : SHELL_SH; if (isDebug()) { // 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. if ((Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper())) { log(ShellOnMainThreadException.EXCEPTION_COMMAND); throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_COMMAND); } log(String.format("[%s%%] START", shell)); log("runCommand.commands: " + listToString(cmd.commands)); } List<String> res = Collections.synchronizedList(new ArrayList<String>()); try { // Combine passed environment with system environment String[] environments = new String[]{}; if (cmd.environments != null) { Map<String, String> newEnvironment = new HashMap<String, String>(); newEnvironment.putAll(System.getenv()); int split; for (String entry : cmd.environments) { if ((split = entry.indexOf("=")) >= 0) { newEnvironment.put(entry.substring(0, split), entry.substring(split + 1)); } } int i = 0; environments = new String[newEnvironment.size()]; for (Map.Entry<String, String> entry : newEnvironment.entrySet()) { environments[i] = entry.getKey() + "=" + entry.getValue(); i++; } } // log("runCommand.environments: " + arrayToString(environments)); // setup our process, retrieve STDIN stream, and STDOUT/STDERR gobblers Process process = Runtime.getRuntime().exec(shell, environments); DataOutputStream STDIN = new DataOutputStream(process.getOutputStream()); StreamGobbler STDOUT = new StreamGobbler(shell + "-", process.getInputStream(), res); StreamGobbler STDERR = new StreamGobbler(shell + "*", process.getErrorStream(), cmd.needStandError ? res : null); // start gobbling and write our commands to the shell STDOUT.start(); STDERR.start(); for (String write : cmd.commands) { if (isDebug()) log(String.format("[%s+] %s", shell, write)); STDIN.writeBytes(write + "\n"); STDIN.flush(); } STDIN.writeBytes("exit\n"); 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(); if (res != null && res.size() > 0) { cmd.output.addAll(res); } cmd.exitValue = process.exitValue(); // in case of su, 255 usually indicates access denied // if (shell.equals("su") && exitValue == 255) { // res = null; // } } catch (InterruptedException e) { cmd.exception = e; e.printStackTrace(); error("runCommand,ex:" + e); } catch (IOException e) { cmd.exception = e; error("runCommand,ex:" + e); e.printStackTrace(); } catch (Exception e) { cmd.exception = e; error("runCommand,ex:" + e); } finally { } if (isDebug()) { log("runCommand.exitValue: " + cmd.exitValue); log("runCommand.result: " + listToString(cmd.output)); log(String.format("[%s%%] END", shell)); } return cmd; } /** * <p>Runs commands using the supplied shell, and returns the output, or null in * case of errors.</p> * <p/> * <p>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.</p> * <p/> * <p>Note as well that this code will intentionally crash when runCommand in debug mode * from the main thread of the application. You should always execute shell * commands from a background thread.</p> * <p/> * <p>When in debug mode, the code will also excessively log the commands passed to * and the output returned from the shell.</p> * <p/> * <p>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 <em>'ls -lR /'</em> * will probably have you runCommand out of memory.</p> * * @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 */ private static List<String> run(String shell, String[] commands, String[] environment, boolean wantSTDERR) throws IOException, InterruptedException { String shellUpper = shell.toUpperCase(); if (isDebug()) { // 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. if ((Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper())) { log(ShellOnMainThreadException.EXCEPTION_COMMAND); throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_COMMAND); } log(String.format("[%s%%] START", shellUpper)); log("runCommand.commands: " + arrayToString(commands)); } List<String> res = Collections.synchronizedList(new ArrayList<String>()); int exitValue = -1; try { // Combine passed environment with system environment if (environment != null) { Map<String, String> newEnvironment = new HashMap<String, String>(); 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<String, String> 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) { if (isDebug()) log(String.format("[%s+] %s", shellUpper, write)); STDIN.writeBytes(write + "\n"); STDIN.flush(); } STDIN.writeBytes("exit\n"); 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(); exitValue = process.exitValue(); // in case of su, 255 usually indicates access denied if (shell.equals("su") && exitValue == 255) { res = null; } } finally { } if (isDebug()) { log("runCommand.exitValue: " + exitValue); log("runCommand.result: " + listToString(res)); log(String.format("[%s%%] END", shell.toUpperCase())); } return res; } private static String arrayToString(String[] strings) { StringBuilder builder = new StringBuilder(); if (strings == null || strings.length == 0) { builder.append("null"); } else { for (String str : strings) { builder.append(" ").append(str).append(";"); } } return builder.toString(); } private static String listToString(List<String> strings) { StringBuilder builder = new StringBuilder(); if (strings == null || strings.isEmpty()) { builder.append("null"); } else { for (String str : strings) { builder.append(" ").append(str).append(";"); } } return builder.toString(); } /** * Runs command and return output * * @param command The command to runCommand * @return Output of the command, or null in case of an error */ public static ShellCommand run(String command) { ShellCommand cmd = new ShellCommand(new String[]{command}, false); return Shell.runCommand(cmd); } /** * Runs commands and return output * * @param commands The commands to runCommand * @return Output of the commands, or null in case of an error */ public static ShellCommand run(List<String> commands) { ShellCommand cmd = new ShellCommand(commands, false); return Shell.runCommand(cmd); } /** * Runs commands and return output * * @param commands The commands to runCommand * @return Output of the commands, or null in case of an error */ public static ShellCommand run(String[] commands) { ShellCommand cmd = new ShellCommand(commands, false); return Shell.runCommand(cmd); } /** * Runs command as root (if available) and return output * * @param command The command to runCommand * @return Output of the command, or null if root isn't available or in case of an error */ public static ShellCommand runAsRoot(String command) { ShellCommand cmd = new ShellCommand(new String[]{command}, true); return Shell.runCommand(cmd); } /** * Runs commands as root (if available) and return output * * @param commands The commands to runCommand * @return Output of the commands, or null if root isn't available or in case of an error */ public static ShellCommand runAsRoot(List<String> commands) { ShellCommand cmd = new ShellCommand(commands, true); return Shell.runCommand(cmd); } /** * Runs commands as root (if available) and return output * * @param commands The commands to runCommand * @return Output of the commands, or null if root isn't available or in case of an error */ public static ShellCommand runAsRoot(String[] commands) { ShellCommand cmd = new ShellCommand(commands, true); return Shell.runCommand(cmd); } /** * 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 isRootAccessAvailable() { // this is only one of many ways this can be done ShellCommand cmd = runAsRoot(new String[]{ "echo -BOC-", "id" }); if (cmd == null || cmd.exitValue == 1 || cmd.exception != null) return false; boolean echo_seen = false; for (String line : cmd.output) { if (line.contains("uid=")) { // id command is working, let's see if we are actually root return 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; } /** * <p>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.</p> * <p/> * <p>Note that su binary version and GUI (APK) version can be completely different.</p> * * @param internal Request human-readable version or application internal version * @return String containing the su version or null */ public static String getSuVersion(boolean internal) { // we add an additional exit call, because the command // line options are not available in all su versions, // thus potentially launching a shell instead ShellCommand cmd = run(new String[]{ internal ? "su -V" : "su -v", "exit" }); if (cmd == null || cmd.output.isEmpty()) return null; for (String line : cmd.output) { if (!internal) { if (line.contains(".")) return line; } else { try { if (Integer.parseInt(line) > 0) return line; } catch (NumberFormatException e) { } } } return null; } /** * Remount a path file as the type. * * @param path the path you want to remount * @param mountType the mount type, including, <i>"ro" means read only, "rw" means read and write</i> * @return the operation result. */ public static boolean remount(String path, String mountType) { if (TextUtils.isEmpty(path) || TextUtils.isEmpty(mountType)) { return false; } if (mountType.equalsIgnoreCase("rw") || mountType.equalsIgnoreCase("ro")) { return Remounter.remount(path, mountType); } else { return false; } } public static boolean isBinaryInstalled(String binary) { for (String path : SU_BINARY_PATH) { File file = new File(path, binary); if (file.exists()) { return true; } } return false; } public static boolean hasUtil(String binary) { return isBinaryInstalled(binary); } public static boolean hasBusyBox() { return isBinaryInstalled(BUSYBOX); } public static boolean hasSu() { return isBinaryInstalled(CMD_SU); } public static boolean hasCp() { return isBinaryInstalled(CMD_CP); } public static boolean hasChown() { return isBinaryInstalled(CMD_CHOWN); } public static boolean hasMount() { return isBinaryInstalled(MOUNT); } public static boolean hasFind() { return isBinaryInstalled(CMD_FIND); } public static void reboot() { runAsRoot("reboot"); } public static void fastReboot() { killProcess("system_server zygote"); } public static void killProcess(String processName) { String fastRebootCommand; if (hasBusyBox()) { fastRebootCommand = "busybox killall " + processName; } else { fastRebootCommand = "killall " + processName; } runAsRoot(fastRebootCommand); } public static boolean restoreAppData(String uid, String src, String dest, boolean needRemount, boolean override) throws Exception { String mountedAs = Remounter.getMountedAs(dest); if (needRemount) { remount(dest, "rw"); } List<String> commands = new ArrayList<String>(); if (override) { String deleteCommand = "rm -rf " + dest; commands.add(deleteCommand); } File destDir = new File(dest); if (!destDir.exists()) { String mkdirsCommand = "mkdir -p " + destDir; commands.add(mkdirsCommand); } boolean hasBusyBox = hasBusyBox(); String cpCommand; if (hasBusyBox) { cpCommand = "busybox cp -rf "; } else { cpCommand = "cp -rf "; } commands.add(cpCommand + src + "/databases " + dest + "/databases"); commands.add(cpCommand + src + "/shared_prefs " + dest + "/shared_prefs"); commands.add(cpCommand + src + "/files " + dest + "/files"); String chownCommand; if (hasBusyBox) { chownCommand = "busybox chown -R " + uid + ":" + uid + " "; } else { chownCommand = "chown " + uid + ":" + uid + " "; } commands.add(chownCommand + dest + "/databases"); commands.add(chownCommand + dest + "/shared_prefs"); commands.add(chownCommand + dest + "/files"); String chmodCommand; if (hasBusyBox) { chmodCommand = "busybox chmod 755 -R "; } else { chmodCommand = "chmod 755 -R "; } commands.add(chmodCommand + dest + "/databases"); commands.add(chmodCommand + dest + "/shared_prefs"); commands.add(chmodCommand + dest + "/files"); ShellCommand cmd = runAsRoot(commands); if (isDebug()) { log("restoreAppData() exitValue is " + cmd.exitValue + " exception=" + cmd.exception + " output is " + listToString(cmd.output)); } if (needRemount) { remount(dest, mountedAs); } return cmd.exception == null; } public static boolean backupAppData(String src, String dest, boolean needRemount, boolean override) throws Exception { String mountedAs = Remounter.getMountedAs(dest); if (needRemount) { remount(dest, "rw"); } List<String> commands = new ArrayList<String>(); if (override) { String deleteCommand = "rm -rf " + dest; commands.add(deleteCommand); } File destDir = new File(dest); if (!destDir.exists()) { String mkdirsCommand = "mkdir -p " + destDir; commands.add(mkdirsCommand); } String cpCommand; if (hasBusyBox()) { cpCommand = "busybox cp -rf "; } else { cpCommand = "cp -rf "; } commands.add(cpCommand + src + "/databases " + dest + "/databases"); commands.add(cpCommand + src + "/shared_prefs " + dest + "/shared_prefs"); commands.add(cpCommand + src + "/files " + dest + "/files"); ShellCommand cmd = runAsRoot(commands); if (isDebug()) { log("backupAppData() exitValue is " + cmd.exitValue + " exception=" + cmd.exception + " output is " + listToString(cmd.output)); } if (needRemount) { remount(dest, mountedAs); } return cmd.exception == null; } public static boolean copyFile(String src, String dest, boolean needRemount, boolean override) throws Exception { String mountedAs = Remounter.getMountedAs(dest); if (needRemount) { remount(dest, "rw"); } List<String> commands = new ArrayList<String>(); if (override) { String deleteCommand = "rm -rf " + dest; commands.add(deleteCommand); } String parentDir = new File(dest).getParent(); if (parentDir != null) { String mkdirsCommand = "mkdir -p " + parentDir; commands.add(mkdirsCommand); } String cpCommand; if (hasBusyBox()) { cpCommand = "busybox cp -rf " + src + " " + dest; } else { cpCommand = "cp -rf " + src + " > " + dest; } commands.add(cpCommand); ShellCommand cmd = runAsRoot(commands); if (isDebug()) { log("copyFile() exitValue is " + cmd.exitValue + " output is " + listToString(cmd.output)); } if (needRemount) { remount(dest, mountedAs); } return cmd.exitValue == 0 && cmd.exception == null; } public static boolean moveFile(String src, String dest, boolean needRemount) throws Exception { String mountedAs = Remounter.getMountedAs(dest); if (needRemount) { remount(dest, "rw"); } String mvCommand; if (hasCp()) { mvCommand = "mv " + src + " " + dest; } else if (hasBusyBox()) { mvCommand = "mv " + src + " " + dest; } else { return false; } ShellCommand cmd = runAsRoot(mvCommand); if (needRemount) { remount(dest, mountedAs); } return cmd.exitValue == 0 && cmd.exception == null; } /** * This will launch the Android market looking for BusyBox * * @param activity pass in your Activity */ public void offerBusyBox(Activity activity) { Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=stericson.busybox")); activity.startActivity(i); } /** * 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 { /** * <p>Command result callback</p> * <p/> * <p>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</p> * <p/> * <p>See {@link Shell.Interactive} for threading details</p> * * @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<String> output); } /** * Internal class to store command block proprties */ 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<Command> commands = new LinkedList<Command>(); private Map<String, String> environment = new HashMap<String, String>(); private StreamGobbler.OutputCallback standinCallback = null; private StreamGobbler.OutputCallback standoutCallback = null; /** * <p>Set a custom handler that will be used to post all callbacks to</p> * <p/> * <p>See {@link Shell.Interactive} for further details on threading and handlers</p> * * @param handler Handler to use * @return This Builder object for method chaining */ public Builder setHandler(Handler handler) { this.handler = handler; return this; } /** * <p>Automatically create a handler if possible ? Default to true</p> * <p/> * <p>See {@link Shell.Interactive} for further details on threading and handlers</p> * * @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<String, String> 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); } /** * <p>Add a command to execute, with a callback to be called on completion</p> * <p/> * <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p> * * @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<String> commands) { return addCommand(commands, 0, null); } /** * <p>Add commands to execute, with a callback to be called on completion (of all commands)</p> * <p/> * <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p> * * @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<String> 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); } /** * <p>Add commands to execute, with a callback to be called on completion (of all commands)</p> * <p/> * <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p> * * @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; } /** * <p>Set a callback called for every line output to STDOUT by the shell</p> * <p/> * <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p> * * @param outputCallback Callback to be called for each line * @return This Builder object for method chaining */ public Builder setStandinCallback(StreamGobbler.OutputCallback outputCallback) { this.standinCallback = outputCallback; return this; } /** * <p>Set a callback called for every line output to STDERR by the shell</p> * <p/> * <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p> * * @param outputCallback Callback to be called for each line * @return This Builder object for method chaining */ public Builder setStandoutCallback(StreamGobbler.OutputCallback outputCallback) { this.standoutCallback = outputCallback; return this; } /** * Construct a {@link Shell.Interactive} instance, and start the shell */ public Interactive open() { return new Interactive(this); } } /** * <p>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.</p> * <p/> * <p>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.</p> * <p/> * <p>Note as well that this code will intentionally crash when runCommand in debug mode * from the main thread of the application. You should always execute shell * commands from a background thread.</p> * <p/> * <p>When in debug mode, the code will also excessively log the commands passed to * and the output returned from the shell.</p> * <p/> * <p>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 <em>'ls -lR /'</em> * will probably have you runCommand out of memory when using a * {@link Shell.OnCommandResultListener}. A work-around is to not supply this callback, * but using (only) . This * way, an internal buffer will not be created and wasting your memory.</p> * <p/> * <h3>Callbacks, threads and handlers</h3> * <p/> * <p>On which thread the callbacks execute is dependent on your initialization. You can * supply a custom Handler using {@link Shell.Builder#setHandler(android.os.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.</p> * <p/> * <p>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.</p> * <p/> * <p>The main thread must certainly has 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.</p> * <p/> * <p>Background threads usually do <em>not</em> 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.</p> */ public static class Interactive { private final Handler handler; private final boolean autoHandler; private final String shell; private final boolean wantSTDERR; private final List<Command> commands; private final Map<String, String> environment; private final StreamGobbler.OutputCallback onSTDOUTLineListener; private final StreamGobbler.OutputCallback onSTDERRLineListener; private Process process = null; private DataOutputStream STDIN = null; private StreamGobbler STDOUT = null; private StreamGobbler STDERR = 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 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<String> buffer = null; /** * The only way to create an instance: Shell.Builder::open() * * @param builder Builder class to take values from */ private Interactive(Builder builder) { autoHandler = builder.autoHandler; shell = builder.shell; wantSTDERR = builder.wantSTDERR; commands = builder.commands; environment = builder.environment; onSTDOUTLineListener = builder.standinCallback; onSTDERRLineListener = builder.standoutCallback; // 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; } open(); } @Override protected void finalize() throws Throwable { if (!closed && isDebug()) { // waste of resources 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); } /** * <p>Add a command to execute, with a callback to be called on completion</p> * <p/> * <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p> * * @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<String> commands) { addCommand(commands, 0, null); } /** * <p>Add commands to execute, with a callback to be called on completion (of all commands)</p> * <p/> * <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p> * * @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<String> 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); } /** * <p>Add commands to execute, with a callback to be called on completion (of all commands)</p> * <p/> * <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p> * * @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) { if (running) { 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); } /** * 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<String>()); } idle = false; this.command = command; for (String write : command.commands) { if (isDebug()) log(String.format("[%s+] %s", shell.toUpperCase(), write)); STDIN.writeBytes(write + "\n"); } STDIN.writeBytes("echo " + command.marker + " $?\n"); STDIN.writeBytes("echo " + command.marker + " >&2\n"); STDIN.flush(); } catch (IOException e) { } } else { runNextCommand(false); } } 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 (command.onCommandResultListener != null) { if (buffer != null) { if (handler != null) { final List<String> fBuffer = buffer; final int fExitCode = lastExitCode; final Command fCommand = command; startCallback(); handler.post(new Runnable() { @Override public void run() { try { fCommand.onCommandResultListener.onCommandResult(fCommand.code, fExitCode, fBuffer); } finally { endCallback(); } } }); } else { command.onCommandResultListener.onCommandResult(command.code, lastExitCode, buffer); } } } 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, StreamGobbler.OutputCallback listener) { if (listener != null) { if (handler != null) { final String fLine = line; final StreamGobbler.OutputCallback fListener = listener; startCallback(); handler.post(new Runnable() { @Override public void run() { try { fListener.onOutput(fLine); } finally { endCallback(); } } }); } else { listener.onOutput(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++; } } /** * 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 stars executing commands. * See {@link Shell.Interactive} * * @return Opened successfully ? */ private synchronized boolean open() { if (isDebug()) log(String.format("[%s%%] START", shell.toUpperCase())); try { // setup our process, retrieve STDIN stream, and STDOUT/STDERR gobblers if (environment.size() == 0) { process = Runtime.getRuntime().exec(shell); } else { Map<String, String> newEnvironment = new HashMap<String, String>(); newEnvironment.putAll(System.getenv()); newEnvironment.putAll(environment); int i = 0; String[] env = new String[newEnvironment.size()]; for (Map.Entry<String, String> 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() + "-", process.getInputStream(), new StreamGobbler.OutputCallback() { @Override public void onOutput(String line) { 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() + "*", process.getErrorStream(), new StreamGobbler.OutputCallback() { @Override public void onOutput(String line) { 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 && isDebug() && (Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper())) { log(ShellOnMainThreadException.EXCEPTION_NOT_IDLE); throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_NOT_IDLE); } if (!_idle) waitForIdle(); try { STDIN.writeBytes("exit\n"); 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(); } catch (IOException e) { // shell probably not found } catch (InterruptedException e) { // this should really be re-thrown } if (isDebug()) log(String.format("[%s%%] END", shell.toUpperCase())); } /** * Is out 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; } /** * <p>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.</p> * <p/> * <p>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.</p> * <p/> * <p>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.</p> * <p/> * <p>In practise 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.</p> * <p/> * <p>See {@link Shell.Interactive} for further details on threading and handlers</p> * * @return True if wait complete, false if wait interrupted */ public boolean waitForIdle() { if (isDebug() && (Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper())) { 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 getIcon 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); } } /** * Thread utility class continuously reading from an InputStream */ public static class StreamGobbler extends Thread { /** * Line callback interface */ public interface OutputCallback { /** * <p>Line callback</p> * <p/> * <p>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</p> * * @param line String that was gobbled */ public void onOutput(String line); } private String shell = null; private BufferedReader reader = null; private List<String> writer = null; private OutputCallback listener = null; /** * <p>StreamGobbler constructor</p> * <p/> * <p>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)</p> * * @param shell Name of the shell * @param inputStream InputStream to read from * @param outputList List<String> to write to, or null */ public StreamGobbler(String shell, InputStream inputStream, List<String> outputList) { this.shell = shell; reader = new BufferedReader(new InputStreamReader(inputStream)); writer = outputList; } /** * <p>StreamGobbler constructor</p> * <p/> * <p>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)</p> * * @param shell Name of the shell * @param inputStream InputStream to read from * @param outputCallback OnLineListener callback */ public StreamGobbler(String shell, InputStream inputStream, OutputCallback outputCallback) { this.shell = shell; reader = new BufferedReader(new InputStreamReader(inputStream)); listener = outputCallback; } @Override public void run() { // keep reading the InputStream until it ends (or an error occurs) try { String line = null; while ((line = reader.readLine()) != null) { if (isDebug()) log(String.format("[%s] %s", shell, line)); if (writer != null) writer.add(line); if (listener != null) listener.onOutput(line); } } catch (IOException e) { } // make sure our stream is closed and resources will be freed try { reader.close(); } catch (IOException e) { } } } /** * Exception class used to notify developer that a shell was not close()d */ public static class ShellNotClosedException extends RuntimeException { public static final String EXCEPTION_NOT_CLOSED = "Application did not close() interactive shell"; public ShellNotClosedException() { super(EXCEPTION_NOT_CLOSED); } } /** * Exception class used to crash application when shell commands are executed * from the main thread, and we are in debug mode. */ public static class ShellOnMainThreadException extends RuntimeException { public static final String EXCEPTION_COMMAND = "Application attempted to runCommand 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); } } /** * * http://en.wikipedia.org/wiki/Chmod * * * * 0 = --- = no access 1 = --x = execute 2 = -w- = write 3 = -wx = write and execute 4 = r-- = read 5 = r-x = read and execute 6 = rw- = read and write 7 = rwx = read write execute (full access) # Permission rwx 7 full 111 6 read and write 110 5 read and execute 101 4 read only 100 3 write and execute 011 2 write only 010 1 execute only 001 0 none 000 Symbolic Notation Octal Notation English ---------- 0000 no permissions ---x--x--x 0111 execute --w--w--w- 0222 write --wx-wx-wx 0333 write & execute -r--r--r-- 0444 read -r-xr-xr-x 0555 read & execute -rw-rw-rw- 0666 read & write -rwxrwxrwx 0777 read, write, & execute **/ }