diff --git a/app/src/main/java/com/jens/automation2/ActivityPermissions.java b/app/src/main/java/com/jens/automation2/ActivityPermissions.java
index ae01b482..d2dbdb34 100644
--- a/app/src/main/java/com/jens/automation2/ActivityPermissions.java
+++ b/app/src/main/java/com/jens/automation2/ActivityPermissions.java
@@ -510,8 +510,9 @@ public class ActivityPermissions extends Activity
case setAirplaneMode:
addToArrayListUnique(Manifest.permission.WRITE_SETTINGS, requiredPermissions);
addToArrayListUnique(Manifest.permission.ACCESS_NETWORK_STATE, requiredPermissions);
- addToArrayListUnique(permissionNameSuperuser, requiredPermissions);
addToArrayListUnique(Manifest.permission.CHANGE_NETWORK_STATE, requiredPermissions);
+ /* Permission was not required anymore, even before Android 6: https://su.chainfire.eu/#updates-permission
+ addToArrayListUnique(permissionNameSuperuser, requiredPermissions);*/
break;
case setBluetooth:
addToArrayListUnique(Manifest.permission.BLUETOOTH_ADMIN, requiredPermissions);
@@ -522,8 +523,9 @@ public class ActivityPermissions extends Activity
case setDataConnection:
addToArrayListUnique(Manifest.permission.WRITE_SETTINGS, requiredPermissions);
addToArrayListUnique(Manifest.permission.ACCESS_NETWORK_STATE, requiredPermissions);
- addToArrayListUnique(permissionNameSuperuser, requiredPermissions);
addToArrayListUnique(Manifest.permission.CHANGE_NETWORK_STATE, requiredPermissions);
+ /* Permission was not required anymore, even before Android 6: https://su.chainfire.eu/#updates-permission
+ addToArrayListUnique(permissionNameSuperuser, requiredPermissions);*/
break;
case setDisplayRotation:
addToArrayListUnique(Manifest.permission.WRITE_SETTINGS, requiredPermissions);
@@ -1273,7 +1275,8 @@ public class ActivityPermissions extends Activity
mapActionPermissions.put("sendTextMessage", Manifest.permission.SEND_SMS);
mapActionPermissions.put("setAirplaneMode", Manifest.permission.WRITE_SETTINGS);
mapActionPermissions.put("setAirplaneMode", Manifest.permission.ACCESS_NETWORK_STATE);
- mapActionPermissions.put("setAirplaneMode", permissionNameSuperuser);
+ /* Permission was not required anymore, even before Android 6: https://su.chainfire.eu/#updates-permission
+ mapActionPermissions.put("setAirplaneMode", permissionNameSuperuser);*/
mapActionPermissions.put("setAirplaneMode", Manifest.permission.CHANGE_NETWORK_STATE);
mapActionPermissions.put("setBluetooth", Manifest.permission.BLUETOOTH_ADMIN);
mapActionPermissions.put("setBluetooth", Manifest.permission.BLUETOOTH);
@@ -1281,7 +1284,8 @@ public class ActivityPermissions extends Activity
mapActionPermissions.put("setBluetooth", Manifest.permission.WRITE_SETTINGS);
mapActionPermissions.put("setDataConnection", Manifest.permission.WRITE_SETTINGS);
mapActionPermissions.put("setDataConnection", Manifest.permission.ACCESS_NETWORK_STATE);
- mapActionPermissions.put("setDataConnection", permissionNameSuperuser);
+ /* Permission was not required anymore, even before Android 6: https://su.chainfire.eu/#updates-permission
+ mapActionPermissions.put("setDataConnection", permissionNameSuperuser);*/
mapActionPermissions.put("setDataConnection", Manifest.permission.CHANGE_NETWORK_STATE);
mapActionPermissions.put("setDisplayRotation", Manifest.permission.WRITE_SETTINGS);
mapActionPermissions.put("setUsbTethering", Manifest.permission.WRITE_SETTINGS);
diff --git a/app/src/main/java/com/jens/automation2/location/WifiBroadcastReceiver.java b/app/src/main/java/com/jens/automation2/location/WifiBroadcastReceiver.java
index 63aaca7f..c2b2ea53 100644
--- a/app/src/main/java/com/jens/automation2/location/WifiBroadcastReceiver.java
+++ b/app/src/main/java/com/jens/automation2/location/WifiBroadcastReceiver.java
@@ -10,6 +10,7 @@ import android.net.NetworkInfo;
import android.net.wifi.WifiManager;
import android.util.Log;
+import com.jens.automation2.AutomationService;
import com.jens.automation2.Miscellaneous;
import com.jens.automation2.PointOfInterest;
import com.jens.automation2.R;
@@ -101,7 +102,7 @@ public class WifiBroadcastReceiver extends BroadcastReceiver
Miscellaneous.logEvent("i", "WifiReceiver", context.getResources().getString(R.string.poiHasNoWifiNotStoppingCellLocationListener), 2);
}
- findRules(parentLocationProvider);
+ findRules(AutomationService.getInstance());
}
else if(myWifi.isConnectedOrConnecting()) // first time connect from wifi-listener-perspective
{
@@ -113,7 +114,7 @@ public class WifiBroadcastReceiver extends BroadcastReceiver
String ssid = myWifiManager.getConnectionInfo().getSSID();
setLastWifiSsid(ssid);
lastConnectedState = true;
- findRules(parentLocationProvider);
+ findRules(AutomationService.getInstance());
}
else if(!myWifi.isConnectedOrConnecting()) // really disconnected? because sometimes also fires on connect
{
@@ -126,7 +127,7 @@ public class WifiBroadcastReceiver extends BroadcastReceiver
mayCellLocationChangedReceiverBeActivatedFromWifiPointOfWifi = true;
CellLocationChangedReceiver.startCellLocationChangedReceiver();
lastConnectedState = false;
- findRules(parentLocationProvider);
+ findRules(AutomationService.getInstance());
}
catch(Exception e)
{
@@ -141,13 +142,13 @@ public class WifiBroadcastReceiver extends BroadcastReceiver
}
}
- public static void findRules(LocationProvider parentLocationProvider)
+ public static void findRules(AutomationService automationServiceInstance)
{
ArrayList Enable or disable debug mode By default, debug mode is enabled for development
* builds and disabled for exported APKs - see
* BuildConfig.DEBUG Is debug mode enabled ? Log a message (internal) Current debug and enabled logtypes decide what gets logged -
- * even if a custom callback is registered Current debug and enabled logtypes decide what gets logged -
+ * even if a custom callback is registered Log a "general" message These messages are infrequent and mostly occur at startup/shutdown or on error Log a "per-command" message This could produce a lot of output if the client runs many commands in the session Log a line of stdout/stderr output This could produce a lot of output if the shell commands are noisy Log pool event Enable or disable logging specific types of message You may | (or) LOG_* constants together. Note that
* debug mode must also be enabled for actual logging to
* occur. Is logging for specific types of messages enabled ? You may | (or) LOG_* constants together, to learn if
* all passed message types are enabled for logging. Note
* that debug mode must also be enabled for actual logging
* to occur. Is logging for specific types of messages enabled ? You may | (or) LOG_* constants together, to learn if
* all message types are enabled for logging. Takes
* debug mode into account for the result. Register a custom log handler Replaces the log method (write to logcat) with your own
* handler. Whether your handler gets called is still dependent
* on debug mode and message types being enabled for logging. Get the currently registered custom log handler Enable or disable sanity checks Enables or disables the library crashing when su is called
+ *
+ * Enables or disables the library crashing when su is called
* from the main thread. Are sanity checks enabled ? Note that debug mode must also be enabled for actual
- * sanity checks to occur.
Are sanity checks enabled ?
- * - *Takes debug mode into account for the result.
- * + * + *Takes debug mode into account for the result.
+ * * @return True if enabled */ public static boolean getSanityChecksEnabledEffective() { @@ -232,11 +253,11 @@ public class Debug { /** *Are we running on the main thread ?
- * + * * @return Running on main thread ? - */ + */ public static boolean onMainThread() { - return ((Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper())); + return ((Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper()) && (Process.myUid() != 0)); } } diff --git a/app/src/main/java/eu/chainfire/libsuperuser/HideOverlaysReceiver.java b/app/src/main/java/eu/chainfire/libsuperuser/HideOverlaysReceiver.java index 4b4ce5a8..f1e43f0c 100644 --- a/app/src/main/java/eu/chainfire/libsuperuser/HideOverlaysReceiver.java +++ b/app/src/main/java/eu/chainfire/libsuperuser/HideOverlaysReceiver.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma + * 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. @@ -37,6 +37,7 @@ import android.content.Intent; * window possibly obscuring SuperSU dialogs". * */ +@SuppressWarnings({"unused"}) public abstract class HideOverlaysReceiver extends BroadcastReceiver { public static final String ACTION_HIDE_OVERLAYS = "eu.chainfire.supersu.action.HIDE_OVERLAYS"; public static final String CATEGORY_HIDE_OVERLAYS = Intent.CATEGORY_INFO; @@ -45,15 +46,17 @@ public abstract class HideOverlaysReceiver extends BroadcastReceiver { @Override public final void onReceive(Context context, Intent intent) { if (intent.hasExtra(EXTRA_HIDE_OVERLAYS)) { - onHideOverlays(intent.getBooleanExtra(EXTRA_HIDE_OVERLAYS, false)); + onHideOverlays(context, intent, intent.getBooleanExtra(EXTRA_HIDE_OVERLAYS, false)); } } /** * Called when overlays should be hidden or may be shown * again. - * + * + * @param context App context + * @param intent Received intent * @param hide Should overlays be hidden? */ - public abstract void onHideOverlays(boolean hide); + public abstract void onHideOverlays(Context context, Intent intent, boolean hide); } diff --git a/app/src/main/java/eu/chainfire/libsuperuser/MarkerInputStream.java b/app/src/main/java/eu/chainfire/libsuperuser/MarkerInputStream.java new file mode 100644 index 00000000..1c55e0c8 --- /dev/null +++ b/app/src/main/java/eu/chainfire/libsuperuser/MarkerInputStream.java @@ -0,0 +1,186 @@ +/* + * 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 java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; + +@SuppressWarnings("WeakerAccess") +@AnyThread +public class MarkerInputStream extends InputStream { + private static final String EXCEPTION_EOF = "EOF encountered, shell probably died"; + + @NonNull + private final StreamGobbler gobbler; + private final InputStream inputStream; + private final byte[] marker; + private final int markerLength; + private final int markerMaxLength; + private final byte[] read1 = new byte[1]; + private final byte[] buffer = new byte[65536]; + private int bufferUsed = 0; + private volatile boolean eof = false; + private volatile boolean done = false; + + public MarkerInputStream(@NonNull StreamGobbler gobbler, @NonNull String marker) throws UnsupportedEncodingException { + this.gobbler = gobbler; + this.gobbler.suspendGobbling(); + this.inputStream = gobbler.getInputStream(); + this.marker = marker.getBytes("UTF-8"); + this.markerLength = marker.length(); + this.markerMaxLength = marker.length() + 5; // marker + space + exitCode(max(3)) + \n + } + + @Override + public int read() throws IOException { + while (true) { + int r = read(read1, 0, 1); + if (r < 0) return -1; + if (r == 0) { + // wait for data to become available + try { + Thread.sleep(16); + } catch (InterruptedException e) { + // no action + } + continue; + } + return (int)read1[0] & 0xFF; + } + } + + @Override + public int read(@NonNull byte[] b) throws IOException { + return read(b, 0, b.length); + } + + private void fill(int safeSizeToWaitFor) { + // fill up our own buffer + if (isEOF()) return; + try { + int a; + while (((a = inputStream.available()) > 0) || (safeSizeToWaitFor > 0)) { + int left = buffer.length - bufferUsed; + if (left == 0) return; + int r = inputStream.read(buffer, bufferUsed, Math.max(safeSizeToWaitFor, Math.min(a, left))); + if (r >= 0) { + bufferUsed += r; + safeSizeToWaitFor -= r; + } else { + // This shouldn't happen *unless* we have both the full content and the end + // marker, otherwise the shell was interrupted/died. An IOException is raised + // in read() below if that is the case. + setEOF(); + break; + } + } + } catch (IOException e) { + setEOF(); + } + } + + @Override + public synchronized int read(@NonNull byte[] b, int off, int len) throws IOException { + if (done) return -1; + + fill(markerLength - bufferUsed); + + // we need our buffer to be big enough to detect the marker + if (bufferUsed < markerLength) return 0; + + // see if we have our marker + int match = -1; + for (int i = Math.max(0, bufferUsed - markerMaxLength); i < bufferUsed - markerLength; i++) { + boolean found = true; + for (int j = 0; j < markerLength; j++) { + if (buffer[i + j] != marker[j]) { + found = false; + break; + } + } + if (found) { + match = i; + break; + } + } + + if (match == 0) { + // marker is at the front of the buffer + while (buffer[bufferUsed -1] != (byte)'\n') { + if (isEOF()) throw new IOException(EXCEPTION_EOF); + fill(1); + } + if (gobbler.getOnLineListener() != null) gobbler.getOnLineListener().onLine(new String(buffer, 0, bufferUsed - 1, "UTF-8")); + done = true; + return -1; + } else { + int ret; + if (match == -1) { + if (isEOF()) throw new IOException(EXCEPTION_EOF); + + // marker isn't in the buffer, drain as far as possible while keeping some space + // leftover so we can still find the marker if its read is split between two fill() + // calls + ret = Math.min(len, bufferUsed - markerMaxLength); + } else { + // even if eof, it is possibly we have both the content and the end marker, which + // counts as a completed command, so we don't throw IOException here + + // marker found, max drain up to marker, this will eventually cause the marker to be + // at the front of the buffer + ret = Math.min(len, match); + } + if (ret > 0) { + System.arraycopy(buffer, 0, b, off, ret); + bufferUsed -= ret; + System.arraycopy(buffer, ret, buffer, 0, bufferUsed); + } else { + try { + // prevent 100% CPU on reading from for example /dev/random + Thread.sleep(4); + } catch (Exception e) { + // no action + } + } + return ret; + } + } + + @SuppressWarnings("StatementWithEmptyBody") + @Override + public synchronized void close() throws IOException { + if (!isEOF() && !done) { + // drain + byte[] buffer = new byte[1024]; + while (read(buffer) >= 0) { + } + } + } + + public synchronized boolean isEOF() { + return eof; + } + + public synchronized void setEOF() { + eof = true; + } +} diff --git a/app/src/main/java/eu/chainfire/libsuperuser/Shell.java b/app/src/main/java/eu/chainfire/libsuperuser/Shell.java index 2c2386df..da387062 100644 --- a/app/src/main/java/eu/chainfire/libsuperuser/Shell.java +++ b/app/src/main/java/eu/chainfire/libsuperuser/Shell.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma + * 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. @@ -16,7 +16,11 @@ 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; @@ -24,6 +28,7 @@ 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; @@ -34,31 +39,102 @@ 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. *
- *- * 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. - *
- * + * + * @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 - public static List- * Command result callback + * Command result callback for STDOUT, optionally interleaved with STDERR *
** Depending on how and on which thread the shell was created, this @@ -551,23 +732,56 @@ public class Shell { * 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, 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 @@ -582,13 +796,112 @@ public class Shell { *
* 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 */ @@ -597,32 +910,77 @@ public class Shell { 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; - public Command(String[] commands, int code, - OnCommandResultListener onCommandResultListener, - OnCommandLineListener onCommandLineListener) { - this.commands = commands; + @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+ * 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; @@ -703,119 +1129,72 @@ public class Shell { /** * 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) { + @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 */ - public Builder addEnvironment(Map- * Add a command to execute, with a callback to be called on completion - *
- *- * The thread on which the callback executes is dependent on various - * factors, see {@link Interactive} for further details - *
- * - * @param command Command to execute - * @param code User-defined value passed back to the callback - * @param onCommandResultListener Callback to be called on completion - * @return This Builder object for method chaining - */ - public Builder addCommand(String command, int code, - OnCommandResultListener onCommandResultListener) { - return addCommand(new String[] { - command - }, code, onCommandResultListener); - } - - /** - * Add commands to execute - * - * @param commands Commands to execute - * @return This Builder object for method chaining - */ - public Builder addCommand(List- * Add commands to execute, with a callback to be called on completion - * (of all commands) + * 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 + * + * @param commands Commands to execute, accepts String, List<String>, and String[] * @param code User-defined value passed back to the callback - * @param onCommandResultListener Callback to be called on completion - * (of all commands) + * @param onResultListener One of OnCommandResultListener, OnCommandLineListener, OnCommandInputStreamListener * @return This Builder object for method chaining */ - public Builder addCommand(List- * Add commands to execute, with a callback to be called on completion - * (of all commands) - *
- *- * The thread on which the callback executes is dependent on various - * factors, see {@link Interactive} for further details - *
- * - * @param commands Commands to execute - * @param code User-defined value passed back to the callback - * @param onCommandResultListener Callback to be called on completion - * (of all commands) - * @return This Builder object for method chaining - */ - public Builder addCommand(String[] commands, int code, - OnCommandResultListener onCommandResultListener) { - this.commands.add(new Command(commands, code, onCommandResultListener, null)); + @NonNull + public Builder addCommand(@NonNull Object commands, int code, @Nullable OnResult onResultListener) { + this.commands.add(new Command(commands, code, onResultListener)); return this; } @@ -827,11 +1206,12 @@ public class 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 */ - public Builder setOnSTDOUTLineListener(OnLineListener onLineListener) { + @NonNull + public Builder setOnSTDOUTLineListener(@Nullable OnLineListener onLineListener) { this.onSTDOUTLineListener = onLineListener; return this; } @@ -844,11 +1224,12 @@ public class 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 */ - public Builder setOnSTDERRLineListener(OnLineListener onLineListener) { + @NonNull + public Builder setOnSTDERRLineListener(@Nullable OnLineListener onLineListener) { this.onSTDERRLineListener = onLineListener; return this; } @@ -867,10 +1248,11 @@ public class Shell { * 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; @@ -883,10 +1265,11 @@ public class Shell { ** 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; @@ -894,20 +1277,188 @@ public class Shell { /** * 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 onCommandResultListener to report success or failure - * - * @param onCommandResultListener Callback to return shell open status + * shell, and call onShellOpenResultListener to report success or failure + * + * @param onShellOpenResultListener Callback to return shell open status + * @return Interactive shell */ - public Interactive open(OnCommandResultListener onCommandResultListener) { - return new Interactive(this, onCommandResultListener); + @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; } /** @@ -917,10 +1468,8 @@ public class Shell { * calling callbacks as each block completes. * *- * STDERR output can be supplied as well, but due to compatibility with - * older Android versions, wantSTDERR is not implemented using - * redirectErrorStream, but rather appended to the output. STDOUT and STDERR - * are thus not guaranteed to be in the correct order in the output. + * 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 @@ -932,14 +1481,14 @@ public class Shell { * passed to and the output returned from the shell. *
*- * Though this function uses background threads to gobble STDOUT and STDERR - * so a deadlock does not occur if the shell produces massive output, the - * output is still stored in a List<String>, and as such doing - * something like 'ls -lR /' will probably have you run out of - * memory when using a {@link OnCommandResultListener}. A work-around - * is to not supply this callback, but using (only) - * {@link Builder#setOnSTDOUTLineListener(OnLineListener)}. This way, - * an internal buffer will not be created and wasting your memory. + * 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. *
*@@ -974,47 +1523,79 @@ public class Shell { * callbacks is thread-safe. *
*/ - public static class Interactive { - private final Handler handler; + 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- * Add a command to execute, with a callback to be called on completion - *
- *- * The thread on which the callback executes is dependent on various - * factors, see {@link Interactive} for further details - *
- * - * @param command Command to execute + * Add commands to execute with a callback. See {@link Shell.Builder#addCommand(Object, int, Shell.OnResult)} + * for details + * + * @see Shell.Builder#addCommand(Object, int, Shell.OnResult) + * + * @param commands Commands to execute, accepts String, List<String>, and String[] * @param code User-defined value passed back to the callback - * @param onCommandResultListener Callback to be called on completion + * @param onResultListener One of OnCommandResultListener, OnCommandLineListener, OnCommandInputStreamListener */ - public void addCommand(String command, int code, - OnCommandResultListener onCommandResultListener) { - addCommand(new String[] { - command - }, code, onCommandResultListener); - } - - /** - *- * 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. - *
- *- * The thread on which the callback executes is dependent on various - * factors, see {@link Interactive} for further details - *
- * - * @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- * Add commands to execute, with a callback to be called on completion - * (of all commands) - *
- *- * The thread on which the callback executes is dependent on various - * factors, see {@link Interactive} for further details - *
- * - * @param commands Commands to execute - * @param code User-defined value passed back to the callback - * @param onCommandResultListener Callback to be called on completion - * (of all commands) - */ - public void addCommand(List- * 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. - *
- *- * The thread on which the callback executes is dependent on various - * factors, see {@link Interactive} for further details - *
- * - * @param commands Commands to execute - * @param code User-defined value passed back to the callback - * @param onCommandLineListener Callback - */ - public void addCommand(List- * Add commands to execute, with a callback to be called on completion - * (of all commands) - *
- *- * The thread on which the callback executes is dependent on various - * factors, see {@link Interactive} for further details - *
- * - * @param commands Commands to execute - * @param code User-defined value passed back to the callback - * @param onCommandResultListener Callback to be called on completion - * (of all commands) - */ - public synchronized void addCommand(String[] commands, int code, - OnCommandResultListener onCommandResultListener) { - this.commands.add(new Command(commands, code, onCommandResultListener, null)); - runNextCommand(); - } - - /** - *- * 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. - *
- *- * The thread on which the callback executes is dependent on various - * factors, see {@link Interactive} for further details - *
- * - * @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)); + @AnyThread + public synchronized void addCommand(@NonNull Object commands, int code, @Nullable OnResult onResultListener) { + this.commands.add(new Command(commands, code, onResultListener)); runNextCommand(); } @@ -1236,23 +1739,26 @@ public class Shell { return; if (!isRunning()) { - exitCode = OnCommandResultListener.SHELL_DIED; - Debug.log(String.format("[%s%%] SHELL_DIED", shell.toUpperCase(Locale.ENGLISH))); + exitCode = OnResult.SHELL_DIED; + Debug.log(String.format(Locale.ENGLISH, "[%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))); + exitCode = OnResult.WATCHDOG_EXIT; + Debug.log(String.format(Locale.ENGLISH, "[%s%%] WATCHDOG_EXIT", shell.toUpperCase(Locale.ENGLISH))); } - if (handler != null) { - postCallback(command, exitCode, buffer); + if (command != null) { + //noinspection ConstantConditions // all write to 'command' are synchronized + postCallback(command, exitCode, bufferSTDOUT, bufferSTDERR, null); } // prevent multiple callbacks for the same command command = null; - buffer = null; + bufferSTDOUT = null; + bufferSTDERR = null; idle = true; + opening = false; watchdog.shutdown(); watchdog = null; @@ -1288,62 +1794,104 @@ public class Shell { /** * 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) + if (!running || closed) { idle = true; + opening = false; + } - if (running && idle && (commands.size() > 0)) { + if (running && !closed && idle && (commands.size() > 0)) { Command command = commands.get(0); commands.remove(0); - buffer = null; + bufferSTDOUT = null; + bufferSTDERR = 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+ * 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; @@ -1658,19 +2428,51 @@ public class Shell { /** * 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 @@ -1700,9 +2502,10 @@ public class Shell { * 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); @@ -1720,38 +2523,1105 @@ public class Shell { } } - 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 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 ArrayListLine callback
- * + * *This callback should process the line as quickly as possible. * Delays in this callback may pause the native process or even * result in a deadlock
- * + * * @param line String that was gobbled */ void onLine(String line); } - private String shell = null; - private BufferedReader reader = null; - private ListStream closed callback
+ */ + void onStreamClosed(); + } + + @NonNull + private final String shell; + @NonNull + private final InputStream inputStream; + @NonNull + private final BufferedReader reader; + @Nullable + private final ListStreamGobbler constructor
- * - *We use this class because shell STDOUT and STDERR should be read as quickly as + * + *
We use this class because shell STDOUT and STDERR should be read as quickly as * possible to prevent a deadlock from occurring, or Process.waitFor() never * returning (as the buffer is full, pausing the native process)
- * + * * @param shell Name of the shell * @param inputStream InputStream to read from - * @param outputList ListStreamGobbler constructor
- * - *We use this class because shell STDOUT and STDERR should be read as quickly as + * + *
We use this class because shell STDOUT and STDERR should be read as quickly as * possible to prevent a deadlock from occurring, or Process.waitFor() never * returning (as the buffer is full, pausing the native process)
- * + * * @param shell Name of the shell * @param inputStream InputStream to read from * @param onLineListener OnLineListener callback + * @param onStreamClosedListener OnStreamClosedListener callback */ - public StreamGobbler(String shell, InputStream inputStream, OnLineListener onLineListener) { + @AnyThread + public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, @Nullable OnLineListener onLineListener, @Nullable OnStreamClosedListener onStreamClosedListener) { + super("Gobbler#" + incThreadCounter()); this.shell = shell; + this.inputStream = inputStream; reader = new BufferedReader(new InputStreamReader(inputStream)); - listener = onLineListener; + lineListener = onLineListener; + streamClosedListener = onStreamClosedListener; + writer = null; } @Override public void run() { // keep reading the InputStream until it ends (or an error occurs) + // optionally pausing when a command is executed that consumes the InputStream itself try { String line; while ((line = reader.readLine()) != null) { - Debug.logOutput(String.format("[%s] %s", shell, line)); + Debug.logOutput(String.format(Locale.ENGLISH, "[%s] %s", shell, line)); if (writer != null) writer.add(line); - if (listener != null) listener.onLine(line); + if (lineListener != null) lineListener.onLine(line); + while (!active) { + synchronized (this) { + try { + this.wait(128); + } catch (InterruptedException e) { + // no action + } + } + } } } catch (IOException e) { // reader probably closed, expected exit condition + if (streamClosedListener != null) { + calledOnClose = true; + streamClosedListener.onStreamClosed(); + } } // make sure our stream is closed and resources will be freed @@ -101,5 +162,96 @@ public class StreamGobbler extends Thread { } catch (IOException e) { // read already closed } + + if (!calledOnClose) { + if (streamClosedListener != null) { + calledOnClose = true; + streamClosedListener.onStreamClosed(); + } + } + } + + /** + *Resume consuming the input from the stream
+ */ + @AnyThread + public void resumeGobbling() { + if (!active) { + synchronized (this) { + active = true; + this.notifyAll(); + } + } + } + + /** + *Suspend gobbling, so other code may read from the InputStream instead
+ * + *This should only be called from the OnLineListener callback!
+ */ + @AnyThread + public void suspendGobbling() { + synchronized (this) { + active = false; + this.notifyAll(); + } + } + + /** + *Wait for gobbling to be suspended
+ * + *Obviously this cannot be called from the same thread as {@link #suspendGobbling()}
+ */ + @WorkerThread + public void waitForSuspend() { + synchronized (this) { + while (active) { + try { + this.wait(32); + } catch (InterruptedException e) { + // no action + } + } + } + } + + /** + *Is gobbling suspended ?
+ * + * @return is gobbling suspended? + */ + @AnyThread + public boolean isSuspended() { + synchronized (this) { + return !active; + } + } + + /** + *Get current source InputStream
+ * + * @return source InputStream + */ + @NonNull + @AnyThread + public InputStream getInputStream() { + return inputStream; + } + + /** + *Get current OnLineListener
+ * + * @return OnLineListener + */ + @Nullable + @AnyThread + public OnLineListener getOnLineListener() { + return lineListener; + } + + void conditionalJoin() throws InterruptedException { + if (calledOnClose) return; // deadlock from callback, we're inside exit procedure + if (Thread.currentThread() == this) return; // can't join self + join(); } }