From 1560fd334308b3fa93b1af33136db5ce292da635 Mon Sep 17 00:00:00 2001
From: jens
Date: Tue, 18 May 2021 20:02:45 +0200
Subject: [PATCH] SuperSu related changes.
---
.../jens/automation2/ActivityPermissions.java | 12 +-
.../location/WifiBroadcastReceiver.java | 13 +-
.../receivers/ConnectivityReceiver.java | 4 +-
.../chainfire/libsuperuser/Application.java | 21 +-
.../java/eu/chainfire/libsuperuser/Debug.java | 125 +-
.../libsuperuser/HideOverlaysReceiver.java | 11 +-
.../libsuperuser/MarkerInputStream.java | 186 ++
.../java/eu/chainfire/libsuperuser/Shell.java | 2906 ++++++++++++++---
.../chainfire/libsuperuser/StreamGobbler.java | 194 +-
9 files changed, 2858 insertions(+), 614 deletions(-)
create mode 100644 app/src/main/java/eu/chainfire/libsuperuser/MarkerInputStream.java
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 ruleCandidates = Rule.findRuleCandidatesByWifiConnection();
for(Rule oneRule : ruleCandidates)
{
- if(oneRule.applies(parentLocationProvider.parentService))
- oneRule.activate(parentLocationProvider.parentService, false);
+ if(oneRule.applies(automationServiceInstance))
+ oneRule.activate(automationServiceInstance, false);
}
}
diff --git a/app/src/main/java/com/jens/automation2/receivers/ConnectivityReceiver.java b/app/src/main/java/com/jens/automation2/receivers/ConnectivityReceiver.java
index 43e0b70b..5345d8c8 100644
--- a/app/src/main/java/com/jens/automation2/receivers/ConnectivityReceiver.java
+++ b/app/src/main/java/com/jens/automation2/receivers/ConnectivityReceiver.java
@@ -161,7 +161,7 @@ public class ConnectivityReceiver extends BroadcastReceiver implements Automatio
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
WifiBroadcastReceiver.setLastWifiSsid(wifiInfo.getSSID());
- WifiBroadcastReceiver.findRules(automationServiceRef.getLocationProvider());
+ WifiBroadcastReceiver.findRules(automationServiceRef);
break;
case ConnectivityManager.TYPE_MOBILE:
boolean isRoaming = isRoaming(context);
@@ -219,7 +219,7 @@ public class ConnectivityReceiver extends BroadcastReceiver implements Automatio
// This will serve as a disconnected event. Happens if wifi is connected, then module deactivated.
Miscellaneous.logEvent("i", "Connectivity", "Wifi deactivated while having been connected before.", 4);
WifiBroadcastReceiver.lastConnectedState = false;
- WifiBroadcastReceiver.findRules(automationServiceRef.getLocationProvider());
+ WifiBroadcastReceiver.findRules(automationServiceRef);
}
}
}
diff --git a/app/src/main/java/eu/chainfire/libsuperuser/Application.java b/app/src/main/java/eu/chainfire/libsuperuser/Application.java
index 095c7209..949affd8 100644
--- a/app/src/main/java/eu/chainfire/libsuperuser/Application.java
+++ b/app/src/main/java/eu/chainfire/libsuperuser/Application.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.
@@ -20,18 +20,24 @@ import android.content.Context;
import android.os.Handler;
import android.widget.Toast;
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
/**
* Base application class to extend from, solving some issues with
* toasts and AsyncTasks you are likely to run into
*/
+@SuppressWarnings("WeakerAccess")
public class Application extends android.app.Application {
/**
* Shows a toast message
- *
+ *
* @param context Any context belonging to this application
* @param message The message to show
*/
- public static void toast(Context context, String message) {
+ @AnyThread
+ public static void toast(@Nullable Context context, @NonNull String message) {
// this is a static method so it is easier to call,
// as the context checking and casting is done for you
@@ -45,7 +51,7 @@ public class Application extends android.app.Application {
final Context c = context;
final String m = message;
- ((Application)context).runInApplicationThread(new Runnable() {
+ ((Application) context).runInApplicationThread(new Runnable() {
@Override
public void run() {
Toast.makeText(c, m, Toast.LENGTH_LONG).show();
@@ -54,14 +60,15 @@ public class Application extends android.app.Application {
}
}
- private static Handler mApplicationHandler = new Handler();
+ private static final Handler mApplicationHandler = new Handler();
/**
* Run a runnable in the main application thread
- *
+ *
* @param r Runnable to run
*/
- public void runInApplicationThread(Runnable r) {
+ @AnyThread
+ public void runInApplicationThread(@NonNull Runnable r) {
mApplicationHandler.post(r);
}
diff --git a/app/src/main/java/eu/chainfire/libsuperuser/Debug.java b/app/src/main/java/eu/chainfire/libsuperuser/Debug.java
index 4ad07187..08cab75d 100644
--- a/app/src/main/java/eu/chainfire/libsuperuser/Debug.java
+++ b/app/src/main/java/eu/chainfire/libsuperuser/Debug.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.
@@ -18,12 +18,19 @@ package eu.chainfire.libsuperuser;
import android.os.Looper;
import android.util.Log;
+import android.os.Process;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.jens.automation2.BuildConfig;
/**
* Utility class for logging and debug features that (by default) does nothing when not in debug mode
*/
+@SuppressWarnings({"WeakerAccess", "UnusedReturnValue", "unused"})
+@AnyThread
public class Debug {
// ----- DEBUGGING -----
@@ -32,23 +39,23 @@ public class Debug {
/**
* Enable or disable debug mode
- *
+ *
* By default, debug mode is enabled for development
* builds and disabled for exported APKs - see
* BuildConfig.DEBUG
- *
+ *
* @param enable Enable debug mode ?
- */
- public static void setDebug(boolean enable) {
- debug = enable;
+ */
+ public static void setDebug(boolean enable) {
+ debug = enable;
}
/**
* Is debug mode enabled ?
- *
+ *
* @return Debug mode enabled
*/
- public static boolean getDebug() {
+ public static boolean getDebug() {
return debug;
}
@@ -63,25 +70,27 @@ public class Debug {
public static final int LOG_GENERAL = 0x0001;
public static final int LOG_COMMAND = 0x0002;
public static final int LOG_OUTPUT = 0x0004;
+ public static final int LOG_POOL = 0x0008;
public static final int LOG_NONE = 0x0000;
public static final int LOG_ALL = 0xFFFF;
private static int logTypes = LOG_ALL;
+ @Nullable
private static OnLogListener logListener = null;
/**
* Log a message (internal)
- *
- * Current debug and enabled logtypes decide what gets logged -
- * even if a custom callback is registered
- *
- * @param type Type of message to log
+ *
+ * Current debug and enabled logtypes decide what gets logged -
+ * even if a custom callback is registered
+ *
+ * @param type Type of message to log
* @param typeIndicator String indicator for message type
- * @param message The message to log
+ * @param message The message to log
*/
- private static void logCommon(int type, String typeIndicator, String message) {
+ private static void logCommon(int type, @NonNull String typeIndicator, @NonNull String message) {
if (debug && ((logTypes & type) == type)) {
if (logListener != null) {
logListener.onLog(type, typeIndicator, message);
@@ -89,52 +98,61 @@ public class Debug {
Log.d(TAG, "[" + TAG + "][" + typeIndicator + "]" + (!message.startsWith("[") && !message.startsWith(" ") ? " " : "") + message);
}
}
- }
+ }
/**
* Log a "general" message
- *
+ *
* These messages are infrequent and mostly occur at startup/shutdown or on error
- *
+ *
* @param message The message to log
*/
- public static void log(String message) {
+ public static void log(@NonNull String message) {
logCommon(LOG_GENERAL, "G", message);
}
/**
* Log a "per-command" message
- *
+ *
* This could produce a lot of output if the client runs many commands in the session
- *
+ *
* @param message The message to log
*/
- public static void logCommand(String message) {
+ public static void logCommand(@NonNull String message) {
logCommon(LOG_COMMAND, "C", message);
}
/**
* Log a line of stdout/stderr output
- *
+ *
* This could produce a lot of output if the shell commands are noisy
- *
+ *
* @param message The message to log
*/
- public static void logOutput(String message) {
+ public static void logOutput(@NonNull String message) {
logCommon(LOG_OUTPUT, "O", message);
}
+ /**
+ * Log pool event
+ *
+ * @param message The message to log
+ */
+ public static void logPool(@NonNull String message) {
+ logCommon(LOG_POOL, "P", message);
+ }
+
/**
* Enable or disable logging specific types of message
- *
+ *
* You may | (or) LOG_* constants together. Note that
* debug mode must also be enabled for actual logging to
* occur.
- *
- * @param type LOG_* constants
+ *
+ * @param type LOG_* constants
* @param enable Enable or disable
*/
- public static void setLogTypeEnabled(int type, boolean enable) {
+ public static void setLogTypeEnabled(int type, boolean enable) {
if (enable) {
logTypes |= type;
} else {
@@ -144,26 +162,28 @@ public class Debug {
/**
* Is logging for specific types of messages enabled ?
- *
+ *
* You may | (or) LOG_* constants together, to learn if
* all passed message types are enabled for logging. Note
* that debug mode must also be enabled for actual logging
* to occur.
- *
+ *
* @param type LOG_* constants
+ * @return enabled?
*/
- public static boolean getLogTypeEnabled(int type) {
- return ((logTypes & type) == type);
+ public static boolean getLogTypeEnabled(int type) {
+ return ((logTypes & type) == type);
}
/**
* Is logging for specific types of messages enabled ?
- *
+ *
* You may | (or) LOG_* constants together, to learn if
* all message types are enabled for logging. Takes
* debug mode into account for the result.
- *
+ *
* @param type LOG_* constants
+ * @return enabled and in debug mode?
*/
public static boolean getLogTypeEnabledEffective(int type) {
return getDebug() && getLogTypeEnabled(type);
@@ -171,22 +191,23 @@ public class Debug {
/**
* Register a custom log handler
- *
+ *
* Replaces the log method (write to logcat) with your own
* handler. Whether your handler gets called is still dependent
* on debug mode and message types being enabled for logging.
- *
+ *
* @param onLogListener Custom log listener or NULL to revert to default
*/
- public static void setOnLogListener(OnLogListener onLogListener) {
+ public static void setOnLogListener(@Nullable OnLogListener onLogListener) {
logListener = onLogListener;
}
/**
* Get the currently registered custom log handler
- *
- * @return Current custom log handler or NULL if none is present
+ *
+ * @return Current custom log handler or NULL if none is present
*/
+ @Nullable
public static OnLogListener getOnLogListener() {
return logListener;
}
@@ -197,10 +218,10 @@ public class Debug {
/**
* 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.
- *
+ *
* @param enable Enable or disable
*/
public static void setSanityChecksEnabled(boolean enable) {
@@ -209,10 +230,10 @@ public class Debug {
/**
* Are sanity checks enabled ?
- *
+ *
* Note that debug mode must also be enabled for actual
- * sanity checks to occur.
- *
+ * sanity checks to occur.
+ *
* @return True if enabled
*/
public static boolean getSanityChecksEnabled() {
@@ -221,9 +242,9 @@ public class Debug {
/**
* 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 run(String shell, String[] commands, boolean wantSTDERR) {
+ @WorkerThread
+ public static List run(@NonNull String shell, @NonNull String[] commands, boolean wantSTDERR) {
return run(shell, commands, null, wantSTDERR);
}
@@ -89,16 +165,22 @@ public class Shell {
* 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 ?
+ * @param environment List of all environment variables (in 'key=value' format) or null for defaults
+ * @param wantSTDERR Return STDERR in the output ?
* @return Output of the commands, or null in case of an error
*/
- public static List run(String shell, String[] commands, String[] environment,
- boolean wantSTDERR) {
+ @Nullable
+ @Deprecated
+ @WorkerThread
+ public static List run(@NonNull String shell, @NonNull String[] commands, @Nullable String[] environment,
+ boolean wantSTDERR) {
String shellUpper = shell.toUpperCase(Locale.ENGLISH);
if (Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) {
@@ -109,15 +191,20 @@ public class Shell {
Debug.log(ShellOnMainThreadException.EXCEPTION_COMMAND);
throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_COMMAND);
}
- Debug.logCommand(String.format("[%s%%] START", shellUpper));
+
+ if (redirectDeprecated) {
+ // use our Threaded pool implementation instead
+ return Pool.getWrapper(shell).run(commands, environment, wantSTDERR);
+ }
+
+ Debug.logCommand(String.format(Locale.ENGLISH, "[%s%%] START", shellUpper));
List res = Collections.synchronizedList(new ArrayList());
try {
// Combine passed environment with system environment
if (environment != null) {
- Map newEnvironment = new HashMap();
- newEnvironment.putAll(System.getenv());
+ Map newEnvironment = new HashMap(System.getenv());
int split;
for (String entry : environment) {
if ((split = entry.indexOf("=")) >= 0) {
@@ -146,18 +233,18 @@ public class Shell {
STDERR.start();
try {
for (String write : commands) {
- Debug.logCommand(String.format("[%s+] %s", shellUpper, write));
+ Debug.logCommand(String.format(Locale.ENGLISH, "[%s+] %s", shellUpper, write));
STDIN.write((write + "\n").getBytes("UTF-8"));
STDIN.flush();
}
STDIN.write("exit\n".getBytes("UTF-8"));
STDIN.flush();
} catch (IOException e) {
- if (e.getMessage().contains("EPIPE")) {
- // method most horrid to catch broken pipe, in which case we
- // do nothing. the command is not a shell, the shell closed
+ if (e.getMessage().contains("EPIPE") || e.getMessage().contains("Stream closed")) {
+ // Method most horrid to catch broken pipe, in which case we
+ // do nothing. The command is not a shell, the shell closed
// STDIN, the script already contained the exit command, etc.
- // these cases we want the output instead of returning null
+ // these cases we want the output instead of returning null.
} else {
// other issues we don't know how to handle, leads to
// returning null
@@ -195,24 +282,23 @@ public class Shell {
res = null;
}
- Debug.logCommand(String.format("[%s%%] END", shell.toUpperCase(Locale.ENGLISH)));
+ Debug.logCommand(String.format(Locale.ENGLISH, "[%s%%] END", shell.toUpperCase(Locale.ENGLISH)));
return res;
}
- protected static String[] availableTestCommands = new String[] {
+ protected static final String[] availableTestCommands = new String[]{
"echo -BOC-",
"id"
};
/**
* See if the shell is alive, and if so, check the UID
- *
+ *
* @param ret Standard output from running availableTestCommands
- * @param checkForRoot true if we are expecting this shell to be running as
- * root
+ * @param checkForRoot true if we are expecting this shell to be running as root
* @return true on success, false on error
*/
- protected static boolean parseAvailableResult(List ret, boolean checkForRoot) {
+ protected static boolean parseAvailableResult(@Nullable List ret, boolean checkForRoot) {
if (ret == null)
return false;
@@ -225,10 +311,8 @@ public class Shell {
return !checkForRoot || line.contains("uid=0");
} else if (line.contains("-BOC-")) {
// if we end up here, at least the su command starts some kind
- // of shell,
- // let's hope it has root privileges - no way to know without
- // additional
- // native binaries
+ // of shell, let's hope it has root privileges - no way to know without
+ // additional native binaries
echo_seen = true;
}
}
@@ -242,33 +326,48 @@ public class Shell {
public static class SH {
/**
* Runs command and return output
- *
+ *
+ * @deprecated Consider using Shell.Pool.SH.run() instead
+ *
* @param command The command to run
* @return Output of the command, or null in case of an error
*/
- public static List run(String command) {
- return Shell.run("sh", new String[] {
+ @Nullable
+ @Deprecated
+ @WorkerThread
+ public static List run(@NonNull String command) {
+ return Shell.run("sh", new String[]{
command
}, null, false);
}
/**
* Runs commands and return output
- *
+ *
+ * @deprecated Consider using Shell.Pool.SH.run() instead
+ *
* @param commands The commands to run
* @return Output of the commands, or null in case of an error
*/
- public static List run(List commands) {
- return Shell.run("sh", commands.toArray(new String[commands.size()]), null, false);
+ @Nullable
+ @Deprecated
+ @WorkerThread
+ public static List run(@NonNull List commands) {
+ return Shell.run("sh", commands.toArray(new String[0]), null, false);
}
/**
* Runs commands and return output
- *
+ *
+ * @deprecated Consider using Shell.Pool.SH.run() instead
+ *
* @param commands The commands to run
* @return Output of the commands, or null in case of an error
*/
- public static List run(String[] commands) {
+ @Nullable
+ @Deprecated
+ @WorkerThread
+ public static List run(@NonNull String[] commands) {
return Shell.run("sh", commands, null, false);
}
}
@@ -279,43 +378,60 @@ public class Shell {
* if so which version.
*/
public static class SU {
+ @Nullable
private static Boolean isSELinuxEnforcing = null;
- private static String[] suVersion = new String[] {
+ @NonNull
+ private static String[] suVersion = new String[]{
null, null
};
/**
* Runs command as root (if available) and return output
- *
+ *
+ * @deprecated Consider using Shell.Pool.SU.run() instead
+ *
* @param command The command to run
* @return Output of the command, or null if root isn't available or in
- * case of an error
+ * case of an error
*/
- public static List run(String command) {
- return Shell.run("su", new String[] {
+ @Nullable
+ @Deprecated
+ @WorkerThread
+ public static List run(@NonNull String command) {
+ return Shell.run("su", new String[]{
command
}, null, false);
}
/**
* Runs commands as root (if available) and return output
- *
+ *
+ * @deprecated Consider using Shell.Pool.SU.run() instead
+ *
* @param commands The commands to run
* @return Output of the commands, or null if root isn't available or in
- * case of an error
+ * case of an error
*/
- public static List run(List commands) {
- return Shell.run("su", commands.toArray(new String[commands.size()]), null, false);
+ @Nullable
+ @Deprecated
+ @WorkerThread
+ public static List run(@NonNull List commands) {
+ return Shell.run("su", commands.toArray(new String[0]), null, false);
}
/**
* Runs commands as root (if available) and return output
- *
+ *
+ * @deprecated Consider using Shell.Pool.SU.run() instead
+ *
* @param commands The commands to run
* @return Output of the commands, or null if root isn't available or in
- * case of an error
+ * case of an error
*/
- public static List run(String[] commands) {
+ @Nullable
+ @Deprecated
+ @WorkerThread
+ public static List run(@NonNull String[] commands) {
return Shell.run("su", commands, null, false);
}
@@ -323,9 +439,10 @@ public class Shell {
* Detects whether or not superuser access is available, by checking the
* output of the "id" command if available, checking if a shell runs at
* all otherwise
- *
+ *
* @return True if superuser access available
*/
+ @WorkerThread
public static boolean available() {
// this is only one of many ways this can be done
@@ -349,22 +466,42 @@ public class Shell {
* This function caches its result to improve performance on multiple
* calls
*
- *
- * @param internal Request human-readable version or application
- * internal version
+ *
+ * @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 ret = Shell.run(
- internal ? "su -V" : "su -v",
- new String[] { "exit" },
- null,
- false
+ List ret;
+ if (!redirectDeprecated) {
+ ret = Shell.run(
+ internal ? "su -V" : "su -v",
+ new String[] { "exit" },
+ null,
+ false
+ );
+ } else {
+ ret = new ArrayList();
+ try {
+ ret = new ArrayList();
+ Shell.Pool.SH.run(
+ new String[] {
+ internal ? "su -V" : "su -v",
+ "exit"
+ },
+ ret,
+ null,
+ false
);
+ } catch (ShellDiedException e) {
+ // no action
+ }
+ }
if (ret != null) {
for (String line : ret) {
@@ -393,10 +530,11 @@ public class Shell {
/**
* Attempts to deduce if the shell command refers to a su shell
- *
+ *
* @param shell Shell command to run
* @return Shell command appears to be su
*/
+ @AnyThread
public static boolean isSU(String shell) {
// Strip parameters
int pos = shell.indexOf(' ');
@@ -410,19 +548,21 @@ public class Shell {
shell = shell.substring(pos + 1);
}
- return shell.equals("su");
+ return shell.toLowerCase(Locale.ENGLISH).equals("su");
}
/**
* Constructs a shell command to start a su shell using the supplied uid
* and SELinux context. This is can be an expensive operation, consider
* caching the result.
- *
+ *
* @param uid Uid to use (0 == root)
* @param context (SELinux) context name to use or null
* @return Shell command
*/
- public static String shell(int uid, String context) {
+ @NonNull
+ @WorkerThread
+ public static String shell(int uid, @Nullable String context) {
// su[ --context ][ ]
String shell = "su";
@@ -431,6 +571,7 @@ public class Shell {
String internal = version(true);
// We only know the format for SuperSU v1.90+ right now
+ //TODO add detection for other su's that support this
if ((display != null) &&
(internal != null) &&
(display.endsWith("SUPERSU")) &&
@@ -452,9 +593,11 @@ public class Shell {
* Constructs a shell command to start a su shell connected to mount
* master daemon, to perform public mounts on Android 4.3+ (or 4.2+ in
* SELinux enforcing mode)
- *
+ *
* @return Shell command
*/
+ @NonNull
+ @AnyThread
public static String shellMountMaster() {
if (android.os.Build.VERSION.SDK_INT >= 17) {
return "su --mount-master";
@@ -464,10 +607,11 @@ public class Shell {
/**
* Detect if SELinux is set to enforcing, caches result
- *
- * @return true if SELinux set to enforcing, or false in the case of
- * permissive or not present
+ *
+ * @return true if SELinux set to enforcing, or false in the case of permissive or not present
*/
+ @SuppressLint("PrivateApi")
+ @WorkerThread
public static synchronized boolean isSELinuxEnforcing() {
if (isSELinuxEnforcing == null) {
Boolean enforcing = null;
@@ -475,24 +619,41 @@ public class Shell {
// First known firmware with SELinux built-in was a 4.2 (17)
// leak
if (android.os.Build.VERSION.SDK_INT >= 17) {
+ if (android.os.Build.VERSION.SDK_INT >= 28) {
+ // Due to non-SDK API greylisting, we cannot determine SELinux status
+ // through the methods below, so we assume SELinux is enforcing and
+ // potentially patch policies for nothing
+ enforcing = true;
+ }
+
// Detect enforcing through sysfs, not always present
- File f = new File("/sys/fs/selinux/enforce");
- if (f.exists()) {
- try {
- InputStream is = new FileInputStream("/sys/fs/selinux/enforce");
+ if (enforcing == null) {
+ File f = new File("/sys/fs/selinux/enforce");
+ if (f.exists()) {
try {
- enforcing = (is.read() == '1');
- } finally {
- is.close();
+ InputStream is = new FileInputStream("/sys/fs/selinux/enforce");
+ try {
+ enforcing = (is.read() == '1');
+ } finally {
+ is.close();
+ }
+ } catch (Exception e) {
+ // we might not be allowed to read, thanks SELinux
}
- } catch (Exception e) {
- // we might not be allowed to read, thanks SELinux
}
}
- // 4.4+ builds are enforcing by default, take the gamble
+ // 4.4+ has a new API to detect SELinux mode, so use it
+ // SELinux is typically in enforced mode, but emulators may have SELinux disabled
if (enforcing == null) {
- enforcing = (android.os.Build.VERSION.SDK_INT >= 19);
+ try {
+ Class> seLinux = Class.forName("android.os.SELinux");
+ Method isSELinuxEnforced = seLinux.getMethod("isSELinuxEnforced");
+ enforcing = (Boolean) isSELinuxEnforced.invoke(seLinux.newInstance());
+ } catch (Exception e) {
+ // 4.4+ release builds are enforcing by default, take the gamble
+ enforcing = (android.os.Build.VERSION.SDK_INT >= 19);
+ }
}
}
@@ -516,6 +677,7 @@ public class Shell {
* not impossible.
*
*/
+ @AnyThread
public static synchronized void clearCachedResults() {
isSELinuxEnforcing = null;
suVersion[0] = null;
@@ -523,8 +685,11 @@ public class Shell {
}
}
- private interface OnResult {
- // for any onCommandResult callback
+ /**
+ * DO NOT USE DIRECTLY. Base interface for result callbacks.
+ */
+ public interface OnResult {
+ // for any callback
int WATCHDOG_EXIT = -1;
int SHELL_DIED = -2;
@@ -534,14 +699,30 @@ public class Shell {
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
+ * 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 output);
+ void onCommandResult(int commandCode, int exitCode, @NonNull List output);
}
/**
- * Command per line callback for parsing the output line by line without
- * buffering It also notifies the recipient of the completion of a command
- * block, including the (last) exit code.
+ * Command result callback, notifies the recipient of the completion of a
+ * command block, including the (last) exit code, and the full output
*/
- public interface OnCommandLineListener extends OnResult, OnLineListener
- {
+ public interface OnCommandResultListener2 extends OnResult {
+ /**
+ *
+ * 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 STDOUT, @NonNull List STDERR);
+ }
+
+ /**
+ * DO NOT USE DIRECTLY. Command result callback that doesn't cause output to be buffered
+ */
+ private interface OnCommandResultListenerUnbuffered extends OnResult {
/**
*
* 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)commands).toArray(new String[0]);
+ } else if (commands instanceof String[]) {
+ this.commands = (String[])commands;
+ } else {
+ throw new IllegalArgumentException("commands parameter must be of type String, List or String[]");
+ }
this.code = code;
- this.onCommandResultListener = onCommandResultListener;
- this.onCommandLineListener = onCommandLineListener;
- this.marker = UUID.randomUUID().toString() + String.format("-%08x", ++commandCounter);
+ this.marker = UUID.randomUUID().toString() + String.format(Locale.ENGLISH, "-%08x", ++commandCounter);
+
+ OnCommandResultListener commandResultListener = null;
+ OnCommandResultListener2 commandResultListener2 = null;
+ OnCommandLineListener commandLineListener = null;
+ OnCommandInputStreamListener commandInputStreamListener = null;
+ if (listener != null) {
+ if (listener instanceof OnCommandInputStreamListener) {
+ commandInputStreamListener = (OnCommandInputStreamListener)listener;
+ } else if (listener instanceof OnCommandLineListener) {
+ commandLineListener = (OnCommandLineListener)listener;
+ } else if (listener instanceof OnCommandResultListener2) {
+ commandResultListener2 = (OnCommandResultListener2)listener;
+ } else if (listener instanceof OnCommandResultListener) {
+ commandResultListener = (OnCommandResultListener)listener;
+ } else {
+ throw new IllegalArgumentException("OnResult is not a supported callback interface");
+ }
+ }
+ this.onCommandResultListener = commandResultListener;
+ this.onCommandResultListener2 = commandResultListener2;
+ this.onCommandLineListener = commandLineListener;
+ this.onCommandInputStreamListener = commandInputStreamListener;
}
}
/**
* Builder class for {@link Interactive}
*/
+ @AnyThread
public static class Builder {
+ @Nullable
private Handler handler = null;
private boolean autoHandler = true;
private String shell = "sh";
private boolean wantSTDERR = false;
+ private boolean shellDiesOnSTDOUTERRClose = true;
+ private boolean detectOpen = true;
+ @NonNull
private List commands = new LinkedList();
+ @NonNull
private Map environment = new HashMap();
+ @Nullable
private OnLineListener onSTDOUTLineListener = null;
+ @Nullable
private OnLineListener onSTDERRLineListener = null;
private int watchdogTimeout = 0;
@@ -634,11 +992,12 @@ public class Shell {
* See {@link Interactive} for further details on threading and
* handlers
*
- *
+ *
* @param handler Handler to use
* @return This Builder object for method chaining
*/
- public Builder setHandler(Handler handler) {
+ @NonNull
+ public Builder setHandler(@Nullable Handler handler) {
this.handler = handler;
return this;
}
@@ -651,10 +1010,11 @@ public class Shell {
* 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;
@@ -663,39 +1023,105 @@ public class Shell {
/**
* Set shell binary to use. Usually "sh" or "su", do not use a full path
* unless you have a good reason to
- *
+ *
* @param shell Shell to use
* @return This Builder object for method chaining
*/
- public Builder setShell(String shell) {
+ @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");
}
/**
- * Set if error output should be appended to command block result output
- *
+ *
+ * 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 addEnvironment) {
+ @NonNull
+ public Builder addEnvironment(@NonNull Map addEnvironment) {
environment.putAll(addEnvironment);
return this;
}
/**
- * Add a command to execute
- *
- * @param command Command to execute
+ * Add commands to execute, without a callback
+ *
+ * @param commands Commands to execute, accepts String, List<String>, and String[]
* @return This Builder object for method chaining
*/
- public Builder addCommand(String command) {
- return addCommand(command, 0, null);
- }
-
- /**
- *
- * Add a command to execute, with a callback to be called on completion
- *
- *
- * The thread on which the callback executes is dependent on various
- * factors, see {@link Interactive} for further details
- *
- *
- * @param command Command to execute
- * @param code User-defined value passed back to the callback
- * @param onCommandResultListener Callback to be called on completion
- * @return This Builder object for method chaining
- */
- public Builder addCommand(String command, int code,
- OnCommandResultListener onCommandResultListener) {
- return addCommand(new String[] {
- command
- }, code, onCommandResultListener);
- }
-
- /**
- * Add commands to execute
- *
- * @param commands Commands to execute
- * @return This Builder object for method chaining
- */
- public Builder addCommand(List commands) {
+ @NonNull
+ public Builder addCommand(@NonNull Object commands) {
return addCommand(commands, 0, null);
}
/**
*
- * 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 commands, int code,
- OnCommandResultListener onCommandResultListener) {
- return addCommand(commands.toArray(new String[commands.size()]), code,
- onCommandResultListener);
- }
-
- /**
- * Add commands to execute
- *
- * @param commands Commands to execute
- * @return This Builder object for method chaining
- */
- public Builder addCommand(String[] commands) {
- return addCommand(commands, 0, null);
- }
-
- /**
- *
- * Add commands to execute, with a callback to be called on completion
- * (of all commands)
- *
- *
- * The thread on which the callback executes is dependent on various
- * factors, see {@link 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(@NonNull Object commands, boolean wantSTDERR);
+
+ /**
+ * Run commands, with a set environment, 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 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
+ List run(@NonNull Object commands, @Nullable String[] environment, boolean wantSTDERR);
+ }
+
+ /**
+ * Base interface for objects that support synchronous commands
+ */
+ @WorkerThread
+ public interface SyncCommands {
+ /**
+ * Run commands and return exit code
+ *
+ * @param commands Commands to execute, accepts String, List<String>, and String[]
+ * @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) throws ShellDiedException;
+
+ /**
+ *
+ * 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 STDOUT, @Nullable List STDERR, boolean clear) throws ShellDiedException;
+
+ /**
+ *
+ * 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.
*
* Callbacks, threads and handlers
*
@@ -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 commands;
+ @NonNull
private final Map environment;
+ @Nullable
private final OnLineListener onSTDOUTLineListener;
+ @Nullable
private final OnLineListener onSTDERRLineListener;
private int watchdogTimeout;
+ @Nullable
private Process process = null;
+ @Nullable
private DataOutputStream STDIN = null;
+ @Nullable
private StreamGobbler STDOUT = null;
+ @Nullable
private StreamGobbler STDERR = null;
+ private final Object STDclosedSync = new Object();
+ private boolean STDOUTclosed = false;
+ private boolean STDERRclosed = false;
+ @Nullable
private ScheduledThreadPoolExecutor watchdog = null;
private volatile boolean running = false;
+ private volatile boolean lastOpening = false;
+ private volatile boolean opening = false;
private volatile boolean idle = true; // read/write only synchronized
- private volatile boolean closed = true;
- private volatile int callbacks = 0;
+ protected volatile boolean closed = true;
+ protected volatile int callbacks = 0;
private volatile int watchdogCount;
+ private volatile boolean doCloseWhenIdle = false;
+ protected volatile boolean inClosingJoin = false;
private final Object idleSync = new Object();
- private final Object callbackSync = new Object();
+ protected final Object callbackSync = new Object();
+ private final Object openingSync = new Object();
+ private final List emptyStringList = new ArrayList();
private volatile int lastExitCode = 0;
+ @Nullable
private volatile String lastMarkerSTDOUT = null;
+ @Nullable
private volatile String lastMarkerSTDERR = null;
+ @Nullable
private volatile Command command = null;
- private volatile List buffer = null;
+ @Nullable
+ private volatile List bufferSTDOUT = null;
+ @Nullable
+ private volatile List bufferSTDERR = null;
/**
- * The only way to create an instance: Shell.Builder::open()
- *
+ * The only way to create an instance: Shell.Builder::open(...)
+ *
+ * @see Shell.Builder#open()
+ * @see Shell.Builder#open(Shell.OnShellOpenResultListener)
+ *
* @param builder Builder class to take values from
+ * @param onShellOpenResultListener Callback
*/
- private Interactive(final Builder builder,
- final OnCommandResultListener onCommandResultListener) {
+ @AnyThread
+ protected Interactive(@NonNull final Builder builder,
+ @Nullable final OnShellOpenResultListener onShellOpenResultListener) {
autoHandler = builder.autoHandler;
shell = builder.shell;
+ shellDiesOnSTDOUTERRClose = builder.shellDiesOnSTDOUTERRClose;
wantSTDERR = builder.wantSTDERR;
commands = builder.commands;
environment = builder.environment;
@@ -1022,36 +1603,82 @@ public class Shell {
onSTDERRLineListener = builder.onSTDERRLineListener;
watchdogTimeout = builder.watchdogTimeout;
- // If a looper is available, we offload the callbacks from the
- // gobbling threads
- // to whichever thread created us. Would normally do this in open(),
- // but then we could not declare handler as final
+ // If a looper is available, we offload the callbacks from the gobbling threads to
+ // whichever thread created us. Would normally do this in open(), but then we could
+ // not declare handler as final
if ((Looper.myLooper() != null) && (builder.handler == null) && autoHandler) {
handler = new Handler();
} else {
handler = builder.handler;
}
- if (onCommandResultListener != null) {
+ if ((onShellOpenResultListener != null) || builder.detectOpen) {
+ lastOpening = true;
+ opening = true;
+
// Allow up to 60 seconds for SuperSU/Superuser dialog, then enable
// the user-specified timeout for all subsequent operations
watchdogTimeout = 60;
- commands.add(0, new Command(Shell.availableTestCommands, 0, new OnCommandResultListener() {
- public void onCommandResult(int commandCode, int exitCode, List output) {
- if ((exitCode == OnCommandResultListener.SHELL_RUNNING) &&
- !Shell.parseAvailableResult(output, SU.isSU(shell))) {
+ commands.add(0, new Command(Shell.availableTestCommands, 0, new OnCommandResultListener2() {
+ @Override
+ public void onCommandResult(int commandCode, int exitCode, @NonNull List STDOUT, @NonNull List STDERR) {
+ // we don't set opening to false here because idle must be set to true first
+ // to prevent falling through if 'isOpening() || isIdle()' is called
+
+ // this always runs in one of the gobbler threads, hard-coded
+ if ((exitCode == OnCommandResultListener2.SHELL_RUNNING) &&
+ !Shell.parseAvailableResult(STDOUT, Shell.SU.isSU(shell))) {
// shell is up, but it's brain-damaged
- exitCode = OnCommandResultListener.SHELL_WRONG_UID;
+ exitCode = OnCommandResultListener2.SHELL_WRONG_UID;
+
+ // we're otherwise technically not idle in this callback, deadlock
+ // we're inside runNextCommand so we needn't bother with idleSync
+ idle = true;
+ closeImmediately(); // triggers SHELL_DIED on remaining commands
}
+
+ // reset watchdog to user value
watchdogTimeout = builder.watchdogTimeout;
- onCommandResultListener.onCommandResult(0, exitCode, output);
+
+ // callback
+ if (onShellOpenResultListener != null) {
+ if (handler != null) {
+ final int fExitCode = exitCode;
+ startCallback();
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ onShellOpenResultListener.onOpenResult(fExitCode == OnShellOpenResultListener.SHELL_RUNNING, fExitCode);
+ } finally {
+ endCallback();
+ }
+ }
+ });
+ } else {
+ onShellOpenResultListener.onOpenResult(exitCode == OnShellOpenResultListener.SHELL_RUNNING, exitCode);
+ }
+ }
}
- }, null));
+ }));
}
- if (!open() && (onCommandResultListener != null)) {
- onCommandResultListener.onCommandResult(0,
- OnCommandResultListener.SHELL_EXEC_FAILED, null);
+ if (!open() && (onShellOpenResultListener != null)) {
+ if (handler != null) {
+ startCallback();
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ onShellOpenResultListener.onOpenResult(false, OnShellOpenResultListener.SHELL_EXEC_FAILED);
+ } finally {
+ endCallback();
+ }
+ }
+ });
+ } else {
+ onShellOpenResultListener.onOpenResult(false, OnShellOpenResultListener.SHELL_EXEC_FAILED);
+ }
}
}
@@ -1066,152 +1693,28 @@ public class Shell {
}
/**
- * Add a command to execute
- *
- * @param command Command to execute
+ * Add commands to execute, without a callback
+ *
+ * @param commands Commands to execute, accepts String, List<String>, and String[]
*/
- public void addCommand(String command) {
- addCommand(command, 0, (OnCommandResultListener) null);
+ @AnyThread
+ public synchronized void addCommand(@NonNull Object commands) {
+ addCommand(commands, 0, null);
}
/**
- *
- * Add a command to execute, with a callback to be called on completion
- *
- *
- * The thread on which the callback executes is dependent on various
- * factors, see {@link 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 commands) {
- addCommand(commands, 0, (OnCommandResultListener) null);
- }
-
- /**
- *
- * Add commands to execute, with a callback to be called on completion
- * (of all commands)
- *
- *
- * The thread on which the callback executes is dependent on various
- * factors, see {@link Interactive} for further details
- *
- *
- * @param commands Commands to execute
- * @param code User-defined value passed back to the callback
- * @param onCommandResultListener Callback to be called on completion
- * (of all commands)
- */
- public void addCommand(List commands, int code,
- OnCommandResultListener onCommandResultListener) {
- addCommand(commands.toArray(new String[commands.size()]), code, onCommandResultListener);
- }
-
- /**
- *
- * Add commands to execute, 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 commands, int code,
- OnCommandLineListener onCommandLineListener) {
- addCommand(commands.toArray(new String[commands.size()]), code, onCommandLineListener);
- }
-
- /**
- * Add commands to execute
- *
- * @param commands Commands to execute
- */
- public void addCommand(String[] commands) {
- addCommand(commands, 0, (OnCommandResultListener) null);
- }
-
- /**
- *
- * 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());
- }
+ // STDIN and STDOUT would never be null here, but checks added to satisfy lint
+ if ((STDIN != null) && (STDOUT != null)) {
+ try {
+ if (command.onCommandResultListener != null) {
+ bufferSTDOUT = Collections.synchronizedList(new ArrayList());
+ } else if (command.onCommandResultListener2 != null) {
+ bufferSTDOUT = Collections.synchronizedList(new ArrayList());
+ bufferSTDERR = Collections.synchronizedList(new ArrayList());
+ }
- idle = false;
- this.command = command;
- startWatchdog();
- for (String write : command.commands) {
- Debug.logCommand(String.format("[%s+] %s",
- shell.toUpperCase(Locale.ENGLISH), write));
- STDIN.write((write + "\n").getBytes("UTF-8"));
+ idle = false;
+ this.command = command;
+ if (command.onCommandInputStreamListener != null) {
+ if (!STDOUT.isSuspended()) {
+ if (Thread.currentThread().getId() == STDOUT.getId()) {
+ // if we're on the Gobbler thread we can suspend immediately,
+ // as we're not currently in a readLine() call
+ STDOUT.suspendGobbling();
+ } else {
+ // if not, we trigger the readLine() call in the Gobbler to
+ // complete, and have the suspend triggered in the next
+ // onLine call
+ STDIN.write(("echo inputstream\n").getBytes("UTF-8"));
+ STDIN.flush();
+ STDOUT.waitForSuspend();
+ }
+ }
+ } else {
+ STDOUT.resumeGobbling();
+ startWatchdog();
+ }
+ for (String write : command.commands) {
+ Debug.logCommand(String.format(Locale.ENGLISH, "[%s+] %s",
+ shell.toUpperCase(Locale.ENGLISH), write));
+ STDIN.write((write + "\n").getBytes("UTF-8"));
+ }
+ STDIN.write(("echo " + command.marker + " $?\n").getBytes("UTF-8"));
+ STDIN.write(("echo " + command.marker + " >&2\n").getBytes("UTF-8"));
+ STDIN.flush();
+ if (command.onCommandInputStreamListener != null) {
+ command.markerInputStream = new MarkerInputStream(STDOUT, command.marker);
+ postCallback(command, 0, null, null, command.markerInputStream);
+ }
+ } catch (IOException e) {
+ // STDIN might have closed
}
- STDIN.write(("echo " + command.marker + " $?\n").getBytes("UTF-8"));
- STDIN.write(("echo " + command.marker + " >&2\n").getBytes("UTF-8"));
- STDIN.flush();
- } catch (IOException e) {
- // STDIN might have closed
}
} else {
runNextCommand(false);
}
- } else if (!running) {
- // our shell died for unknown reasons - abort all submissions
+ } else if (!running || closed) {
+ // our shell died for unknown reasons or was closed - abort all submissions
+ Debug.log(String.format(Locale.ENGLISH, "[%s%%] SHELL_DIED", shell.toUpperCase(Locale.ENGLISH)));
while (commands.size() > 0) {
- postCallback(commands.remove(0), OnCommandResultListener.SHELL_DIED, null);
+ postCallback(commands.remove(0), OnResult.SHELL_DIED, null, null, null);
+ }
+ onClosed();
+ }
+
+ if (idle) {
+ if (running && doCloseWhenIdle) {
+ doCloseWhenIdle = false;
+ closeImmediately(true);
+ }
+ if (notifyIdle) {
+ synchronized (idleSync) {
+ idleSync.notifyAll();
+ }
}
}
- if (idle && notifyIdle) {
- synchronized (idleSync) {
- idleSync.notifyAll();
+ if (lastOpening && !opening) {
+ lastOpening = opening;
+ synchronized (openingSync) {
+ openingSync.notifyAll();
}
}
}
@@ -1351,62 +1899,82 @@ public class Shell {
/**
* Processes a STDOUT/STDERR line containing an end/exitCode marker
*/
+ @SuppressWarnings("ConstantConditions") // all writes to 'command' are synchronized
private synchronized void processMarker() {
- if (command.marker.equals(lastMarkerSTDOUT)
- && (command.marker.equals(lastMarkerSTDERR))) {
- postCallback(command, lastExitCode, buffer);
+ if ((command != null) &&
+ command.marker.equals(lastMarkerSTDOUT) &&
+ command.marker.equals(lastMarkerSTDERR)) {
+ postCallback(command, lastExitCode, bufferSTDOUT, bufferSTDERR, null);
stopWatchdog();
command = null;
- buffer = null;
+ bufferSTDOUT = null;
+ bufferSTDERR = null;
idle = true;
+ opening = false;
runNextCommand();
}
}
/**
- * Process a normal STDOUT/STDERR line
- *
+ * Process a normal STDOUT/STDERR line, post to callback
+ *
* @param line Line to process
- * @param listener Callback to call or null
+ * @param listener Callback to call or null, supports OnLineListener, OnCommandLineSTDOUT, OnCommandLineSTDERR
*/
- private synchronized void processLine(String line, OnLineListener listener) {
+ private synchronized void processLine(@NonNull final String line, @Nullable final Object listener, final boolean isSTDERR) {
if (listener != null) {
if (handler != null) {
- final String fLine = line;
- final OnLineListener fListener = listener;
-
startCallback();
handler.post(new Runnable() {
@Override
public void run() {
try {
- fListener.onLine(fLine);
+ if (listener instanceof OnLineListener) {
+ ((OnLineListener)listener).onLine(line);
+ } else if ((listener instanceof OnCommandLineSTDOUT) && !isSTDERR) {
+ ((OnCommandLineSTDOUT)listener).onSTDOUT(line);
+ } else if ((listener instanceof OnCommandLineSTDERR) && isSTDERR) {
+ ((OnCommandLineSTDERR)listener).onSTDERR(line);
+ }
} finally {
endCallback();
}
}
});
} else {
- listener.onLine(line);
+ if (listener instanceof OnLineListener) {
+ ((OnLineListener)listener).onLine(line);
+ } else if ((listener instanceof OnCommandLineSTDOUT) && !isSTDERR) {
+ ((OnCommandLineSTDOUT)listener).onSTDOUT(line);
+ } else if ((listener instanceof OnCommandLineSTDERR) && isSTDERR) {
+ ((OnCommandLineSTDERR)listener).onSTDERR(line);
+ }
}
}
}
/**
* Add line to internal buffer
- *
+ *
* @param line Line to add
*/
- private synchronized void addBuffer(String line) {
- if (buffer != null) {
- buffer.add(line);
+ @SuppressWarnings("ConstantConditions") // all writes to bufferSTDxxx are synchronized
+ private synchronized void addBuffer(@NonNull String line, boolean isSTDERR) {
+ if (isSTDERR) {
+ if (bufferSTDERR != null) {
+ bufferSTDERR.add(line);
+ } else if (wantSTDERR && (bufferSTDOUT != null)) {
+ bufferSTDOUT.add(line);
+ }
+ } else if (bufferSTDOUT != null) {
+ bufferSTDOUT.add(line);
}
}
/**
* Increase callback counter
*/
- private void startCallback() {
+ void startCallback() {
synchronized (callbackSync) {
callbacks++;
}
@@ -1414,43 +1982,68 @@ public class Shell {
/**
* Schedule a callback to run on the appropriate thread
+ *
+ * @return if callback has already completed
*/
- private void postCallback(final Command fCommand, final int fExitCode,
- final List fOutput) {
- if (fCommand.onCommandResultListener == null && fCommand.onCommandLineListener == null) {
- return;
+ private boolean postCallback(@NonNull final Command fCommand, final int fExitCode,
+ @Nullable final List fSTDOUT, @Nullable final List fSTDERR,
+ @Nullable final InputStream inputStream) {
+ if (
+ (fCommand.onCommandResultListener == null) &&
+ (fCommand.onCommandResultListener2 == null) &&
+ (fCommand.onCommandLineListener == null) &&
+ (fCommand.onCommandInputStreamListener == null)
+ ) {
+ return true;
}
- if (handler == null) {
- if ((fCommand.onCommandResultListener != null) && (fOutput != null))
- fCommand.onCommandResultListener.onCommandResult(fCommand.code, fExitCode,
- fOutput);
- if (fCommand.onCommandLineListener != null)
- fCommand.onCommandLineListener.onCommandResult(fCommand.code, fExitCode);
- return;
+
+ // we run the shell open test commands result immediately even if we have a handler, so
+ // it may close the shell before other commands start and pass them SHELL_DIED exit code
+ if ((handler == null) || (fCommand.commands == availableTestCommands)) {
+ if (inputStream == null) {
+ if (fCommand.onCommandResultListener != null)
+ fCommand.onCommandResultListener.onCommandResult(fCommand.code, fExitCode, fSTDOUT != null ? fSTDOUT : emptyStringList);
+ if (fCommand.onCommandResultListener2 != null)
+ fCommand.onCommandResultListener2.onCommandResult(fCommand.code, fExitCode, fSTDOUT != null ? fSTDOUT : emptyStringList, fSTDERR != null ? fSTDERR : emptyStringList);
+ if (fCommand.onCommandLineListener != null)
+ fCommand.onCommandLineListener.onCommandResult(fCommand.code, fExitCode);
+ if (fCommand.onCommandInputStreamListener != null)
+ fCommand.onCommandInputStreamListener.onCommandResult(fCommand.code, fExitCode);
+ } else if (fCommand.onCommandInputStreamListener != null) {
+ fCommand.onCommandInputStreamListener.onInputStream(inputStream);
+ }
+ return true;
}
startCallback();
handler.post(new Runnable() {
@Override
public void run() {
try {
- if ((fCommand.onCommandResultListener != null) && (fOutput != null))
- fCommand.onCommandResultListener.onCommandResult(fCommand.code,
- fExitCode, fOutput);
- if (fCommand.onCommandLineListener != null)
- fCommand.onCommandLineListener
- .onCommandResult(fCommand.code, fExitCode);
+ if (inputStream == null) {
+ if (fCommand.onCommandResultListener != null)
+ fCommand.onCommandResultListener.onCommandResult(fCommand.code, fExitCode, fSTDOUT != null ? fSTDOUT : emptyStringList);
+ if (fCommand.onCommandResultListener2 != null)
+ fCommand.onCommandResultListener2.onCommandResult(fCommand.code, fExitCode, fSTDOUT != null ? fSTDOUT : emptyStringList, fSTDERR != null ? fSTDERR : emptyStringList);
+ if (fCommand.onCommandLineListener != null)
+ fCommand.onCommandLineListener.onCommandResult(fCommand.code, fExitCode);
+ if (fCommand.onCommandInputStreamListener != null)
+ fCommand.onCommandInputStreamListener.onCommandResult(fCommand.code, fExitCode);
+ } else if (fCommand.onCommandInputStreamListener != null) {
+ fCommand.onCommandInputStreamListener.onInputStream(inputStream);
+ }
} finally {
endCallback();
}
}
});
+ return false;
}
/**
* Decrease callback counter, signals callback complete state when
* dropped to 0
*/
- private void endCallback() {
+ void endCallback() {
synchronized (callbackSync) {
callbacks--;
if (callbacks == 0) {
@@ -1462,11 +2055,11 @@ public class Shell {
/**
* Internal call that launches the shell, starts gobbling, and starts
* executing commands. See {@link Interactive}
- *
+ *
* @return Opened successfully ?
*/
private synchronized boolean open() {
- Debug.log(String.format("[%s%%] START", shell.toUpperCase(Locale.ENGLISH)));
+ Debug.log(String.format(Locale.ENGLISH, "[%s%%] START", shell.toUpperCase(Locale.ENGLISH)));
try {
// setup our process, retrieve STDIN stream, and STDOUT/STDERR
@@ -1486,52 +2079,135 @@ public class Shell {
process = Runtime.getRuntime().exec(shell, env);
}
+ // this should never actually happen
+ if (process == null) throw new NullPointerException();
+
+ OnStreamClosedListener onStreamClosedListener = new OnStreamClosedListener() {
+ @Override
+ public void onStreamClosed() {
+ if (shellDiesOnSTDOUTERRClose || !isRunning()) {
+ if ((STDERR != null) && (Thread.currentThread() == STDOUT)) STDERR.resumeGobbling();
+ if ((STDOUT != null) && (Thread.currentThread() == STDERR)) STDOUT.resumeGobbling();
+
+ boolean isLast;
+ synchronized (STDclosedSync){
+ if (Thread.currentThread() == STDOUT) STDOUTclosed = true;
+ if (Thread.currentThread() == STDERR) STDERRclosed = true;
+ isLast = STDOUTclosed && STDERRclosed;
+
+ Command c = command;
+ if (c != null) {
+ MarkerInputStream mis = c.markerInputStream;
+ if (mis != null) {
+ mis.setEOF();
+ }
+ }
+ }
+
+ if (isLast) { // make sure both are done
+ waitForCallbacks();
+
+ synchronized (Interactive.this) {
+ // our shell died for unknown reasons - abort all submissions
+ if (command != null) {
+ //noinspection ConstantConditions // all writes to 'command' are synchronized
+ postCallback(command, OnResult.SHELL_DIED, bufferSTDOUT, bufferSTDERR, null);
+ command = null;
+ }
+ closed = true;
+ opening = false;
+ runNextCommand();
+ }
+ }
+ }
+ }
+ };
+
STDIN = new DataOutputStream(process.getOutputStream());
STDOUT = new StreamGobbler(shell.toUpperCase(Locale.ENGLISH) + "-",
process.getInputStream(), new OnLineListener() {
- @Override
- public void onLine(String line) {
- synchronized (Interactive.this) {
- if (command == null) {
- return;
- }
- if (line.startsWith(command.marker)) {
- try {
- lastExitCode = Integer.valueOf(
- line.substring(command.marker.length() + 1), 10);
- } catch (Exception e) {
- // this really shouldn't happen
- e.printStackTrace();
- }
- lastMarkerSTDOUT = command.marker;
- processMarker();
- } else {
- addBuffer(line);
- processLine(line, onSTDOUTLineListener);
- processLine(line, command.onCommandLineListener);
- }
- }
+ @SuppressWarnings("ConstantConditions") // all writes to 'command' are synchronized
+ @Override
+ public void onLine(@NonNull String line) {
+ Command cmd = command;
+ if ((cmd != null) && (cmd.onCommandInputStreamListener != null)) {
+ // we need to suspend the normal input reader
+ if (line.equals("inputstream")) {
+ if (STDOUT != null) STDOUT.suspendGobbling();
+ return;
}
- });
+ }
+
+ synchronized (Interactive.this) {
+ if (command == null) {
+ return;
+ }
+
+ String contentPart = line;
+ String markerPart = null;
+
+ int markerIndex = line.indexOf(command.marker);
+ if (markerIndex == 0) {
+ contentPart = null;
+ markerPart = line;
+ } else if (markerIndex > 0) {
+ contentPart = line.substring(0, markerIndex);
+ markerPart = line.substring(markerIndex);
+ }
+
+ if (contentPart != null) {
+ addBuffer(contentPart, false);
+ processLine(contentPart, onSTDOUTLineListener, false);
+ processLine(contentPart, command.onCommandLineListener, false);
+ }
+
+ if (markerPart != null) {
+ try {
+ lastExitCode = Integer.valueOf(
+ markerPart.substring(command.marker.length() + 1), 10);
+ } catch (Exception e) {
+ // this really shouldn't happen
+ e.printStackTrace();
+ }
+ lastMarkerSTDOUT = command.marker;
+ processMarker();
+ }
+ }
+ }
+ }, onStreamClosedListener);
STDERR = new StreamGobbler(shell.toUpperCase(Locale.ENGLISH) + "*",
process.getErrorStream(), new OnLineListener() {
- @Override
- public void onLine(String line) {
- synchronized (Interactive.this) {
- if (command == null) {
- return;
- }
- if (line.startsWith(command.marker)) {
- lastMarkerSTDERR = command.marker;
- processMarker();
- } else {
- if (wantSTDERR)
- addBuffer(line);
- processLine(line, onSTDERRLineListener);
- }
- }
+ @SuppressWarnings("ConstantConditions") // all writes to 'command' are synchronized
+ @Override
+ public void onLine(@NonNull String line) {
+ synchronized (Interactive.this) {
+ if (command == null) {
+ return;
}
- });
+
+ String contentPart = line;
+
+ int markerIndex = line.indexOf(command.marker);
+ if (markerIndex == 0) {
+ contentPart = null;
+ } else if (markerIndex > 0) {
+ contentPart = line.substring(0, markerIndex);
+ }
+
+ if (contentPart != null) {
+ addBuffer(contentPart, true);
+ processLine(contentPart, onSTDERRLineListener, true);
+ processLine(contentPart, command.onCommandLineListener, true);
+ processLine(contentPart, command.onCommandInputStreamListener, true);
+ }
+
+ if (markerIndex >= 0) {
+ lastMarkerSTDERR = command.marker;
+ processMarker();
+ }
+ }
+ }
+ }, onStreamClosedListener);
// start gobbling and write our commands to the shell
STDOUT.start();
@@ -1549,6 +2225,25 @@ public class Shell {
}
}
+ /**
+ * Currently unused. May be called multiple times for each actual close.
+ */
+ protected void onClosed() {
+ // callbacks may still be scheduled/running at this point, and this may be called
+ // multiple times!
+ // if (inClosingJoin) return; // prevent deadlock, we will be called after
+ }
+
+ /**
+ * Currently redirects to {@link #closeImmediately()}. You should use
+ * {@link #closeImmediately()} or {@link #closeWhenIdle()} directly instead
+ */
+ // not annotated @deprecated because we do want to use this method in Threaded
+ @WorkerThread // if shell not idle
+ public void close() {
+ closeImmediately();
+ }
+
/**
* Close shell and clean up all resources. Call this when you are done
* with the shell. If the shell is not idle (all commands completed) you
@@ -1556,7 +2251,15 @@ public class Shell {
* block for a long time. This method will intentionally crash your app
* (if in debug mode) if you try to do this anyway.
*/
- public void close() {
+ @WorkerThread // if shell not idle
+ public void closeImmediately() {
+ closeImmediately(false);
+ }
+
+ protected void closeImmediately(boolean fromIdle) {
+ // these should never happen, satisfy lint
+ if ((STDIN == null) || (STDOUT == null) || (STDERR == null) || (process == null)) throw new NullPointerException();
+
boolean _idle = isIdle(); // idle must be checked synchronized
synchronized (this) {
@@ -1566,6 +2269,11 @@ public class Shell {
closed = true;
}
+ if (!isRunning()) {
+ onClosed();
+ return;
+ }
+
// This method should not be called from the main thread unless the
// shell is idle and can be cleaned up with (minimal) waiting. Only
// throw in debug mode.
@@ -1582,7 +2290,7 @@ public class Shell {
STDIN.write(("exit\n").getBytes("UTF-8"));
STDIN.flush();
} catch (IOException e) {
- if (e.getMessage().contains("EPIPE")) {
+ if (e.getMessage().contains("EPIPE") || e.getMessage().contains("Stream closed")) {
// we're not running a shell, the shell closed STDIN,
// the script already contained the exit command, etc.
} else {
@@ -1604,8 +2312,19 @@ public class Shell {
} catch (IOException e) {
// STDIN going missing is no reason to abort
}
- STDOUT.join();
- STDERR.join();
+
+ // Make sure our threads our running
+ if (Thread.currentThread() != STDOUT) STDOUT.resumeGobbling();
+ if (Thread.currentThread() != STDERR) STDERR.resumeGobbling();
+
+ // Otherwise we may deadlock waiting on eachother, happens when this is run from OnShellOpenResultListener
+ if ((Thread.currentThread() != STDOUT) && (Thread.currentThread() != STDERR)) {
+ inClosingJoin = true;
+ STDOUT.conditionalJoin();
+ STDERR.conditionalJoin();
+ inClosingJoin = false;
+ }
+
stopWatchdog();
process.destroy();
} catch (IOException e) {
@@ -1614,7 +2333,23 @@ public class Shell {
// this should really be re-thrown
}
- Debug.log(String.format("[%s%%] END", shell.toUpperCase(Locale.ENGLISH)));
+ Debug.log(String.format(Locale.ENGLISH, "[%s%%] END", shell.toUpperCase(Locale.ENGLISH)));
+
+ onClosed();
+ }
+
+ /**
+ * {@link #close()} the shell when it becomes idle. Note that in contrast to
+ * {@link #closeImmediately()}, this method does not block until the
+ * shell is closed!
+ */
+ @AnyThread
+ public void closeWhenIdle() {
+ if (idle) {
+ closeImmediately(true);
+ } else {
+ doCloseWhenIdle = true;
+ }
}
/**
@@ -1622,7 +2357,11 @@ public class Shell {
* wedged. Hopefully the StreamGobblers will croak on their own when the
* other side of the pipe is closed.
*/
+ @WorkerThread
public synchronized void kill() {
+ // these should never happen, satisfy lint
+ if ((STDIN == null) || (process == null)) throw new NullPointerException();
+
running = false;
closed = true;
@@ -1636,13 +2375,44 @@ public class Shell {
} catch (Exception e) {
// in case it was already destroyed or can't be
}
+
+ idle = true;
+ opening = false;
+ synchronized (idleSync) {
+ idleSync.notifyAll();
+ }
+ if (lastOpening && !opening) {
+ lastOpening = opening;
+ synchronized (openingSync) {
+ openingSync.notifyAll();
+ }
+ }
+
+ onClosed();
+ }
+
+ /**
+ *
+ * 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 STDOUT, @Nullable final List STDERR, boolean clear) throws ShellDiedException {
+ if (clear) {
+ if (STDOUT != null) STDOUT.clear();
+ if (STDERR != null) STDERR.clear();
+ }
+ final int[] exitCode = new int[1];
+ addCommand(commands, 0, new OnCommandResultListener2() {
+ @Override
+ public void onCommandResult(int commandCode, int intExitCode, @NonNull List intSTDOUT, @NonNull List intSTDERR) {
+ exitCode[0] = intExitCode;
+ if (STDOUT != null) STDOUT.addAll(intSTDOUT);
+ if (STDERR != null) STDERR.addAll(intSTDERR);
+ }
+ });
+ waitForIdle();
+ if (exitCode[0] < 0) throw new ShellDiedException();
+ return exitCode[0];
+ }
+
+ // documented in SyncCommands interface
+ @Override
+ @WorkerThread
+ public int run(@NonNull Object commands, @NonNull final OnSyncCommandLineListener onSyncCommandLineListener) throws ShellDiedException {
+ final int[] exitCode = new int[1];
+ addCommand(commands, 0, new OnCommandLineListener() {
+ @Override
+ public void onSTDERR(@NonNull String line) {
+ onSyncCommandLineListener.onSTDERR(line);
+ }
+
+ @Override
+ public void onSTDOUT(@NonNull String line) {
+ onSyncCommandLineListener.onSTDOUT(line);
+ }
+
+ @Override
+ public void onCommandResult(int commandCode, int intExitCode) {
+ exitCode[0] = intExitCode;
+ }
+ });
+ waitForIdle();
+ if (exitCode[0] < 0) throw new ShellDiedException();
+ return exitCode[0];
+ }
+
+ // documented in SyncCommands interface
+ @Override
+ @WorkerThread
+ public int run(@NonNull Object commands, @NonNull final OnSyncCommandInputStreamListener onSyncCommandInputStreamListener) throws ShellDiedException {
+ final int[] exitCode = new int[1];
+ addCommand(commands, 0, new OnCommandInputStreamListener() {
+ @Override
+ public void onSTDERR(@NonNull String line) {
+ onSyncCommandInputStreamListener.onSTDERR(line);
+ }
+
+ @Override
+ public void onInputStream(@NonNull InputStream inputStream) {
+ onSyncCommandInputStreamListener.onInputStream(inputStream);
+ }
+
+ @Override
+ public void onCommandResult(int commandCode, int intExitCode) {
+ exitCode[0] = intExitCode;
+ }
+ });
+ waitForIdle();
+ if (exitCode[0] < 0) throw new ShellDiedException();
+ return exitCode[0];
+ }
+ }
+
+ /**
+ *
+ * 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 run(@NonNull Object commands, final boolean wantSTDERR) {
+ try {
+ Threaded shell = get();
+ try {
+ final int[] exitCode = new int[1];
+ final List output = new ArrayList();
+ shell.addCommand(commands, 0, new OnCommandResultListener2() {
+ @Override
+ public void onCommandResult(int commandCode, int intExitCode, @NonNull List intSTDOUT, @NonNull List intSTDERR) {
+ exitCode[0] = intExitCode;
+ output.addAll(intSTDOUT);
+ if (wantSTDERR) {
+ output.addAll(intSTDERR);
+ }
+ }
+ });
+ shell.waitForIdle();
+ if (exitCode[0] < 0) return null;
+ return output;
+ } finally {
+ shell.close();
+ }
+ } catch (ShellDiedException e) {
+ return null;
+ }
+ }
+
+ // documented in DeprecatedSyncCommands interface
+ @Nullable
+ @SuppressWarnings("unchecked") // if the user passes in List<> of anything other than String, that's on them
+ @Deprecated
+ @WorkerThread
+ public List run(@NonNull Object commands, @Nullable String[] environment, boolean wantSTDERR) {
+ if (environment == null) {
+ return run(commands, wantSTDERR);
+ } else {
+ String[] _commands;
+ if (commands instanceof String) {
+ _commands = new String[] { (String)commands };
+ } else if (commands instanceof List>) {
+ _commands = ((List)commands).toArray(new String[0]);
+ } else if (commands instanceof String[]) {
+ _commands = (String[])commands;
+ } else {
+ throw new IllegalArgumentException("commands parameter must be of type String, List or String[]");
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (String entry : environment) {
+ int split;
+ if ((split = entry.indexOf("=")) >= 0) {
+ boolean quoted = entry.substring(split + 1, split + 2).equals("\"");
+ sb.append(entry, 0, split);
+ sb.append(quoted ? "=" : "=\"");
+ sb.append(entry.substring(split + 1));
+ sb.append(quoted ? " " : "\" ");
+ }
+ }
+ sb.append("sh -c \"\n");
+ for (String line : _commands) {
+ sb.append(line);
+ sb.append("\n");
+ }
+ sb.append("\"");
+ return run(new String[] { sb.toString().
+ replace("\\", "\\\\").
+ replace("$", "\\$")
+ }, wantSTDERR);
+ }
+ }
+
+ // 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 List STDOUT, @Nullable List STDERR, boolean clear) throws ShellDiedException {
+ Threaded shell = get();
+ try {
+ return shell.run(commands, STDOUT, STDERR, clear);
+ } finally {
+ shell.close();
+ }
+ }
+
+ // documented in SyncCommands interface
+ @Override
+ @WorkerThread
+ public int run(@NonNull Object commands, @NonNull OnSyncCommandLineListener onSyncCommandLineListener) throws ShellDiedException {
+ Threaded shell = get();
+ try {
+ return shell.run(commands, onSyncCommandLineListener);
+ } finally {
+ shell.close();
+ }
+ }
+
+ // documented in SyncCommands interface
+ @Override
+ @WorkerThread
+ public int run(@NonNull Object commands, @NonNull OnSyncCommandInputStreamListener onSyncCommandInputStreamListener) throws ShellDiedException {
+ Threaded shell = get();
+ try {
+ return shell.run(commands, onSyncCommandInputStreamListener);
+ } finally {
+ shell.close();
+ }
+ }
+ }
+
+ /**
+ *
+ * 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> pool = new HashMap>();
+ private static int poolSize = 4; // only applicable to su, we keep only 1 of others
+
+ /**
+ * Get the currently set {@link OnNewBuilderListener} callback. {@link #defaultOnNewBuilderListener}
+ * is used when null
+ *
+ * @return Current {@link OnNewBuilderListener} interface, or null for {@link #defaultOnNewBuilderListener}
+ */
+ @Nullable
+ @AnyThread
+ public static synchronized OnNewBuilderListener getOnNewBuilderListener() {
+ return onNewBuilderListener;
+ }
+
+ /**
+ * Set current {@link OnNewBuilderListener} callback
+ *
+ * @param onNewBuilderListener {@link OnNewBuilderListener} to use, or null to revert to {@link #defaultOnNewBuilderListener}
+ */
+ @AnyThread
+ public static synchronized void setOnNewBuilderListener(@Nullable OnNewBuilderListener onNewBuilderListener) {
+ Pool.onNewBuilderListener = onNewBuilderListener;
+ }
+
+ /**
+ *
+ * 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 shellsModify = pool.get(key);
+ if (shellsModify == null) continue;
+
+ @SuppressWarnings("unchecked")
+ ArrayList shellsCheck = (ArrayList)shellsModify.clone();
+ // we use this so we don't need to synchronize the entire method, but can still
+ // prevent issues by the list being modified asynchronously
+
+ int wantedTotal = Shell.SU.isSU(key) ? poolSize : 1;
+ int haveTotal = 0;
+ int haveAvailable = 0;
+
+ for (int i = shellsCheck.size() - 1; i >= 0; i--) {
+ Threaded threaded = shellsCheck.get(i);
+ if (!threaded.isRunning() || (threaded == toRemove) || removeAll) {
+ Debug.logPool("shell removed");
+ shellsCheck.remove(threaded);
+ synchronized (pool) {
+ shellsModify.remove(threaded);
+ }
+ if (removeAll) threaded.closeWhenIdle();
+ } else {
+ haveTotal += 1;
+ if (!threaded.isReserved()) {
+ haveAvailable++;
+ }
+ }
+ }
+
+ if ((haveTotal > wantedTotal) && (haveAvailable > 1)) {
+ int kill = Math.min(haveAvailable - 1, haveTotal - wantedTotal);
+ for (int i = shellsCheck.size() - 1; i >= 0; i--) {
+ Threaded threaded = shellsCheck.get(i);
+ if (!threaded.isReserved() && threaded.isIdle()) {
+ Debug.logPool("shell killed");
+ shellsCheck.remove(threaded);
+ synchronized (pool) {
+ shellsModify.remove(threaded);
+ }
+ // not calling closeImmediately() due to possible race
+ threaded.closeWhenIdle(true);
+ kill--;
+ if (kill == 0)
+ break;
+ }
+ }
+ }
+
+ synchronized (pool) {
+ if (shellsModify.size() == 0) {
+ pool.remove(key);
+ }
+ }
+ }
+
+ if (Debug.getDebug()) {
+ synchronized (pool) {
+ for (String key : pool.keySet()) {
+ int reserved = 0;
+ ArrayList shells = pool.get(key);
+ if (shells == null) continue; // never happens, satisfy lint
+ for (int i = 0; i < shells.size(); i++) {
+ if (shells.get(i).isReserved()) reserved++;
+ }
+ Debug.logPool(String.format(Locale.ENGLISH, "cleanup: shell:%s count:%d reserved:%d", key, shells.size(), reserved));
+ }
+ }
+ }
+ }
+
+ /**
+ *
+ * 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 shells = pool.get(shellUpper);
+ if (shells != null) {
+ for (Threaded instance : shells) {
+ if (!instance.isReserved()) {
+ threaded = instance;
+ threaded.setReserved(true);
+ break;
+ }
+ }
+ }
+ }
+
+ if (threaded == null) {
+ // create instance
+ threaded = newInstance(shell, onShellOpenResultListener, true);
+ if (!threaded.isRunning()) {
+ throw new ShellDiedException();
+ } else {
+ if (!(Debug.getSanityChecksEnabledEffective() && Debug.onMainThread())) {
+ if (!threaded.waitForOpened(null)) {
+ throw new ShellDiedException();
+ }
+ }
+ // otherwise failure will be in callbacks
+ }
+ synchronized (Pool.class) {
+ if (!threaded.wasPoolRemoveCalled()) {
+ if (pool.get(shellUpper) == null) {
+ pool.put(shellUpper, new ArrayList());
+ }
+ //noinspection ConstantConditions // pool.get(shellUpper) is never null here
+ pool.get(shellUpper).add(threaded);
+ }
+ }
+ } else {
+ // shell is already open, but if an OnShellOpenResultListener was passed, call it
+ if (onShellOpenResultListener != null) {
+ final Threaded fThreaded = threaded;
+ threaded.startCallback();
+ //noinspection ConstantConditions // handler is never null
+ threaded.handler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ onShellOpenResultListener.onOpenResult(true, OnShellOpenResultListener.SHELL_RUNNING);
+ } finally {
+ fThreaded.endCallback();
+ }
+ }
+ });
+ }
+ }
+
+ return threaded;
+ }
+
+ /**
+ * @param threaded Shell to return to pool
+ */
+ private static synchronized void releaseReservation(@NonNull Threaded threaded) {
+ Debug.logPool("releaseReservation");
+ threaded.setReserved(false);
+ cleanup(null, false);
+ }
+
+ /**
+ * @param threaded This shell is dead
+ */
+ private static synchronized void removeShell(@NonNull Threaded threaded) {
+ Debug.logPool("removeShell");
+ cleanup(threaded, false);
+ }
+
+ /**
+ * Close (as soon as they become idle) all pooled {@link Threaded} shells
+ */
+ @AnyThread
+ public static synchronized void closeAll() {
+ cleanup(null, true);
+ }
+
+ /**
+ * Create a {@link PoolWrapper} for the given shell command. A returned shell is
+ * automatically pooled. If the command is based on "su", {@link #getPoolSize()}
+ * applies, if not, only a single instance is kept
+ *
+ * @param shell Shell command, like "sh" or "su"
+ * @return {@link PoolWrapper} for this shell command
+ */
+ @AnyThread
+ public static PoolWrapper getWrapper(@NonNull String shell) {
+ if (shell.toUpperCase(Locale.ENGLISH).equals("SH") && (SH != null)) {
+ return SH;
+ } else if (shell.toUpperCase(Locale.ENGLISH).equals("SU") && (SU != null)) {
+ return SU;
+ } else {
+ return new PoolWrapper(shell);
+ }
+ }
+
+ /**
+ * {@link PoolWrapper} for the "sh" shell
+ */
+ public static final PoolWrapper SH = getWrapper("sh");
+
+ /**
+ * {@link PoolWrapper} for the "su" (root) shell
+ */
+ public static final PoolWrapper SU = getWrapper("su");
+ }
+
+ /**
+ *
+ * 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 shells = new ArrayList();
+
+ @AnyThread
+ static synchronized void protect(@NonNull Threaded shell) {
+ if (shells.indexOf(shell) == -1) {
+ shells.add(shell);
+ }
+ }
+
+ @AnyThread
+ static synchronized void collect(@NonNull Threaded shell) {
+ if (shells.indexOf(shell) != -1) {
+ shells.remove(shell);
+ }
+ }
}
}
diff --git a/app/src/main/java/eu/chainfire/libsuperuser/StreamGobbler.java b/app/src/main/java/eu/chainfire/libsuperuser/StreamGobbler.java
index 4ce66974..08994201 100644
--- a/app/src/main/java/eu/chainfire/libsuperuser/StreamGobbler.java
+++ b/app/src/main/java/eu/chainfire/libsuperuser/StreamGobbler.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.
@@ -21,78 +21,139 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
+import java.util.Locale;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
/**
* Thread utility class continuously reading from an InputStream
*/
-public class StreamGobbler extends Thread {
+@SuppressWarnings({"WeakerAccess"})
+public class StreamGobbler extends Thread {
+ private static int threadCounter = 0;
+ private static int incThreadCounter() {
+ synchronized (StreamGobbler.class) {
+ int ret = threadCounter;
+ threadCounter++;
+ return ret;
+ }
+ }
+
/**
* Line callback interface
*/
- public interface OnLineListener {
+ public interface OnLineListener {
/**
* Line callback
- *
+ *
* This callback should process the line as quickly as possible.
* Delays in this callback may pause the native process or even
* result in a deadlock
- *
+ *
* @param line String that was gobbled
*/
void onLine(String line);
}
- private String shell = null;
- private BufferedReader reader = null;
- private List writer = null;
- private OnLineListener listener = null;
+ /**
+ * Stream closed callback interface
+ */
+ public interface OnStreamClosedListener {
+ /**
+ * Stream closed callback
+ */
+ void onStreamClosed();
+ }
+
+ @NonNull
+ private final String shell;
+ @NonNull
+ private final InputStream inputStream;
+ @NonNull
+ private final BufferedReader reader;
+ @Nullable
+ private final List writer;
+ @Nullable
+ private final OnLineListener lineListener;
+ @Nullable
+ private final OnStreamClosedListener streamClosedListener;
+ private volatile boolean active = true;
+ private volatile boolean calledOnClose = false;
/**
* StreamGobbler 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 List to write to, or null
+ * @param outputList {@literal List} to write to, or null
*/
- public StreamGobbler(String shell, InputStream inputStream, List outputList) {
+ @AnyThread
+ public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, @Nullable List outputList) {
+ super("Gobbler#" + incThreadCounter());
this.shell = shell;
+ this.inputStream = inputStream;
reader = new BufferedReader(new InputStreamReader(inputStream));
writer = outputList;
+ lineListener = null;
+ streamClosedListener = null;
}
/**
* StreamGobbler 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();
}
}