/* * Copyright (C) 2012-2019 Jorrit "Chainfire" Jongma * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package eu.chainfire.libsuperuser; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; 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 java.lang.Object; import java.lang.String; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import eu.chainfire.libsuperuser.StreamGobbler.OnLineListener; import eu.chainfire.libsuperuser.StreamGobbler.OnStreamClosedListener; /** * Class providing functionality to execute commands in a (root) shell */ @SuppressWarnings({"WeakerAccess", "UnusedReturnValue", "unused", "StatementWithEmptyBody", "DeprecatedIsStillUsed", "deprecation"}) public class Shell { /** * Exception class used to crash application when shell commands are executed * from the main thread, and we are in debug mode. */ @SuppressWarnings({"serial", "WeakerAccess"}) public static class ShellOnMainThreadException extends RuntimeException { public static final String EXCEPTION_COMMAND = "Application attempted to run a shell command from the main thread"; public static final String EXCEPTION_NOT_IDLE = "Application attempted to wait for a non-idle shell to close on the main thread"; public static final String EXCEPTION_WAIT_IDLE = "Application attempted to wait for a shell to become idle on the main thread"; public static final String EXCEPTION_TOOLBOX = "Application attempted to init the Toolbox class from the main thread"; public ShellOnMainThreadException(String message) { super(message); } } /** * Exception class used to notify developer that a shell was not close()d */ @SuppressWarnings({"serial", "WeakerAccess"}) 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 notify developer that a shell was not close()d */ @SuppressWarnings({"serial", "WeakerAccess"}) public static class ShellDiedException extends Exception { public static final String EXCEPTION_SHELL_DIED = "Shell died (or access was not granted)"; public ShellDiedException() { super(EXCEPTION_SHELL_DIED); } } private static volatile boolean redirectDeprecated = true; /** * @see #setRedirectDeprecated(boolean) * * @return Whether deprecated calls are automatically redirected to {@link PoolWrapper} */ public static boolean isRedirectDeprecated() { return redirectDeprecated; } /** * Set whether deprecated calls (such as Shell.run, Shell.SH/SU.run, etc) should automatically * redirect to Shell.Pool.?.run(). This is true by default, but it is possible to disable this * behavior for backwards compatibility * * @param redirectDeprecated Whether deprecated calls should be automatically redirected to {@link PoolWrapper} (default true) */ public static void setRedirectDeprecated(boolean redirectDeprecated) { Shell.redirectDeprecated = redirectDeprecated; } /** *
* Runs commands using the supplied shell, and returns the output, or null * in case of errors. *
* * @deprecated This method is deprecated and only provided for backwards * compatibility. Use {@link Pool}'s method instead. If {@link #isRedirectDeprecated()} * is true (default), these calls are now automatically redirected. * * @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 */ @Nullable @Deprecated @WorkerThread public static List* Runs commands using the supplied shell, and returns the output, or null * in case of errors. *
** Note that due to compatibility with older Android versions, wantSTDERR is * not implemented using redirectErrorStream, but rather appended to the * output. STDOUT and STDERR are thus not guaranteed to be in the correct * order in the output. *
** Note as well that this code will intentionally crash when run in debug * mode from the main thread of the application. You should always execute * shell commands from a background thread. *
** When in debug mode, the code will also excessively log the commands * passed to and the output returned from the shell. *
** Though this function uses background threads to gobble STDOUT and STDERR * so a deadlock does not occur if the shell produces massive output, the * output is still stored in a List<String>, and as such doing * something like 'ls -lR /' will probably have you run out of * memory. *
* * @deprecated This method is deprecated and only provided for backwards * compatibility. Use {@link Pool}'s method instead. If {@link #isRedirectDeprecated()} * is true (default), these calls are now automatically redirected. * * @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 */ @Nullable @Deprecated @WorkerThread public static List* Detects the version of the su binary installed (if any), if supported * by the binary. Most binaries support two different version numbers, * the public version that is displayed to users, and an internal * version number that is used for version number comparisons. Returns * null if su not available or retrieving the version isn't supported. *
** Note that su binary version and GUI (APK) version can be completely * different. *
** This function caches its result to improve performance on multiple * calls *
* * @param internal Request human-readable version or application internal version * @return String containing the su version or null */ @Nullable @WorkerThread // if not cached public static synchronized String version(boolean internal) { int idx = internal ? 0 : 1; if (suVersion[idx] == null) { String version = null; List* Clears results cached by isSELinuxEnforcing() and version(boolean * internal) calls. *
** 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. *
*/ @AnyThread public static synchronized void clearCachedResults() { isSELinuxEnforcing = null; suVersion[0] = null; suVersion[1] = null; } } /** * DO NOT USE DIRECTLY. Base interface for result callbacks. */ public interface OnResult { // for any callback int WATCHDOG_EXIT = -1; int SHELL_DIED = -2; // for Interactive.open() callbacks only int SHELL_EXEC_FAILED = -3; int SHELL_WRONG_UID = -4; int SHELL_RUNNING = 0; } /** * Callback for {@link Shell.Builder#open(Shell.OnShellOpenResultListener)} */ public interface OnShellOpenResultListener extends OnResult { /** * Callback for shell open result * * @param success whether the shell is opened * @param reason reason why the shell isn't opened */ void onOpenResult(boolean success, int reason); } /** * Command result callback, notifies the recipient of the completion of a * command block, including the (last) exit code, and the full output * * @deprecated You probably want to use {@link OnCommandResultListener2} instead */ @Deprecated public interface OnCommandResultListener extends OnResult { /** ** Command result callback for STDOUT, optionally interleaved with STDERR *
** 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 *
** If wantSTDERR is set, output of STDOUT and STDERR is interleaved into * the output buffer. There are no guarantees of absolutely order * correctness (just like in a real terminal) *
** To get separate STDOUT and STDERR output, use {@link OnCommandResultListener2} *
** See {@link Interactive} for threading details *
* * @param commandCode Value previously supplied to addCommand * @param exitCode Exit code of the last command in the block * @param output All output generated by the command block */ void onCommandResult(int commandCode, int exitCode, @NonNull List* Command result callback with separated STDOUT and STDERR *
** Depending on how and on which thread the shell was created, this * callback may be executed on one of the gobbler threads. In that case, * it is important the callback returns as quickly as possible, as * delays in this callback may pause the native process or even result * in a deadlock *
** See {@link Interactive} for threading details *
* * @param commandCode Value previously supplied to addCommand * @param exitCode Exit code of the last command in the block * @param STDOUT All STDOUT output generated by the command block * @param STDERR All STDERR output generated by the command block */ void onCommandResult(int commandCode, int exitCode, @NonNull List* Command result callback *
** Depending on how and on which thread the shell was created, this * callback may be executed on one of the gobbler threads. In that case, * it is important the callback returns as quickly as possible, as * delays in this callback may pause the native process or even result * in a deadlock *
** See {@link Interactive} for threading details *
* * @param commandCode Value previously supplied to addCommand * @param exitCode Exit code of the last command in the block */ void onCommandResult(int commandCode, int exitCode); } /** * DO NOT USE DIRECTLY. Line callback for STDOUT */ private interface OnCommandLineSTDOUT { /** ** Line callback for STDOUT *
** Depending on how and on which thread the shell was created, this * callback may be executed on one of the gobbler threads. In that case, * it is important the callback returns as quickly as possible, as * delays in this callback may pause the native process or even result * in a deadlock *
** See {@link Interactive} for threading details *
* * @param line One line of STDOUT output */ void onSTDOUT(@NonNull String line); } /** * DO NOT USE DIRECTLY. Line callback for STDERR */ private interface OnCommandLineSTDERR { /** ** Line callback for STDERR *
** Depending on how and on which thread the shell was created, this * callback may be executed on one of the gobbler threads. In that case, * it is important the callback returns as quickly as possible, as * delays in this callback may pause the native process or even result * in a deadlock *
** See {@link Interactive} for threading details *
* * @param line One line of STDERR output */ void onSTDERR(@NonNull String line); } /** * 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 OnCommandResultListenerUnbuffered, OnCommandLineSTDOUT, OnCommandLineSTDERR { } /** * DO NOT USE DIRECTLY. InputStream callback */ public interface OnCommandInputStream extends OnCommandLineSTDERR { /** ** InputStream callback *
** The read() methods will return -1 when all input is consumed, and throw an * IOException if the shell died before all data being read. *
** If a Handler is not setup, 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. It may also be executed on the main thread, in which * case you should offload handling to a different thread *
** If a Handler is setup and it executes callbacks on the main thread, * you should offload handling to a different thread, as reading from * the InputStream would block your UI *
** You must drain the InputStream (read until it returns -1 or throws * an IOException), or call close(), otherwise execution of root commands will * not continue. This cannot be solved automatically without keeping it safe to * offload the InputStream to another thread. *
* * @param inputStream InputStream to read from */ void onInputStream(@NonNull InputStream inputStream); } /** * Command InputStream callback for direct access to STDOUT. It also notifies the * recipient of the completion of a command block, including the (last) exit code. */ public interface OnCommandInputStreamListener extends OnCommandResultListenerUnbuffered, OnCommandInputStream { } /** * Internal class to store command block properties */ private static class Command { private static int commandCounter = 0; private final String[] commands; private final int code; @Nullable private final OnCommandResultListener onCommandResultListener; @Nullable private final OnCommandResultListener2 onCommandResultListener2; @Nullable private final OnCommandLineListener onCommandLineListener; @Nullable private final OnCommandInputStreamListener onCommandInputStreamListener; @NonNull private final String marker; @Nullable private volatile MarkerInputStream markerInputStream = null; @SuppressWarnings("unchecked") // if the user passes in List<> of anything other than String, that's on them public Command(@NonNull Object commands, int code, @Nullable OnResult listener) { if (commands instanceof String) { this.commands = new String[] { (String)commands }; } else if (commands instanceof List>) { this.commands = ((List* Set a custom handler that will be used to post all callbacks to *
** See {@link Interactive} for further details on threading and * handlers *
* * @param handler Handler to use * @return This Builder object for method chaining */ @NonNull public Builder setHandler(@Nullable Handler handler) { this.handler = handler; return this; } /** ** Automatically create a handler if possible ? Default to true *
** See {@link Interactive} for further details on threading and * handlers *
* * @param autoHandler Auto-create handler ? * @return This Builder object for method chaining */ @NonNull 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 */ @NonNull public Builder setShell(@NonNull String shell) { this.shell = shell; return this; } /** * Convenience function to set "sh" as used shell * * @return This Builder object for method chaining */ @NonNull public Builder useSH() { return setShell("sh"); } /** * Convenience function to set "su" as used shell * * @return This Builder object for method chaining */ @NonNull public Builder useSU() { return setShell("su"); } /** ** Detect whether the shell was opened correctly ? *
** When active, this runs test commands in the shell * before it runs your own commands to determine if * the shell is functioning correctly. This is also * required for the {@link Interactive#isOpening()} * method to return a proper result *
** You probably want to keep this turned on, the * option to turn it off exists only to support * code using older versions of this library that * may depend on these commands not being * run *
* * @deprecated New users should leave the default * * @param detectOpen Detect shell running properly (default true) * @return This Builder object for method chaining */ @NonNull @Deprecated public Builder setDetectOpen(boolean detectOpen) { this.detectOpen = detectOpen; return this; } /** ** Treat STDOUT/STDERR close as shell death ? *
** You probably want to keep this turned on. It is not * completely unthinkable you may need to turn this off, * but it is unlikely. Turning it off will break dead * shell detection on commands providing an InputStream *
* * @deprecated New users should leave the default unless absolutely necessary * * @param shellDies Treat STDOUT/STDERR close as shell death (default true) * @return This Builder object for method chaining */ @NonNull @Deprecated public Builder setShellDiesOnSTDOUTERRClose(boolean shellDies) { this.shellDiesOnSTDOUTERRClose = shellDies; return this; } /** ** Set if STDERR output should be interleaved with STDOUT output (only) when {@link OnCommandResultListener} is used *
** If you want separate STDOUT and STDERR output, use {@link OnCommandResultListener2} instead *
* * @deprecated You probably want to use {@link OnCommandResultListener2}, which ignores this setting * * @param wantSTDERR Want error output ? * @return This Builder object for method chaining */ @NonNull @Deprecated 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 */ @NonNull public Builder addEnvironment(@NonNull String key, @NonNull 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 */ @NonNull public Builder addEnvironment(@NonNull Map* Add commands to execute, with a callback. Several callback interfaces are supported *
** {@link OnCommandResultListener2}: provides only a callback with the result of the entire * command and the (last) exit code. The results are buffered until command completion, so * commands that generate massive amounts of output should use {@link OnCommandLineListener} * instead. *
** {@link OnCommandLineListener}: provides a per-line callback without internal buffering. * Also provides a command completion callback with the (last) exit code. *
** {@link OnCommandInputStreamListener}: provides a callback that is called with an * InputStream you can read STDOUT from directly. Also provides a command completion * callback with the (last) exit code. Note that this callback ignores the watchdog. *
** The thread on which the callback executes is dependent on various * factors, see {@link Interactive} for further details *
* * @param commands Commands to execute, accepts String, List<String>, and String[] * @param code User-defined value passed back to the callback * @param onResultListener One of OnCommandResultListener, OnCommandLineListener, OnCommandInputStreamListener * @return This Builder object for method chaining */ @NonNull public Builder addCommand(@NonNull Object commands, int code, @Nullable OnResult onResultListener) { this.commands.add(new Command(commands, code, onResultListener)); return this; } /** ** Set a callback called for every line output to STDOUT by the shell *
** The thread on which the callback executes is dependent on various * factors, see {@link Interactive} for further details *
* * @param onLineListener Callback to be called for each line * @return This Builder object for method chaining */ @NonNull public Builder setOnSTDOUTLineListener(@Nullable OnLineListener onLineListener) { this.onSTDOUTLineListener = onLineListener; return this; } /** ** Set a callback called for every line output to STDERR by the shell *
** The thread on which the callback executes is dependent on various * factors, see {@link Interactive} for further details *
* * @param onLineListener Callback to be called for each line * @return This Builder object for method chaining */ @NonNull public Builder setOnSTDERRLineListener(@Nullable OnLineListener onLineListener) { this.onSTDERRLineListener = onLineListener; return this; } /** ** Enable command timeout callback *
** This will invoke the onCommandResult() callback with exitCode * WATCHDOG_EXIT if a command takes longer than watchdogTimeout seconds * to complete. *
** If a watchdog timeout occurs, it generally means that the Interactive * session is out of sync with the shell process. The caller should * close the current session and open a new one. *
* * @param watchdogTimeout Timeout, in seconds; 0 to disable * @return This Builder object for method chaining */ @NonNull public Builder setWatchdogTimeout(int watchdogTimeout) { this.watchdogTimeout = watchdogTimeout; return this; } /** ** Enable/disable reduced logcat output *
** Note that this is a global setting *
* * @param useMinimal true for reduced output, false for full output * @return This Builder object for method chaining */ @NonNull public Builder setMinimalLogging(boolean useMinimal) { Debug.setLogTypeEnabled(Debug.LOG_COMMAND | Debug.LOG_OUTPUT, !useMinimal); return this; } /** * Construct a {@link Interactive} instance, and start the shell * * @return Interactive shell */ @NonNull public Interactive open() { return new Interactive(this, null); } /** * Construct a {@link Interactive} instance, try to start the * shell, and call onShellOpenResultListener to report success or failure * * @param onShellOpenResultListener Callback to return shell open status * @return Interactive shell */ @NonNull public Interactive open(@Nullable OnShellOpenResultListener onShellOpenResultListener) { return new Interactive(this, onShellOpenResultListener); } /** ** Construct a {@link Threaded} instance, and start the shell *
** {@link Threaded} ignores the {@link #setHandler(Handler)}, * {@link #setAutoHandler(boolean)}, {@link #setDetectOpen(boolean)} * and {@link #setShellDiesOnSTDOUTERRClose(boolean)} settings on this * Builder and uses its own values *
** On API >= 19, the return value is {@link ThreadedAutoCloseable} * rather than {@link Threaded} *
* * @return Threaded interactive shell */ @NonNull public Threaded openThreaded() { return openThreadedEx(null, false); } /** ** Construct a {@link Threaded} instance, try to start the * shell, and call onShellOpenResultListener to report success or failure *
** {@link Threaded} ignores the {@link #setHandler(Handler)}, * {@link #setAutoHandler(boolean)}, {@link #setDetectOpen(boolean)} * and {@link #setShellDiesOnSTDOUTERRClose(boolean)} settings on this * Builder and uses its own values *
** On API >= 19, the return value is {@link ThreadedAutoCloseable} * rather than {@link Threaded} *
* * @param onShellOpenResultListener Callback to return shell open status * @return Threaded interactive shell */ @NonNull public Threaded openThreaded(@Nullable OnShellOpenResultListener onShellOpenResultListener) { return openThreadedEx(onShellOpenResultListener, false); } private Threaded openThreadedEx(OnShellOpenResultListener onShellOpenResultListener, boolean pooled) { if (Build.VERSION.SDK_INT >= 19) { return new ThreadedAutoCloseable(this, onShellOpenResultListener, pooled); } else { return new Threaded(this, onShellOpenResultListener, pooled); } } } /** * Callback interface for {@link SyncCommands#run(Object, Shell.OnSyncCommandLineListener)} */ public interface OnSyncCommandLineListener extends OnCommandLineSTDOUT, OnCommandLineSTDERR { } /** * Callback interface for {@link SyncCommands#run(Object, Shell.OnSyncCommandInputStreamListener)} */ public interface OnSyncCommandInputStreamListener extends OnCommandInputStream, OnCommandLineSTDERR { } /** * Base interface for objects that support deprecated synchronous commands */ @Deprecated @WorkerThread public interface DeprecatedSyncCommands { /** * Run commands, returning the output, or null on error * * @deprecated This methods exists only as drop-in replacement for Shell.SU/SH.run() methods and should not be used by new users * * @param commands Commands to execute, accepts String, List<String>, and String[] * @param wantSTDERR Return STDERR in the output ? * @return Output of the commands, or null in case of an error */ @Nullable @Deprecated List* Run commands and return STDOUT and STDERR output, and exit code *
** Note that all output is buffered, and very large outputs may cause you to run out of * memory. *
* * @param commands Commands to execute, accepts String, List<String>, and String[] * @param STDOUT List<String> to receive STDOUT output, or null * @param STDERR List<String> to receive STDERR output, or null * @param clear Clear STDOUT/STDOUT before adding output ? * @return Exit code * @throws ShellDiedException if shell is closed, was closed during command execution, or was never open (access denied) */ int run(@NonNull Object commands, @Nullable List* Run commands using a callback that receives per-line STDOUT and STDERR output as they happen, and returns exit code *
** You should not call other synchronous methods from the callback *
* * @param commands Commands to execute, accepts String, List<String>, and String[] * @param onSyncCommandLineListener Callback interface for per-line output * @return Exit code * @throws ShellDiedException if shell is closed, was closed during command execution, or was never open (access denied) */ int run(@NonNull Object commands, @NonNull OnSyncCommandLineListener onSyncCommandLineListener) throws ShellDiedException; /** ** Run commands using a callback that receives an InputStream for STDOUT and per-line STDERR output as it happens, and returns exit code *
** You should not call other synchronous methods from the callback *
* * @param commands Commands to execute, accepts String, List<String>, and String[] * @param onSyncCommandInputStreamListener Callback interface for InputStream output * @return Exit code * @throws ShellDiedException if shell is closed, was closed during command execution, or was never open (access denied) */ int run(@NonNull Object commands, @NonNull OnSyncCommandInputStreamListener onSyncCommandInputStreamListener) throws ShellDiedException; } /** ** An interactive shell - initially created with {@link Builder} - * that executes blocks of commands you supply in the background, optionally * calling callbacks as each block completes. *
** STDERR output can be supplied as well, but (just like in a real terminal) * output order between STDOUT and STDERR cannot be guaranteed to be correct. *
** Note as well that the close() and waitForIdle() methods will * intentionally crash when run in debug mode from the main thread of the * application. Any blocking call should be run from a background thread. *
** When in debug mode, the code will also excessively log the commands * passed to and the output returned from the shell. *
** Background threads are used to gobble STDOUT and STDERR so a deadlock does * not occur if the shell produces massive output, but if you're using * {@link OnCommandResultListener} or {@link OnCommandResultListener2} for * callbacks, the output gets added to a List<String> until the command * completes. As such if you're doing something like 'ls -lR /' you * will probably run out of memory. A work-around is to use {@link OnCommandLineListener} * which does not buffer the data nor waste memory, but you should make sure * those callbacks do not block unnecessarily. *
** On which thread the callbacks execute is dependent on your * initialization. You can supply a custom Handler using * {@link Builder#setHandler(Handler)} if needed. If you do not supply * a custom Handler - unless you set * {@link Builder#setAutoHandler(boolean)} to false - a Handler will * be auto-created if the thread used for instantiation of the object has a * Looper. *
** If no Handler was supplied and it was also not auto-created, all * callbacks will be called from either the STDOUT or STDERR gobbler * threads. These are important threads that should be blocked as little as * possible, as blocking them may in rare cases pause the native process or * even create a deadlock. *
** The main thread must certainly have a Looper, thus if you call * {@link Builder#open()} from the main thread, a handler will (by * default) be auto-created, and all the callbacks will be called on the * main thread. While this is often convenient and easy to code with, you * should be aware that if your callbacks are 'expensive' to execute, this * may negatively impact UI performance. *
** Background threads usually do not have a Looper, so calling * {@link Builder#open()} from such a background thread will (by * default) result in all the callbacks being executed in one of the gobbler * threads. You will have to make sure the code you execute in these * callbacks is thread-safe. *
*/ public static class Interactive implements SyncCommands { @Nullable protected final Handler handler; private final boolean autoHandler; private final String shell; private boolean shellDiesOnSTDOUTERRClose; private final boolean wantSTDERR; @NonNull private final List* Is our shell currently being opened ? *
** Requires OnShellOpenResultCallback to be used when opening, or * {@link Builder#setDetectOpen(boolean)} to be true *
* * @return Shell opening ? */ @AnyThread public boolean isOpening() { return isRunning() && opening; } /** * Is our shell still running ? * * @return Shell running ? */ @AnyThread public boolean isRunning() { if (process == null) { return false; } try { process.exitValue(); return false; } catch (IllegalThreadStateException e) { // if this is thrown, we're still running } return true; } /** * Have all commands completed executing ? * * @return Shell idle ? */ @AnyThread public synchronized boolean isIdle() { if (!isRunning()) { idle = true; opening = false; synchronized (idleSync) { idleSync.notifyAll(); } if (lastOpening && !opening) { lastOpening = opening; synchronized (openingSync) { openingSync.notifyAll(); } } } return idle; } private boolean waitForCallbacks() { 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; } /** ** Wait for idle state. As this is a blocking call, you should not call * it from the main UI thread. If you do so and debug mode is enabled, * this method will intentionally crash your app. *
** If not interrupted, this method will not return until all commands * have finished executing. Note that this does not necessarily mean * that all the callbacks have fired yet. *
** If no Handler is used, all callbacks will have been executed when * this method returns. If a Handler is used, and this method is called * from a different thread than associated with the Handler's Looper, * all callbacks will have been executed when this method returns as * well. If however a Handler is used but this method is called from the * same thread as associated with the Handler's Looper, there is no way * to know. *
** In practice this means that in most simple cases all callbacks will * have completed when this method returns, but if you actually depend * on this behavior, you should make certain this is indeed the case. *
** See {@link Interactive} for further details on threading and * handlers *
* * @return True if wait complete, false if wait interrupted */ @WorkerThread 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; } } } return waitForCallbacks(); } return true; } /** ** Wait for shell opening to complete *
** Requires OnShellOpenResultCallback to be used when opening, or * {@link Builder#setDetectOpen(boolean)} to be true *
* * @param defaultIfInterrupted What to return if an interrupt occurs, null to keep waiting * @return If shell was opened successfully */ @WorkerThread public boolean waitForOpened(@Nullable Boolean defaultIfInterrupted) { if (Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) { Debug.log(ShellOnMainThreadException.EXCEPTION_WAIT_IDLE); throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_WAIT_IDLE); } if (isRunning()) { synchronized (openingSync) { while (opening) { try { openingSync.wait(); } catch (InterruptedException e) { if (defaultIfInterrupted != null) { return defaultIfInterrupted; } } } } } return isRunning(); } /** * Are we using a Handler to post callbacks ? * * @return Handler used ? */ @AnyThread public boolean hasHandler() { return (handler != null); } /** * Are there any commands scheduled ? * * @return Commands scheduled ? */ @AnyThread public boolean hasCommands() { return (commands.size() > 0); } // documented in SyncCommands interface @Override @WorkerThread public int run(@NonNull Object commands) throws ShellDiedException { return run(commands, null, null, false); } // documented in SyncCommands interface @Override @WorkerThread public int run(@NonNull Object commands, @Nullable final List* Variant of {@link Interactive} that uses a dedicated background thread for callbacks, * rather than requiring the library users to manage this themselves. It also provides * support for pooling, and is used by {@link Pool} *
** While {@link Interactive}'s asynchronous calls are relatively easy to use from the * main UI thread, many developers struggle with implementing it correctly in background * threads. This class is a one-stop solution for those issues *
** You can use this class from the main UI thread as well, though you should take note * that since the callbacks are run in a background thread, you cannot manipulate the * UI directly. You can use the Activity#runOnUiThread() to work around this *
** Please note that the {@link #close()} method behaves differently from the implementation * in {@link Interactive}! *
* * @see Interactive */ public static class Threaded extends Interactive { private static int threadCounter = 0; private static int incThreadCounter() { synchronized (Threaded.class) { int ret = threadCounter; threadCounter++; return ret; } } @NonNull private final HandlerThread handlerThread; private final boolean pooled; private final Object onCloseCalledSync = new Object(); private volatile boolean onClosedCalled = false; private final Object onPoolRemoveCalledSync = new Object(); private volatile boolean onPoolRemoveCalled = false; private volatile boolean reserved = true; private volatile boolean closeEvenIfPooled = false; private static Handler createHandlerThread() { // to work-around having to call super() as first line in constructor, but still // being able to keep fields final HandlerThread handlerThread = new HandlerThread("Shell.Threaded#" + incThreadCounter()); handlerThread.start(); return new Handler(handlerThread.getLooper()); } /** * The only way to create an instance: Shell.Builder::openThreaded(...) * * @see Shell.Builder#openThreaded() * @see Shell.Builder#openThreaded(Shell.OnShellOpenResultListener) * * @param builder Builder class to take values from * @param onShellOpenResultListener Callback * @param pooled Will this instance be pooled ? */ protected Threaded(Builder builder, OnShellOpenResultListener onShellOpenResultListener, boolean pooled) { super(builder.setHandler(createHandlerThread()). setDetectOpen(true). setShellDiesOnSTDOUTERRClose(true), onShellOpenResultListener); // don't try this at home //noinspection ConstantConditions handlerThread = (HandlerThread)handler.getLooper().getThread(); // it's ok if close is called before this this.pooled = pooled; if (this.pooled) protect(); } @Override protected void finalize() throws Throwable { if (pooled) closed = true; // prevent ShellNotClosedException exception on pool super.finalize(); } private void protect() { synchronized (onCloseCalledSync) { if (!onClosedCalled) { Garbage.protect(this); } } } private void collect() { Garbage.collect(this); } /** ** Redirects to non-blocking {@link #closeWhenIdle()} if this instance is not part of a * pool; if it is, returns the instance to the pool *
** Note that this behavior is different from the superclass' behavior, which redirects * to the blocking {@link #closeImmediately()} *
** This change in behavior between super- and subclass is a clear code smell, but it is * needed to support AutoCloseable, makes the flow with {@link Pool} better, and maintains * compatibility with older code using this library (which there is quite a bit of) *
*/ @Override @AnyThread public void close() { protect(); // NOT close(Immediately), but closeWhenIdle, note! if (pooled) { super.closeWhenIdle(); } else { closeWhenIdle(); } } @Override protected void closeImmediately(boolean fromIdle) { protect(); if (pooled) { if (fromIdle) { boolean callRelease = false; synchronized (onPoolRemoveCalledSync) { if (!onPoolRemoveCalled) { callRelease = true; } } if (callRelease) Pool.releaseReservation(this); if (closeEvenIfPooled) { super.closeImmediately(true); } } else { boolean callRemove = false; synchronized (onPoolRemoveCalledSync) { if (!onPoolRemoveCalled) { onPoolRemoveCalled = true; callRemove = true; } } if (callRemove) Pool.removeShell(this); super.closeImmediately(false); } } else { super.closeImmediately(fromIdle); } } private void closeWhenIdle(boolean fromPool) { protect(); if (pooled) { synchronized (onPoolRemoveCalledSync) { if (!onPoolRemoveCalled) { onPoolRemoveCalled = true; Pool.removeShell(this); } } if (fromPool) { closeEvenIfPooled = true; } } super.closeWhenIdle(); } @Override @AnyThread public void closeWhenIdle() { closeWhenIdle(false); } boolean wasPoolRemoveCalled() { synchronized (onPoolRemoveCalledSync) { return onPoolRemoveCalled; } } @SuppressWarnings("ConstantConditions") // handler is never null @Override protected void onClosed() { // clean up our thread if (inClosingJoin) return; // prevent deadlock, we will be called after if (pooled) { boolean callRemove = false; synchronized (onPoolRemoveCalledSync) { if (!onPoolRemoveCalled) { onPoolRemoveCalled = true; callRemove = true; } } if (callRemove) { protect(); Pool.removeShell(this); } } // we've been GC'd by removeShell above, code below should already have been executed if (onCloseCalledSync == null) return; synchronized (onCloseCalledSync) { if (onClosedCalled) return; onClosedCalled = true; } try { super.onClosed(); } finally { if (!handlerThread.isAlive()) { collect(); } else { handler.post(new Runnable() { @SuppressWarnings("ConstantConditions") // handler is never null @Override public void run() { synchronized (callbackSync) { if (callbacks > 0) { // we still have some callbacks running handler.postDelayed(this, 1000); } else { collect(); if (Build.VERSION.SDK_INT >= 18) { handlerThread.quitSafely(); } else { handlerThread.quit(); } } } } }); } } } /** ** Cast current instance to {@link ThreadedAutoCloseable} which can be used with * try-with-resources *
** On API >= 19, all instances are safe to cast this way, as all instances are * created as {@link ThreadedAutoCloseable}. On older API levels, this returns null *
* * @return ThreadedAutoCloseable on API >= 19, null otherwise */ @Nullable @AnyThread public ThreadedAutoCloseable ac() { if (this instanceof ThreadedAutoCloseable) { return (ThreadedAutoCloseable)this; } else { return null; } } private boolean isReserved() { return reserved; } private void setReserved(boolean reserved) { this.reserved = reserved; } } /** ** AutoClosable variant of {@link Threaded} that can be used with try-with-resources *
** This class is automatically used instead of {@link Threaded} everywhere on * API >= 19. Use {@link Threaded#ac()} to auto-cast (returns null on API <= 19) *
*/ @TargetApi(Build.VERSION_CODES.KITKAT) public static class ThreadedAutoCloseable extends Threaded implements AutoCloseable { protected ThreadedAutoCloseable(@NonNull Builder builder, OnShellOpenResultListener onShellOpenResultListener, boolean pooled) { super(builder, onShellOpenResultListener, pooled); } } /** ** Helper class for pooled command execution *
** {@link Interactive} and {@link Threaded}'s run() methods operate on a specific * shell instance. This class supports the same synchronous methods, but runs them * on any available shell instance from the pool, creating a new instance when none * is available. *
** {@link Pool#SH} and {@link Pool#SH} are instances of this class, you can create * a wrapper for other shell commands using {@link Pool#getWrapper(String)} *
* * @see Pool */ public static class PoolWrapper implements DeprecatedSyncCommands, SyncCommands { private final String shellCommand; /** * Constructor for {@link PoolWrapper} * * @param shell Shell command, like "sh" or "su" */ @AnyThread public PoolWrapper(@NonNull String shell) { this.shellCommand = shell; } /** ** Retrieves a {@link Threaded} instance from the {@link Pool}, creating a new one * if none are available. You must call {@link Threaded#close()} to return * the instance to the {@link Pool} *
** If called from a background thread, the shell is fully opened before this method * returns. If called from the main UI thread, the shell may not have completed * opening. *
* * @return A {@link Threaded} instance from the {@link Pool} * @throws ShellDiedException if a shell could not be retrieved (execution failed, access denied) */ @NonNull @AnyThread public Threaded get() throws ShellDiedException { return Shell.Pool.get(shellCommand); } /** ** Retrieves a {@link Threaded} instance from the {@link Pool}, creating a new one * if none are available. You must call {@link Threaded#close()} to return * the instance to the {@link Pool} *
** The callback with open status is called before this method returns if this method * is called from a background thread. When called from the main UI thread, the * method may return before the callback is executed (or the shell has completed opening) *
* * @param onShellOpenResultListener Callback to return shell open status * @return A {@link Threaded} instance from the {@link Pool} * @throws ShellDiedException if a shell could not be retrieved (execution failed, access denied) */ @NonNull @AnyThread public Threaded get(@Nullable OnShellOpenResultListener onShellOpenResultListener) throws ShellDiedException { return Shell.Pool.get(shellCommand, onShellOpenResultListener); } /** ** Retrieves a new {@link Threaded} instance that is not part of the {@link Pool} *
* * @return A {@link Threaded} instance */ @NonNull @AnyThread public Threaded getUnpooled() { return Shell.Pool.getUnpooled(shellCommand); } /** ** Retrieves a new {@link Threaded} instance that is not part of the {@link Pool} *
* * @param onShellOpenResultListener Callback to return shell open status * @return A {@link Threaded} instance */ @NonNull @AnyThread public Threaded getUnpooled(@Nullable OnShellOpenResultListener onShellOpenResultListener) { return Shell.Pool.getUnpooled(shellCommand, onShellOpenResultListener); } // documented in DeprecatedSyncCommands interface @Nullable @Deprecated @WorkerThread public List* Class that manages {@link Threaded} shell pools. When one of its (or {@link PoolWrapper}'s) * get() methods is used, a shell is retrieved from the pool, or a new one is created if none * are available. You must call {@link Threaded#close()} to return a shell to the pool * for reuse *
** While as many shells are created on-demand as necessary {@link #setPoolSize(int)} governs * how many open shells are kept around in the pool once they become idle. Note that this only * applies to "su"-based (root) shells, there is at most one instance of other shells * (such as "sh") kept around, based on the assumption that starting those is cheap, while * starting a "su"-based shell is expensive (and may interrupt the user with a permission * dialog) *
** If you want to change the default settings the {@link Threaded} shells are created with, * call {@link #setOnNewBuilderListener(OnNewBuilderListener)}. It is advised to this only * once, from Application::onCreate(). If you change this after shells have been created, * you may end up with some shells have the default settings, and others having your * customized ones *
** For convenience, getUnpooled() methods are also provided, creating new {@link Threaded} * shells you can manage on your own, but using the same {@link Builder} settings as * configured for the pool *
** {@link PoolWrapper} instances are setup for you already as {@link Shell.SH} and * {@link Shell.SU}, allowing you to call Shell.SH/SU.run(...) without further management * requirements. These methods retrieve an idle shell from the pool, run the passed * commands in synchronous fashion (throwing {@link ShellDiedException} on any issue), * and return the used shell to pool. Though their signatures are (intentionally) the * same as the run(...) methods from the {@link Threaded} class (and indeed those are * used internally), they should not be confused: the ones in the {@link Threaded} class * operate specifically on that {@link Threaded} instance, that you should have get(...) * before and will close() afterwards, while the {@link PoolWrapper} methods handle the * pooling for you *
** Should you need to pool shells that aren't "sh" or "su", a {@link PoolWrapper} instance * can be created for these with {@link #getWrapper(String)} *
* */ public static class Pool { /** * Callback interface to create a {@link Builder} for new shell instances */ public interface OnNewBuilderListener { /** * Called when a new {@link Builder} needs to be instantiated * * @return New {@link Builder} instance */ @NonNull Shell.Builder newBuilder(); } /** * Default {@link OnNewBuilderListener} interface * * @see #setOnNewBuilderListener(OnNewBuilderListener) */ public static final OnNewBuilderListener defaultOnNewBuilderListener = new OnNewBuilderListener() { @NonNull @SuppressWarnings("deprecation") @Override public Shell.Builder newBuilder() { return (new Shell.Builder()) .setWantSTDERR(true) .setWatchdogTimeout(0) .setMinimalLogging(false); } }; @Nullable private static OnNewBuilderListener onNewBuilderListener = null; @NonNull private static final Map* Retrieve current kept pool size for "su"-based (root) shells. Only one instance of * non-root shells is kept around long-term *
** Note that more shells may be created as needed, this number only indicates how many * idle instances to keep around for later use *
* * @return Current pool size */ @AnyThread public static synchronized int getPoolSize() { return poolSize; } /** ** Set current kept pool size for "su"-based (root) shells. Only one instance of * non-root shells is kept around long-term *
** Note that more shells may be created as needed, this number only indicates how many * idle instances to keep around for later use *
* * @param poolSize Pool size to use */ @AnyThread public static synchronized void setPoolSize(int poolSize) { poolSize = Math.max(poolSize, 1); if (poolSize != Pool.poolSize) { Pool.poolSize = poolSize; cleanup(null, false); } } @NonNull @AnyThread private static synchronized Shell.Builder newBuilder() { if (onNewBuilderListener != null) { return onNewBuilderListener.newBuilder(); } else { return defaultOnNewBuilderListener.newBuilder(); } } /** * Retrieves a new {@link Threaded} instance that is not part of the {@link Pool} * * @param shell Shell command * @return A {@link Threaded} */ @NonNull @AnyThread public static Threaded getUnpooled(@NonNull String shell) { return getUnpooled(shell, null); } /** ** Retrieves a new {@link Threaded} instance that is not part of the {@link Pool}, * with open result callback *
* * @param shell Shell command * @param onShellOpenResultListener Callback to return shell open status * @return A {@link Threaded} */ @NonNull @AnyThread public static Threaded getUnpooled(@NonNull String shell, @Nullable OnShellOpenResultListener onShellOpenResultListener) { return newInstance(shell, onShellOpenResultListener, false); } private static Threaded newInstance(@NonNull String shell, @Nullable OnShellOpenResultListener onShellOpenResultListener, boolean pooled) { Debug.logPool(String.format(Locale.ENGLISH, "newInstance(shell:%s, pooled:%d)", shell, pooled ? 1 : 0)); return newBuilder().setShell(shell).openThreadedEx(onShellOpenResultListener, pooled); } /** * Cleanup cycle for pooled shells * * @param toRemove Shell to remove, should already be closed, or null * @param removeAll Remove all shells, closing them */ private static void cleanup(@Nullable Threaded toRemove, boolean removeAll) { String[] keySet; synchronized (pool) { keySet = pool.keySet().toArray(new String[0]); } for (String key : keySet) { ArrayList* Retrieves a {@link Threaded} instance from the {@link Pool}, creating a new one * if none are available. You must call {@link Threaded#close()} to return * the instance to the {@link Pool} *
** If called from a background thread, the shell is fully opened before this method * returns. If called from the main UI thread, the shell may not have completed * opening. *
* * @param shell Shell command * @return A {@link Threaded} * @throws ShellDiedException if a shell could not be retrieved (execution failed, access denied) */ @NonNull @AnyThread public static Threaded get(@NonNull String shell) throws ShellDiedException { return get(shell, null); } /** ** Retrieves a {@link Threaded} instance from the {@link Pool}, creating a new one * if none are available. You must call {@link Threaded#close()} to return * the instance to the {@link Pool} *
** The callback with open status is called before this method returns if this method * is called from a background thread. When called from the main UI thread, the * method may return before the callback is executed (or the shell has completed opening) *
* * @param shell Shell command * @param onShellOpenResultListener Callback to return shell open status * @return A {@link Threaded} instance from the {@link Pool} * @throws ShellDiedException if a shell could not be retrieved (execution failed, access denied) */ @SuppressLint("WrongThread") @NonNull @AnyThread public static Threaded get(@NonNull String shell, @Nullable final OnShellOpenResultListener onShellOpenResultListener) throws ShellDiedException { Threaded threaded = null; String shellUpper = shell.toUpperCase(Locale.ENGLISH); synchronized (Pool.class) { cleanup(null, false); // find instance ArrayList* Helper class to prevent {@link Threaded} instances being garbage collected too soon. Not * reference counted, a single {@link #collect(Threaded)} call clears the protection. *
*/ static class Garbage { static final ArrayList