1737 lines
64 KiB
Java
1737 lines
64 KiB
Java
/*
|
|
* Copyright (C) 2012-2014 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 {
|
|
/**
|
|
* <p>
|
|
* Runs commands using the supplied shell, and returns the output, or null
|
|
* in case of errors.
|
|
* </p>
|
|
* <p>
|
|
* 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.
|
|
* </p>
|
|
*
|
|
* @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<String> run(String shell, String[] commands, boolean wantSTDERR) {
|
|
return run(shell, commands, null, wantSTDERR);
|
|
}
|
|
|
|
/**
|
|
* <p>
|
|
* Runs commands using the supplied shell, and returns the output, or null
|
|
* in case of errors.
|
|
* </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>
|
|
* 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.
|
|
* </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>
|
|
* 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 run 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
|
|
*/
|
|
public static List<String> 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<String> res = Collections.synchronizedList(new ArrayList<String>());
|
|
|
|
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) {
|
|
Debug.logCommand(String.format("[%s+] %s", shellUpper, write));
|
|
STDIN.write((write + "\n").getBytes("UTF-8"));
|
|
STDIN.flush();
|
|
}
|
|
try {
|
|
STDIN.write("exit\n".getBytes("UTF-8"));
|
|
STDIN.flush();
|
|
} catch (IOException e) {
|
|
// happens if the script already contains the exit line - if
|
|
// there were a more serious issue, it would already have thrown
|
|
// an exception while writing the script to STDIN
|
|
}
|
|
|
|
// 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<String> 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<String> 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<String> run(List<String> 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<String> 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 {
|
|
private static Boolean isSELinuxEnforcing = null;
|
|
private static String[] suVersion = new String[] {
|
|
null, null
|
|
};
|
|
|
|
/**
|
|
* 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<String> 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<String> run(List<String> 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<String> 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<String> ret = run(Shell.availableTestCommands);
|
|
return Shell.parseAvailableResult(ret, true);
|
|
}
|
|
|
|
/**
|
|
* <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>
|
|
* Note that su binary version and GUI (APK) version can be completely
|
|
* different.
|
|
* </p>
|
|
* <p>
|
|
* This function caches its result to improve performance on multiple
|
|
* calls
|
|
* </p>
|
|
*
|
|
* @param internal Request human-readable version or application
|
|
* internal version
|
|
* @return String containing the su version or null
|
|
*/
|
|
public static synchronized String version(boolean internal) {
|
|
int idx = internal ? 0 : 1;
|
|
if (suVersion[idx] == null) {
|
|
String version = null;
|
|
|
|
List<String> ret = Shell.run(
|
|
internal ? "su -V" : "su -v",
|
|
new String[] {},
|
|
null,
|
|
false
|
|
);
|
|
|
|
if (ret != null) {
|
|
for (String line : ret) {
|
|
if (!internal) {
|
|
if (line.contains(".")) {
|
|
version = line;
|
|
break;
|
|
}
|
|
} else {
|
|
try {
|
|
if (Integer.parseInt(line) > 0) {
|
|
version = line;
|
|
break;
|
|
}
|
|
} catch (NumberFormatException e) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
suVersion[idx] = version;
|
|
}
|
|
return suVersion[idx];
|
|
}
|
|
|
|
/**
|
|
* 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 <context>][ <uid>]
|
|
String shell = "su";
|
|
|
|
if ((context != null) && isSELinuxEnforcing()) {
|
|
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 <uid>" 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;
|
|
}
|
|
|
|
/**
|
|
* Constructs a shell command to start a su shell connected to mount
|
|
* master daemon, to perform public mounts on Android 4.3+ (or 4.2+ in
|
|
* SELinux enforcing mode)
|
|
*
|
|
* @return Shell command
|
|
*/
|
|
public static String shellMountMaster() {
|
|
if (android.os.Build.VERSION.SDK_INT >= 17) {
|
|
return "su --mount-master";
|
|
}
|
|
return "su";
|
|
}
|
|
|
|
/**
|
|
* Detect if SELinux is set to enforcing, caches result
|
|
*
|
|
* @return true if SELinux set to enforcing, or false in the case of
|
|
* permissive or not present
|
|
*/
|
|
public static synchronized boolean isSELinuxEnforcing() {
|
|
if (isSELinuxEnforcing == null) {
|
|
Boolean enforcing = null;
|
|
|
|
// First known firmware with SELinux built-in was a 4.2 (17)
|
|
// leak
|
|
if (android.os.Build.VERSION.SDK_INT >= 17) {
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
if (enforcing == null) {
|
|
enforcing = false;
|
|
}
|
|
|
|
isSELinuxEnforcing = enforcing;
|
|
}
|
|
return isSELinuxEnforcing;
|
|
}
|
|
|
|
/**
|
|
* <p>
|
|
* Clears results cached by isSELinuxEnforcing() and version(boolean
|
|
* internal) calls.
|
|
* </p>
|
|
* <p>
|
|
* Most apps should never need to call this, as neither enforcing status
|
|
* nor su version is likely to change on a running device - though it is
|
|
* not impossible.
|
|
* </p>
|
|
*/
|
|
public static synchronized void clearCachedResults() {
|
|
isSELinuxEnforcing = null;
|
|
suVersion[0] = null;
|
|
suVersion[1] = null;
|
|
}
|
|
}
|
|
|
|
private interface OnResult {
|
|
// 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;
|
|
}
|
|
|
|
/**
|
|
* 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 extends OnResult {
|
|
/**
|
|
* <p>
|
|
* Command result callback
|
|
* </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>
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* Command per line callback for parsing the output line by line without
|
|
* buffering It also notifies the recipient of the completion of a command
|
|
* block, including the (last) exit code.
|
|
*/
|
|
public interface OnCommandLineListener extends OnResult, OnLineListener {
|
|
/**
|
|
* <p>
|
|
* Command result callback
|
|
* </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>
|
|
* 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
|
|
*/
|
|
public void onCommandResult(int commandCode, int exitCode);
|
|
}
|
|
|
|
/**
|
|
* 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 OnCommandLineListener onCommandLineListener;
|
|
private final String marker;
|
|
|
|
public Command(String[] commands, int code,
|
|
OnCommandResultListener onCommandResultListener,
|
|
OnCommandLineListener onCommandLineListener) {
|
|
this.commands = commands;
|
|
this.code = code;
|
|
this.onCommandResultListener = onCommandResultListener;
|
|
this.onCommandLineListener = onCommandLineListener;
|
|
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 OnLineListener onSTDOUTLineListener = null;
|
|
private OnLineListener onSTDERRLineListener = null;
|
|
private int watchdogTimeout = 0;
|
|
|
|
/**
|
|
* <p>
|
|
* Set a custom handler that will be used to post all callbacks to
|
|
* </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>
|
|
* 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>
|
|
* 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>
|
|
* 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>
|
|
* 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, null));
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* <p>
|
|
* Set a callback called for every line output to STDOUT by the shell
|
|
* </p>
|
|
* <p>
|
|
* The thread on which the callback executes is dependent on various
|
|
* factors, see {@link Shell.Interactive} for further details
|
|
* </p>
|
|
*
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* <p>
|
|
* Set a callback called for every line output to STDERR by the shell
|
|
* </p>
|
|
* <p>
|
|
* The thread on which the callback executes is dependent on various
|
|
* factors, see {@link Shell.Interactive} for further details
|
|
* </p>
|
|
*
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* <p>
|
|
* Enable command timeout callback
|
|
* </p>
|
|
* <p>
|
|
* This will invoke the onCommandResult() callback with exitCode
|
|
* WATCHDOG_EXIT if a command takes longer than watchdogTimeout seconds
|
|
* to complete.
|
|
* </p>
|
|
* <p>
|
|
* 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.
|
|
* </p>
|
|
*
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* <p>
|
|
* Enable/disable reduced logcat output
|
|
* </p>
|
|
* <p>
|
|
* Note that this is a global setting
|
|
* </p>
|
|
*
|
|
* @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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* <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>
|
|
* 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>
|
|
* 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.
|
|
* </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>
|
|
* 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 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.
|
|
* </p>
|
|
* <h3>Callbacks, threads and handlers</h3>
|
|
* <p>
|
|
* 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.
|
|
* </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>
|
|
* 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.
|
|
* </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 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<String> 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<String> 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, (OnCommandResultListener) null);
|
|
}
|
|
|
|
/**
|
|
* <p>
|
|
* Add a command to execute, with a callback to be called on completion
|
|
* </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);
|
|
}
|
|
|
|
/**
|
|
* <p>
|
|
* Add a command to execute, with a callback. This callback gobbles the
|
|
* output line by line without buffering it and also returns the result
|
|
* code on completion.
|
|
* </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 onCommandLineListener Callback
|
|
*/
|
|
public void addCommand(String command, int code, OnCommandLineListener onCommandLineListener) {
|
|
addCommand(new String[] {
|
|
command
|
|
}, code, onCommandLineListener);
|
|
}
|
|
|
|
/**
|
|
* Add commands to execute
|
|
*
|
|
* @param commands Commands to execute
|
|
*/
|
|
public void addCommand(List<String> commands) {
|
|
addCommand(commands, 0, (OnCommandResultListener) null);
|
|
}
|
|
|
|
/**
|
|
* <p>
|
|
* Add commands to execute, with a callback to be called on completion
|
|
* (of all commands)
|
|
* </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);
|
|
}
|
|
|
|
/**
|
|
* <p>
|
|
* Add commands to execute, with a callback. This callback gobbles the
|
|
* output line by line without buffering it and also returns the result
|
|
* code on completion.
|
|
* </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 onCommandLineListener Callback
|
|
*/
|
|
public void addCommand(List<String> commands, int code,
|
|
OnCommandLineListener onCommandLineListener) {
|
|
addCommand(commands.toArray(new String[commands.size()]), code, onCommandLineListener);
|
|
}
|
|
|
|
/**
|
|
* Add commands to execute
|
|
*
|
|
* @param commands Commands to execute
|
|
*/
|
|
public void addCommand(String[] commands) {
|
|
addCommand(commands, 0, (OnCommandResultListener) null);
|
|
}
|
|
|
|
/**
|
|
* <p>
|
|
* Add commands to execute, with a callback to be called on completion
|
|
* (of all commands)
|
|
* </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) {
|
|
this.commands.add(new Command(commands, code, onCommandResultListener, null));
|
|
runNextCommand();
|
|
}
|
|
|
|
/**
|
|
* <p>
|
|
* Add commands to execute, with a callback. This callback gobbles the
|
|
* output line by line without buffering it and also returns the result
|
|
* code on completion.
|
|
* </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 onCommandLineListener Callback
|
|
*/
|
|
public synchronized void addCommand(String[] commands, int code,
|
|
OnCommandLineListener onCommandLineListener) {
|
|
this.commands.add(new Command(commands, code, null, onCommandLineListener));
|
|
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<String>());
|
|
}
|
|
|
|
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))) {
|
|
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<String> fOutput) {
|
|
if (fCommand.onCommandResultListener == null && fCommand.onCommandLineListener == null) {
|
|
return;
|
|
}
|
|
if (handler == null) {
|
|
if ((fCommand.onCommandResultListener != null) && (fOutput != null))
|
|
fCommand.onCommandResultListener.onCommandResult(fCommand.code, fExitCode,
|
|
fOutput);
|
|
if (fCommand.onCommandLineListener != null)
|
|
fCommand.onCommandLineListener.onCommandResult(fCommand.code, fExitCode);
|
|
return;
|
|
}
|
|
startCallback();
|
|
handler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if ((fCommand.onCommandResultListener != null) && (fOutput != null))
|
|
fCommand.onCommandResultListener.onCommandResult(fCommand.code,
|
|
fExitCode, fOutput);
|
|
if (fCommand.onCommandLineListener != null)
|
|
fCommand.onCommandLineListener
|
|
.onCommandResult(fCommand.code, fExitCode);
|
|
} 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<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(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);
|
|
processLine(line, command.onCommandLineListener);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
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() {
|
|
if (process == null) {
|
|
return false;
|
|
}
|
|
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>
|
|
* 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>
|
|
* 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>
|
|
* 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.
|
|
* </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 (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);
|
|
}
|
|
}
|
|
}
|