Support launching RootServices with a non main shell

This commit is contained in:
topjohnwu 2022-02-27 00:03:10 -08:00
parent a458697a43
commit 88f384b39c
2 changed files with 99 additions and 49 deletions

View File

@ -49,9 +49,9 @@ import com.topjohnwu.superuser.ShellUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@ -73,6 +73,12 @@ public class RootServiceManager implements Handler.Callback {
static final int MSG_STOP = 2;
private static final String INTENT_EXTRA_KEY = "extra.bundle";
private static final String API_27_DEBUG =
"-Xrunjdwp:transport=dt_android_adb,suspend=n,server=y " +
"-Xcompiler-option --debuggable";
private static final String API_28_DEBUG =
"-XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y " +
"-Xcompiler-option --debuggable";
private static final int REMOTE_EN_ROUTE = 1 << 0;
private static final int DAEMON_EN_ROUTE = 1 << 1;
@ -131,7 +137,7 @@ public class RootServiceManager implements Handler.Callback {
private RootServiceManager() {}
private Runnable createStartRootProcessTask(ComponentName name, String action) {
private Shell.Task startRootProcess(ComponentName name, String action) {
Context context = Utils.getContext();
boolean[] debug = new boolean[1];
if (filterAction == null) {
@ -179,32 +185,36 @@ public class RootServiceManager implements Handler.Callback {
// Reference of the params to start jdwp:
// https://developer.android.com/ndk/guides/wrap-script#debugging_when_using_wrapsh
if (Build.VERSION.SDK_INT == 27) {
debugParams = "-Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable";
debugParams = API_27_DEBUG;
} else {
debugParams = "-XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable";
debugParams = API_28_DEBUG;
}
}
String cmd = String.format(Locale.ROOT,
"(%s CLASSPATH=%s /proc/%d/exe %s /system/bin --nice-name=%s:root " +
"com.topjohnwu.superuser.internal.RootServerMain %s %d %s %s)&",
"com.topjohnwu.superuser.internal.RootServerMain %s %d %s %s >/dev/null 2>&1)&",
logParams, mainJar, Process.myPid(), debugParams, context.getPackageName(),
name.flattenToString().replace("$", "\\$"), // args[0]
Process.myUid(), // args[1]
filterAction, // args[2]
action); // args[3]
return () -> {
try {
// Dump main.jar as trampoline
try (InputStream in = context.getResources().getAssets().open("main.jar");
OutputStream out = new FileOutputStream(mainJar)) {
Utils.pump(in, out);
}
Shell.su(cmd).exec();
} catch (IOException e) {
Utils.err(TAG, e);
return (stdin, stdout, stderr) -> {
// Dump main.jar as trampoline
try (InputStream in = context.getResources().getAssets().open("main.jar");
OutputStream out = new FileOutputStream(mainJar)) {
Utils.pump(in, out);
}
Utils.log(TAG, cmd);
// Write command to stdin
byte[] bytes = cmd.getBytes(StandardCharsets.UTF_8);
stdin.write(bytes);
stdin.write('\n');
stdin.flush();
// Since all output for the command is redirected to /dev/null and
// the command runs in the background, we don't need to wait and
// can just return.
};
}
@ -246,7 +256,7 @@ public class RootServiceManager implements Handler.Callback {
return null;
}
public Runnable createBindTask(Intent intent, Executor executor, ServiceConnection conn) {
public Shell.Task createBindTask(Intent intent, Executor executor, ServiceConnection conn) {
Pair<ComponentName, Boolean> key = bindInternal(intent, executor, conn);
if (key != null) {
pendingTasks.add(() -> bindInternal(intent, executor, conn) == null);
@ -254,7 +264,7 @@ public class RootServiceManager implements Handler.Callback {
String action = key.second ? CMDLINE_START_DAEMON : CMDLINE_START_SERVICE;
if ((flags & mask) == 0) {
flags |= mask;
return createStartRootProcessTask(key.first, action);
return startRootProcess(key.first, action);
}
}
return null;
@ -296,7 +306,7 @@ public class RootServiceManager implements Handler.Callback {
}
}
public void stop(Intent intent) {
public Shell.Task createStopTask(Intent intent) {
enforceMainThread();
Pair<ComponentName, Boolean> key = enforceIntent(intent);
@ -304,10 +314,9 @@ public class RootServiceManager implements Handler.Callback {
if (p == null) {
if (key.second) {
// Start a new root process to stop daemon
Runnable r = createStartRootProcessTask(key.first, CMDLINE_STOP_SERVICE);
Shell.EXECUTOR.execute(r);
return startRootProcess(key.first, CMDLINE_STOP_SERVICE);
}
return;
return null;
}
stopInternal(key);
@ -316,6 +325,7 @@ public class RootServiceManager implements Handler.Callback {
} catch (RemoteException e) {
Utils.err(TAG, e);
}
return null;
}
@Override

View File

@ -36,6 +36,7 @@ import com.topjohnwu.superuser.internal.RootServiceServer;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import com.topjohnwu.superuser.internal.Utils;
import java.io.IOException;
import java.util.concurrent.Executor;
/**
@ -85,13 +86,13 @@ public abstract class RootService extends ContextWrapper {
* <p>
* Add this category in the intents passed to {@link #bind(Intent, ServiceConnection)},
* {@link #bind(Intent, Executor, ServiceConnection)}, or
* {@link #createBindTask(Intent, Executor, ServiceConnection)}
* {@link #bindOrTask(Intent, Executor, ServiceConnection)}
* to have the service launched in "Daemon Mode".
*/
public static final String CATEGORY_DAEMON_MODE = "com.topjohnwu.superuser.DAEMON_MODE";
/**
* Connect to a root service, creating it if needed.
* Bind to a root service, launching a new root process if needed.
* @param intent identifies the service to connect to.
* @param executor callbacks on ServiceConnection will be called on this executor.
* @param conn receives information as the service is started and stopped.
@ -102,14 +103,14 @@ public abstract class RootService extends ContextWrapper {
@NonNull Intent intent,
@NonNull Executor executor,
@NonNull ServiceConnection conn) {
Runnable r = createBindTask(intent, executor, conn);
if (r != null) {
Shell.EXECUTOR.execute(r);
Shell.Task task = bindOrTask(intent, executor, conn);
if (task != null) {
Shell.EXECUTOR.execute(asRunnable(task));
}
}
/**
* Connect to a root service, creating it if needed.
* Bind to a root service, launching a new root process if needed.
* @param intent identifies the service to connect to.
* @param conn receives information as the service is started and stopped.
* @see Context#bindService(Intent, ServiceConnection, int)
@ -120,20 +121,19 @@ public abstract class RootService extends ContextWrapper {
}
/**
* Connect to a root service, creating it if needed.
* Bind to a root service, creating a task to launch a new root process if needed.
* <p>
* This method is useful if you want to precisely manage which background thread and the
* timing to do I/O operations and execute root commands for creating a new root process.
* <p>
* Binding will NOT happen if the developer does not run the returned {@link Runnable}.
* @return a {@link Runnable} instance on which a new root process will be launched upon
* calling {@link Runnable#run()}. If there is no need for creating a new root process,
* {@code null} is returned.
* If the application is already connected to a root process, binding will happen immediately
* and this method will return {@code null}. Or else this method returns a {@link Shell.Task}
* that has to be executed to launch the root process. Binding will only happen after the
* developer has executed the returned task with {@link Shell#execTask(Shell.Task)}.
* @return the task to launch a root process. If there is no need to launch a new root
* process, {@code null} is returned.
* @see #bind(Intent, Executor, ServiceConnection)
*/
@MainThread
@Nullable
public static Runnable createBindTask(
public static Shell.Task bindOrTask(
@NonNull Intent intent,
@NonNull Executor executor,
@NonNull ServiceConnection conn) {
@ -141,8 +141,9 @@ public abstract class RootService extends ContextWrapper {
}
/**
* Disconnect from a root service.
* @param conn the connection interface previously supplied to {@link #bind(Intent, ServiceConnection)}
* Unbind from a root service.
* @param conn the connection interface previously supplied to
* {@link #bind(Intent, ServiceConnection)}
* @see Context#unbindService(ServiceConnection)
*/
@MainThread
@ -151,18 +152,43 @@ public abstract class RootService extends ContextWrapper {
}
/**
* Force stop a root service.
* Force stop a root service, launching a new root process if needed.
* <p>
* Since root services are bound only, unlike {@link Context#stopService(Intent)}, this
* method is used to immediately stop a root service regardless of its state.
* Only use this method to stop a daemon root service; for normal root services please use
* {@link #unbind(ServiceConnection)} instead as this method could potentially end up starting
* an additional root process to make sure daemon services are stopped.
* This method is used to immediately stop a root service regardless of its state.
* ONLY use this method to stop a daemon root service; for normal root services, please use
* {@link #unbind(ServiceConnection)} instead as this method has to potentially launch
* an additional root process to ensure daemon services are stopped.
* @param intent identifies the service to stop.
*/
@MainThread
public static void stop(@NonNull Intent intent) {
RootServiceManager.getInstance().stop(intent);
Shell.Task task = stopOrTask(intent);
if (task != null) {
Shell.EXECUTOR.execute(asRunnable(task));
}
}
/**
* Force stop a root service, creating a task to launch a new root process if needed.
* <p>
* This method returns a {@link Shell.Task} that has to be executed to launch a
* root process if necessary, or else {@code null} will be returned.
* @see #stop(Intent)
*/
@MainThread
@Nullable
public static Shell.Task stopOrTask(@NonNull Intent intent) {
return RootServiceManager.getInstance().createStopTask(intent);
}
private static Runnable asRunnable(Shell.Task task) {
return () -> {
try {
Shell.getShell().execTask(task);
} catch (IOException e) {
Utils.err(e);
}
};
}
public RootService() {
@ -180,7 +206,7 @@ public abstract class RootService extends ContextWrapper {
/**
* Return the component name that will be used for service lookup.
* <p>
* Overriding this method is only for very unusual situations when a different
* Overriding this method is only for very unusual use cases when a different
* component name other than the actual class name is desired.
* @return the desired component name
*/
@ -222,11 +248,25 @@ public abstract class RootService extends ContextWrapper {
public void onDestroy() {}
/**
* Force stop this root service process.
* <p>
* This is the same as calling {@link #stop(Intent)} for this particular service.
* Force stop this root service.
*/
public final void stopSelf() {
RootServiceServer.getInstance(this).selfStop(getComponentName());
}
// Deprecated APIs
/**
* @deprecated use {@link #bindOrTask(Intent, Executor, ServiceConnection)}
*/
@MainThread
@Nullable
@Deprecated
public static Runnable createBindTask(
@NonNull Intent intent,
@NonNull Executor executor,
@NonNull ServiceConnection conn) {
Shell.Task task = bindOrTask(intent, executor, conn);
return task == null ? null : asRunnable(task);
}
}