mirror of
https://github.com/topjohnwu/libsu.git
synced 2024-11-26 21:40:42 +00:00
Multiuser daemon service support
This commit is contained in:
parent
07b34a3802
commit
99e9b1deed
@ -20,6 +20,8 @@ import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.os.Build;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.RestrictTo;
|
||||
@ -34,6 +36,10 @@ import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||
public final class Utils {
|
||||
@ -117,4 +123,20 @@ public final class Utils {
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
static <K, V> Map<K, V> newArrayMap() {
|
||||
if (Build.VERSION.SDK_INT >= 19) {
|
||||
return new ArrayMap<>();
|
||||
} else {
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
static <E> Set<E> newArraySet() {
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
return new ArraySet<>();
|
||||
} else {
|
||||
return new HashSet<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,5 +6,5 @@ package com.topjohnwu.libsuexample;
|
||||
interface ITestService {
|
||||
int getPid();
|
||||
int getUid();
|
||||
String readCmdline();
|
||||
String getUUID();
|
||||
}
|
||||
|
@ -16,23 +16,9 @@
|
||||
|
||||
#include <jni.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
extern "C" JNIEXPORT JNICALL
|
||||
jint Java_com_topjohnwu_libsuexample_AIDLService_nativeGetUid(
|
||||
JNIEnv *env, jobject instance) {
|
||||
return getuid();
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT JNICALL
|
||||
jstring Java_com_topjohnwu_libsuexample_AIDLService_nativeReadFile(
|
||||
JNIEnv *env, jobject instance, jstring name) {
|
||||
const char *path = env->GetStringUTFChars(name, nullptr);
|
||||
int fd = open(path, O_RDONLY);
|
||||
env->ReleaseStringUTFChars(name, path);
|
||||
char buf[4096];
|
||||
buf[read(fd, buf, sizeof(buf) - 1)] = 0;
|
||||
return env->NewStringUTF(buf);
|
||||
}
|
||||
|
@ -16,6 +16,8 @@
|
||||
|
||||
package com.topjohnwu.libsuexample;
|
||||
|
||||
import static com.topjohnwu.libsuexample.MainActivity.TAG;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.os.Process;
|
||||
@ -25,10 +27,11 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import com.topjohnwu.superuser.ipc.RootService;
|
||||
|
||||
import static com.topjohnwu.libsuexample.MainActivity.TAG;
|
||||
import java.util.UUID;
|
||||
|
||||
// Demonstrate RootService using AIDL (daemon mode)
|
||||
class AIDLService extends RootService {
|
||||
|
||||
static {
|
||||
// Only load the library when this class is loaded in a root process.
|
||||
// The classloader will load this class (and call this static block) in the non-root
|
||||
@ -40,7 +43,6 @@ class AIDLService extends RootService {
|
||||
|
||||
// Demonstrate we can also run native code via JNI with RootServices
|
||||
native int nativeGetUid();
|
||||
native String nativeReadFile(String file);
|
||||
|
||||
class TestIPC extends ITestService.Stub {
|
||||
@Override
|
||||
@ -54,12 +56,19 @@ class AIDLService extends RootService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readCmdline() {
|
||||
// Normally we cannot read /proc/cmdline without root
|
||||
return nativeReadFile("/proc/cmdline");
|
||||
public String getUUID() {
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
|
||||
private String uuid;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
uuid = UUID.randomUUID().toString();
|
||||
Log.d(TAG, "AIDLService: onCreate, " + uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRebind(@NonNull Intent intent) {
|
||||
// This callback will be called when we are reusing a previously started root process
|
||||
@ -75,7 +84,7 @@ class AIDLService extends RootService {
|
||||
@Override
|
||||
public boolean onUnbind(@NonNull Intent intent) {
|
||||
Log.d(TAG, "AIDLService: onUnbind, client process unbound");
|
||||
// We return true here to tell libsu that we want this service to run as a daemon
|
||||
// Return true here so onRebindg will be called
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -31,19 +31,24 @@ import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.topjohnwu.superuser.internal.Utils;
|
||||
import com.topjohnwu.superuser.ipc.RootService;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
// Demonstrate root service using Messengers
|
||||
class MSGService extends RootService implements Handler.Callback {
|
||||
|
||||
static final int MSG_GETINFO = 1;
|
||||
static final int MSG_STOP = 2;
|
||||
static final String CMDLINE_KEY = "cmdline";
|
||||
static final String UUID_KEY = "uuid";
|
||||
|
||||
private String uuid;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
uuid = UUID.randomUUID().toString();
|
||||
Log.d(TAG, "MSGService: onCreate, " + uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(@NonNull Intent intent) {
|
||||
@ -65,15 +70,8 @@ class MSGService extends RootService implements Handler.Callback {
|
||||
reply.what = msg.what;
|
||||
reply.arg1 = Process.myPid();
|
||||
reply.arg2 = Process.myUid();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
try (FileInputStream in = new FileInputStream("/proc/cmdline")) {
|
||||
// libsu internal util method, pumps input to output
|
||||
Utils.pump(in, out);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "IO error", e);
|
||||
}
|
||||
Bundle data = new Bundle();
|
||||
data.putString(CMDLINE_KEY, out.toString());
|
||||
data.putString(UUID_KEY, uuid);
|
||||
reply.setData(data);
|
||||
try {
|
||||
msg.replyTo.send(reply);
|
||||
@ -86,7 +84,7 @@ class MSGService extends RootService implements Handler.Callback {
|
||||
@Override
|
||||
public boolean onUnbind(@NonNull Intent intent) {
|
||||
Log.d(TAG, "MSGService: onUnbind, client process unbound");
|
||||
// Default returns false, which means NOT daemon mode
|
||||
// Default returns false, which means onRebind will not be called
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,6 @@ import com.topjohnwu.superuser.ipc.RootService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class MainActivity extends Activity implements Handler.Callback {
|
||||
@ -57,15 +56,10 @@ public class MainActivity extends Activity implements Handler.Callback {
|
||||
);
|
||||
}
|
||||
|
||||
private ITestService testIPC;
|
||||
private Messenger remoteMessenger;
|
||||
private Messenger myMessenger = new Messenger(new Handler(Looper.getMainLooper(), this));
|
||||
private MSGConnection conn = new MSGConnection();
|
||||
private boolean daemonTestQueued = false;
|
||||
private boolean serviceTestQueued = false;
|
||||
private final Messenger me = new Messenger(new Handler(Looper.getMainLooper(), this));
|
||||
private final List<String> consoleList = new AppendCallbackList();
|
||||
|
||||
private ActivityMainBinding binding;
|
||||
private List<String> consoleList = new AppendCallbackList();
|
||||
|
||||
// Demonstrate Shell.Initializer
|
||||
static class ExampleInitializer extends Shell.Initializer {
|
||||
@ -79,135 +73,157 @@ public class MainActivity extends Activity implements Handler.Callback {
|
||||
}
|
||||
}
|
||||
|
||||
private AIDLConnection aidlConn;
|
||||
private AIDLConnection daemonConn;
|
||||
|
||||
class AIDLConnection implements ServiceConnection {
|
||||
|
||||
private final boolean isDaemon;
|
||||
|
||||
AIDLConnection(boolean b) {
|
||||
isDaemon = b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
Log.d(TAG, "daemon onServiceConnected");
|
||||
testIPC = ITestService.Stub.asInterface(service);
|
||||
if (daemonTestQueued) {
|
||||
daemonTestQueued = false;
|
||||
testDaemon();
|
||||
Log.d(TAG, "AIDL onServiceConnected");
|
||||
if (isDaemon) daemonConn = this;
|
||||
else aidlConn = this;
|
||||
refreshUI();
|
||||
|
||||
ITestService ipc = ITestService.Stub.asInterface(service);
|
||||
try {
|
||||
consoleList.add("AIDL PID : " + ipc.getPid());
|
||||
consoleList.add("AIDL UID : " + ipc.getUid());
|
||||
consoleList.add("AIDL UUID: " + ipc.getUUID());
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Remote error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
Log.d(TAG, "daemon onServiceDisconnected");
|
||||
testIPC = null;
|
||||
Log.d(TAG, "AIDL onServiceDisconnected");
|
||||
if (isDaemon) daemonConn = null;
|
||||
else aidlConn = null;
|
||||
refreshUI();
|
||||
}
|
||||
}
|
||||
|
||||
private MSGConnection msgConn;
|
||||
|
||||
class MSGConnection implements ServiceConnection {
|
||||
|
||||
private Messenger m;
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
Log.d(TAG, "service onServiceConnected");
|
||||
remoteMessenger = new Messenger(service);
|
||||
if (serviceTestQueued) {
|
||||
serviceTestQueued = false;
|
||||
testService();
|
||||
Log.d(TAG, "MSG onServiceConnected");
|
||||
m = new Messenger(service);
|
||||
msgConn = this;
|
||||
refreshUI();
|
||||
|
||||
Message msg = Message.obtain(null, MSGService.MSG_GETINFO);
|
||||
msg.replyTo = me;
|
||||
try {
|
||||
m.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Remote error", e);
|
||||
} finally {
|
||||
msg.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
Log.d(TAG, "service onServiceDisconnected");
|
||||
remoteMessenger = null;
|
||||
Log.d(TAG, "MSG onServiceDisconnected");
|
||||
msgConn = null;
|
||||
refreshUI();
|
||||
}
|
||||
}
|
||||
|
||||
private void testDaemon() {
|
||||
try {
|
||||
consoleList.add("Daemon PID: " + testIPC.getPid());
|
||||
consoleList.add("Daemon UID: " + testIPC.getUid());
|
||||
String[] cmds = testIPC.readCmdline().split(" ");
|
||||
if (cmds.length > 5) {
|
||||
cmds = Arrays.copyOf(cmds, 6);
|
||||
cmds[5] = "...";
|
||||
void stop() {
|
||||
if (m == null)
|
||||
return;
|
||||
Message msg = Message.obtain(null, MSGService.MSG_STOP);
|
||||
try {
|
||||
m.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Remote error", e);
|
||||
} finally {
|
||||
msg.recycle();
|
||||
}
|
||||
consoleList.add("/proc/cmdline:");
|
||||
consoleList.addAll(Arrays.asList(cmds));
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Remote error", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void testService() {
|
||||
Message message = Message.obtain(null, MSGService.MSG_GETINFO);
|
||||
message.replyTo = myMessenger;
|
||||
try {
|
||||
remoteMessenger.send(message);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Remote error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleMessage(@NonNull Message msg) {
|
||||
consoleList.add("Remote PID: " + msg.arg1);
|
||||
consoleList.add("Remote UID: " + msg.arg2);
|
||||
String cmdline = msg.getData().getString(MSGService.CMDLINE_KEY);
|
||||
String[] cmds = cmdline.split(" ");
|
||||
if (cmds.length > 5) {
|
||||
cmds = Arrays.copyOf(cmds, 6);
|
||||
cmds[5] = "...";
|
||||
}
|
||||
consoleList.add("/proc/cmdline:");
|
||||
consoleList.addAll(Arrays.asList(cmds));
|
||||
consoleList.add("MSG PID : " + msg.arg1);
|
||||
consoleList.add("MSG UID : " + msg.arg2);
|
||||
String uuid = msg.getData().getString(MSGService.UUID_KEY);
|
||||
consoleList.add("MSG UUID : " + uuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void refreshUI() {
|
||||
binding.aidlSvc.setText(aidlConn == null ? "Bind AIDL" : "Unbind AIDL");
|
||||
binding.msgSvc.setText(msgConn == null ? "Bind MSG" : "Unbind MSG");
|
||||
binding.testDaemon.setText(daemonConn == null ? "Bind Daemon" : "Unbind Daemon");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
// Bind to a root service; IPC via Messages
|
||||
binding.testSvc.setOnClickListener(v -> {
|
||||
if (remoteMessenger == null) {
|
||||
serviceTestQueued = true;
|
||||
Intent intent = new Intent(this, MSGService.class);
|
||||
RootService.bind(intent, conn);
|
||||
return;
|
||||
}
|
||||
testService();
|
||||
});
|
||||
|
||||
// Unbind service through RootService API
|
||||
binding.unbindSvc.setOnClickListener(v -> RootService.unbind(conn));
|
||||
|
||||
// Send a message to service and ask it to stop itself to demonstrate stopSelf()
|
||||
binding.stopSvc.setOnClickListener(v -> {
|
||||
if (remoteMessenger != null) {
|
||||
Message message = Message.obtain(null, MSGService.MSG_STOP);
|
||||
try {
|
||||
remoteMessenger.send(message);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Remote error", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Bind to a daemon root service; IPC via AIDL
|
||||
binding.testDaemon.setOnClickListener(v -> {
|
||||
if (testIPC == null) {
|
||||
daemonTestQueued = true;
|
||||
// Bind to a root service; IPC via AIDL
|
||||
binding.aidlSvc.setOnClickListener(v -> {
|
||||
if (aidlConn == null) {
|
||||
Intent intent = new Intent(this, AIDLService.class);
|
||||
RootService.bind(intent, new AIDLConnection());
|
||||
return;
|
||||
RootService.bind(intent, new AIDLConnection(false));
|
||||
} else {
|
||||
RootService.unbind(aidlConn);
|
||||
}
|
||||
testDaemon();
|
||||
});
|
||||
|
||||
// Bind to a root service; IPC via Messages
|
||||
binding.msgSvc.setOnClickListener(v -> {
|
||||
if (msgConn == null) {
|
||||
Intent intent = new Intent(this, MSGService.class);
|
||||
RootService.bind(intent, new MSGConnection());
|
||||
} else {
|
||||
RootService.unbind(msgConn);
|
||||
}
|
||||
});
|
||||
|
||||
// Send a message to service and ask it to stop itself to test stopSelf()
|
||||
binding.selfStop.setOnClickListener(v -> {
|
||||
if (msgConn != null) {
|
||||
msgConn.stop();
|
||||
}
|
||||
});
|
||||
|
||||
// To verify whether the daemon actually works, kill the app and try testing the
|
||||
// daemon again. You should get the same PID as last time (as it was re-using the
|
||||
// previous daemon process), and in AIDLService, onRebind should be called.
|
||||
// Note: re-running the app in Android Studio is not the same as kill + relaunch.
|
||||
// The root process will kill itself when it detects the client APK has updated.
|
||||
|
||||
// Bind to a daemon root service
|
||||
binding.testDaemon.setOnClickListener(v -> {
|
||||
if (daemonConn == null) {
|
||||
Intent intent = new Intent(this, AIDLService.class);
|
||||
intent.addCategory(RootService.CATEGORY_DAEMON_MODE);
|
||||
RootService.bind(intent, new AIDLConnection(true));
|
||||
} else {
|
||||
RootService.unbind(daemonConn);
|
||||
}
|
||||
});
|
||||
|
||||
// Test the stop API
|
||||
binding.stopDaemon.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(this, AIDLService.class);
|
||||
// Use stop here instead of unbind because AIDLService is running as a daemon.
|
||||
// To verify whether the daemon actually works, kill the app and try testing the
|
||||
// daemon again. You should get the same PID as last time (as it was re-using the
|
||||
// previous daemon process), and in AIDLService, onRebind should be called.
|
||||
// Note: re-running the app in Android Studio is not the same as kill + relaunch.
|
||||
// The root process will kill itself when it detects the client APK has updated.
|
||||
intent.addCategory(RootService.CATEGORY_DAEMON_MODE);
|
||||
RootService.stop(intent);
|
||||
});
|
||||
|
||||
|
@ -72,23 +72,23 @@
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/test_svc"
|
||||
android:id="@+id/aidl_svc"
|
||||
style="?android:borderlessButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Test SVC" />
|
||||
android:text="Bind AIDL" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/unbind_svc"
|
||||
android:id="@+id/msg_svc"
|
||||
style="?android:borderlessButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Unbind" />
|
||||
android:text="Bind MSG" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/stop_svc"
|
||||
android:id="@+id/self_stop"
|
||||
style="?android:borderlessButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
@ -118,7 +118,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Test Daemon" />
|
||||
android:text="Bind Daemon" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/stop_daemon"
|
||||
|
@ -5,8 +5,8 @@ package com.topjohnwu.superuser.internal;
|
||||
|
||||
interface IRootServiceManager {
|
||||
oneway void broadcast(int uid, String action);
|
||||
oneway void stop(in ComponentName name);
|
||||
oneway void connect(in Bundle bundle);
|
||||
oneway void stop(in ComponentName name, int uid, String action);
|
||||
oneway void connect(in IBinder binder, boolean debug);
|
||||
IBinder bind(in Intent intent);
|
||||
oneway void unbind(in ComponentName name);
|
||||
}
|
||||
|
Binary file not shown.
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2022 John "topjohnwu" Wu
|
||||
*
|
||||
* 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 com.topjohnwu.superuser.internal;
|
||||
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
|
||||
abstract class BinderHolder implements IBinder.DeathRecipient {
|
||||
|
||||
private final IBinder binder;
|
||||
|
||||
BinderHolder(IBinder b) throws RemoteException {
|
||||
binder = b;
|
||||
binder.linkToDeath(this, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void binderDied() {
|
||||
binder.unlinkToDeath(this, 0);
|
||||
UiThreadHandler.run(this::onBinderDied);
|
||||
}
|
||||
|
||||
protected abstract void onBinderDied();
|
||||
}
|
@ -24,7 +24,6 @@ import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
@ -44,12 +43,13 @@ Expected command-line args:
|
||||
args[0]: client service component name
|
||||
args[1]: client UID
|
||||
args[2]: client broadcast receiver intent filter
|
||||
args[3]: {@link #CMDLINE_START_SERVICE} or {@link #CMDLINE_STOP_SERVICE}
|
||||
args[3]: CMDLINE_START_SERVICE, CMDLINE_START_DAEMON, or CMDLINE_STOP_SERVICE
|
||||
*/
|
||||
class RootServerMain extends ContextWrapper implements Callable<Pair<Integer, String>> {
|
||||
class RootServerMain extends ContextWrapper implements Callable<Object[]> {
|
||||
|
||||
static final String CMDLINE_STOP_SERVICE = "stop";
|
||||
static final String CMDLINE_START_SERVICE = "start";
|
||||
static final String CMDLINE_START_DAEMON = "daemon";
|
||||
static final String CMDLINE_STOP_SERVICE = "stop";
|
||||
|
||||
static final Method getService;
|
||||
static final Method addService;
|
||||
@ -110,10 +110,15 @@ class RootServerMain extends ContextWrapper implements Callable<Pair<Integer, St
|
||||
|
||||
private final int uid;
|
||||
private final String filter;
|
||||
private final boolean isDaemon;
|
||||
|
||||
@Override
|
||||
public Pair<Integer, String> call() {
|
||||
return new Pair<>(uid, filter);
|
||||
public Object[] call() {
|
||||
Object[] objs = new Object[3];
|
||||
objs[0] = uid;
|
||||
objs[1] = filter;
|
||||
objs[2] = isDaemon;
|
||||
return objs;
|
||||
}
|
||||
|
||||
public RootServerMain(String[] args) throws Exception {
|
||||
@ -123,30 +128,38 @@ class RootServerMain extends ContextWrapper implements Callable<Pair<Integer, St
|
||||
uid = Integer.parseInt(args[1]);
|
||||
filter = args[2];
|
||||
String action = args[3];
|
||||
boolean stop = false;
|
||||
|
||||
// Get existing daemon process
|
||||
Object binder = getService.invoke(null, getServiceName(name.getPackageName()));
|
||||
IRootServiceManager m = IRootServiceManager.Stub.asInterface((IBinder) binder);
|
||||
|
||||
if (action.equals(CMDLINE_STOP_SERVICE)) {
|
||||
if (m != null) {
|
||||
try {
|
||||
m.stop(name);
|
||||
// If the process wasn't killed yet, send another broadcast
|
||||
m.broadcast(uid, filter);
|
||||
} catch (RemoteException ignored) {}
|
||||
}
|
||||
System.exit(0);
|
||||
switch (action) {
|
||||
case CMDLINE_STOP_SERVICE:
|
||||
stop = true;
|
||||
// fallthrough
|
||||
case CMDLINE_START_DAEMON:
|
||||
isDaemon = true;
|
||||
break;
|
||||
default:
|
||||
isDaemon = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (m != null) {
|
||||
try {
|
||||
if (isDaemon) daemon: try {
|
||||
// Get existing daemon process
|
||||
Object binder = getService.invoke(null, getServiceName(name.getPackageName()));
|
||||
IRootServiceManager m = IRootServiceManager.Stub.asInterface((IBinder) binder);
|
||||
if (m == null)
|
||||
break daemon;
|
||||
|
||||
if (stop) {
|
||||
m.stop(name, uid, filter);
|
||||
} else {
|
||||
m.broadcast(uid, filter);
|
||||
// Terminate process if broadcast went through
|
||||
// Terminate process if broadcast went through without exception
|
||||
System.exit(0);
|
||||
} catch (RemoteException ignored) {
|
||||
// Daemon process dead, continue
|
||||
}
|
||||
} catch (RemoteException ignored) {
|
||||
} finally {
|
||||
if (stop)
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
Context systemContext = getSystemContext();
|
||||
|
@ -16,8 +16,11 @@
|
||||
|
||||
package com.topjohnwu.superuser.internal;
|
||||
|
||||
import static com.topjohnwu.superuser.internal.RootServerMain.CMDLINE_START_DAEMON;
|
||||
import static com.topjohnwu.superuser.internal.RootServerMain.CMDLINE_START_SERVICE;
|
||||
import static com.topjohnwu.superuser.internal.RootServerMain.CMDLINE_STOP_SERVICE;
|
||||
import static com.topjohnwu.superuser.internal.Utils.newArrayMap;
|
||||
import static com.topjohnwu.superuser.ipc.RootService.CATEGORY_DAEMON_MODE;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.BroadcastReceiver;
|
||||
@ -36,7 +39,6 @@ import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@ -51,7 +53,6 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@ -60,12 +61,11 @@ import java.util.UUID;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||
public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callback {
|
||||
public class RootServiceManager implements Handler.Callback {
|
||||
|
||||
private static RootServiceManager mInstance;
|
||||
|
||||
static final String TAG = "IPC";
|
||||
static final String BUNDLE_DEBUG_KEY = "debug";
|
||||
static final String BUNDLE_BINDER_KEY = "binder";
|
||||
static final String LOGGING_ENV = "LIBSU_VERBOSE_LOGGING";
|
||||
|
||||
@ -74,6 +74,9 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
|
||||
|
||||
private static final String INTENT_EXTRA_KEY = "extra.bundle";
|
||||
|
||||
private static final int REMOTE_EN_ROUTE = 1 << 0;
|
||||
private static final int DAEMON_EN_ROUTE = 1 << 1;
|
||||
|
||||
public static RootServiceManager getInstance() {
|
||||
if (mInstance == null) {
|
||||
mInstance = new RootServiceManager();
|
||||
@ -98,7 +101,7 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static ComponentName enforceIntent(Intent intent) {
|
||||
private static Pair<ComponentName, Boolean> enforceIntent(Intent intent) {
|
||||
ComponentName name = intent.getComponent();
|
||||
if (name == null) {
|
||||
throw new IllegalArgumentException("The intent does not have a component set");
|
||||
@ -106,38 +109,35 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
|
||||
if (!name.getPackageName().equals(Utils.getContext().getPackageName())) {
|
||||
throw new IllegalArgumentException("RootServices outside of the app are not supported");
|
||||
}
|
||||
return name;
|
||||
return new Pair<>(name, intent.hasCategory(CATEGORY_DAEMON_MODE));
|
||||
}
|
||||
|
||||
private IRootServiceManager mRM;
|
||||
private List<Runnable> pendingTasks;
|
||||
private static void disconnect(Map.Entry<ServiceConnection, Pair<RemoteService, Executor>> e) {
|
||||
e.getValue().second.execute(() ->
|
||||
e.getKey().onServiceDisconnected(e.getValue().first.key.first));
|
||||
}
|
||||
|
||||
private RemoteProcess mRemote;
|
||||
private RemoteProcess mDaemon;
|
||||
|
||||
private String filterAction;
|
||||
private int flags = 0;
|
||||
|
||||
private final Map<ComponentName, RemoteService> services;
|
||||
private final Map<ServiceConnection, Pair<RemoteService, Executor>> connections;
|
||||
private final List<BindTask> pendingTasks = new ArrayList<>();
|
||||
private final Map<Pair<ComponentName, Boolean>, RemoteService> services = newArrayMap();
|
||||
private final Map<ServiceConnection, Pair<RemoteService, Executor>> connections = newArrayMap();
|
||||
|
||||
private RootServiceManager() {
|
||||
if (Build.VERSION.SDK_INT >= 19) {
|
||||
services = new ArrayMap<>();
|
||||
connections = new ArrayMap<>();
|
||||
} else {
|
||||
services = new HashMap<>();
|
||||
connections = new HashMap<>();
|
||||
}
|
||||
}
|
||||
private RootServiceManager() {}
|
||||
|
||||
private Runnable createStartRootProcessTask(ComponentName name, String action) {
|
||||
Context context = Utils.getContext();
|
||||
Bundle b = null;
|
||||
boolean[] debug = new boolean[1];
|
||||
if (filterAction == null) {
|
||||
filterAction = UUID.randomUUID().toString();
|
||||
Bundle connectArgs = new Bundle();
|
||||
b = connectArgs;
|
||||
|
||||
// Receive ACK and service stop signal
|
||||
Handler h = new Handler(Looper.getMainLooper(), this);
|
||||
Messenger m = new Messenger(h);
|
||||
connectArgs.putBinder(BUNDLE_BINDER_KEY, m.getBinder());
|
||||
|
||||
// Register receiver to receive binder from root process
|
||||
IntentFilter filter = new IntentFilter(filterAction);
|
||||
@ -150,9 +150,9 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
|
||||
IBinder binder = bundle.getBinder(BUNDLE_BINDER_KEY);
|
||||
if (binder == null)
|
||||
return;
|
||||
IRootServiceManager m = IRootServiceManager.Stub.asInterface(binder);
|
||||
IRootServiceManager sm = IRootServiceManager.Stub.asInterface(binder);
|
||||
try {
|
||||
m.connect(connectArgs);
|
||||
sm.connect(m.getBinder(), debug[0]);
|
||||
} catch (RemoteException e) {
|
||||
Utils.err(TAG, e);
|
||||
}
|
||||
@ -173,9 +173,7 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
|
||||
|
||||
// Only support debugging on SDK >= 27
|
||||
if (Build.VERSION.SDK_INT >= 27 && Debug.isDebuggerConnected()) {
|
||||
if (b != null) {
|
||||
b.putBoolean(BUNDLE_DEBUG_KEY, true);
|
||||
}
|
||||
debug[0] = true;
|
||||
// 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) {
|
||||
@ -208,51 +206,53 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
|
||||
};
|
||||
}
|
||||
|
||||
private boolean bind(Intent intent, Executor executor, ServiceConnection conn) {
|
||||
// Returns null if binding is done synchronously, or else return key
|
||||
private Pair<ComponentName, Boolean> bindInternal(
|
||||
Intent intent, Executor executor, ServiceConnection conn) {
|
||||
enforceMainThread();
|
||||
|
||||
// Local cache
|
||||
ComponentName name = enforceIntent(intent);
|
||||
RemoteService s = services.get(name);
|
||||
Pair<ComponentName, Boolean> key = enforceIntent(intent);
|
||||
RemoteService s = services.get(key);
|
||||
if (s != null) {
|
||||
connections.put(conn, new Pair<>(s, executor));
|
||||
s.refCount++;
|
||||
executor.execute(() -> conn.onServiceConnected(name, s.binder));
|
||||
return true;
|
||||
executor.execute(() -> conn.onServiceConnected(key.first, s.binder));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mRM == null)
|
||||
return false;
|
||||
RemoteProcess p = key.second ? mDaemon : mRemote;
|
||||
if (p == null)
|
||||
return key;
|
||||
|
||||
try {
|
||||
IBinder binder = mRM.bind(intent);
|
||||
IBinder binder = p.sm.bind(intent);
|
||||
if (binder != null) {
|
||||
RemoteService r = new RemoteService(name, binder);
|
||||
RemoteService r = new RemoteService(key, binder, p);
|
||||
connections.put(conn, new Pair<>(r, executor));
|
||||
services.put(name, r);
|
||||
executor.execute(() -> conn.onServiceConnected(name, binder));
|
||||
services.put(key, r);
|
||||
executor.execute(() -> conn.onServiceConnected(key.first, binder));
|
||||
} else if (Build.VERSION.SDK_INT >= 28) {
|
||||
executor.execute(() -> conn.onNullBinding(name));
|
||||
executor.execute(() -> conn.onNullBinding(key.first));
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Utils.err(TAG, e);
|
||||
mRM = null;
|
||||
return false;
|
||||
p.binderDied();
|
||||
return key;
|
||||
}
|
||||
|
||||
return true;
|
||||
return null;
|
||||
}
|
||||
|
||||
public Runnable createBindTask(Intent intent, Executor executor, ServiceConnection conn) {
|
||||
if (!bind(intent, executor, conn)) {
|
||||
boolean launch = false;
|
||||
if (pendingTasks == null) {
|
||||
pendingTasks = new ArrayList<>();
|
||||
launch = true;
|
||||
}
|
||||
pendingTasks.add(() -> bind(intent, executor, conn));
|
||||
if (launch) {
|
||||
return createStartRootProcessTask(intent.getComponent(), CMDLINE_START_SERVICE);
|
||||
Pair<ComponentName, Boolean> key = bindInternal(intent, executor, conn);
|
||||
if (key != null) {
|
||||
pendingTasks.add(() -> bindInternal(intent, executor, conn) == null);
|
||||
int mask = key.second ? DAEMON_EN_ROUTE : REMOTE_EN_ROUTE;
|
||||
String action = key.second ? CMDLINE_START_DAEMON : CMDLINE_START_SERVICE;
|
||||
if ((flags & mask) == 0) {
|
||||
flags |= mask;
|
||||
return createStartRootProcessTask(key.first, action);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@ -261,18 +261,15 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
|
||||
public void unbind(@NonNull ServiceConnection conn) {
|
||||
enforceMainThread();
|
||||
|
||||
if (mRM == null)
|
||||
return;
|
||||
|
||||
Pair<RemoteService, Executor> p = connections.remove(conn);
|
||||
if (p != null) {
|
||||
p.first.refCount--;
|
||||
p.second.execute(() -> conn.onServiceDisconnected(p.first.name));
|
||||
p.second.execute(() -> conn.onServiceDisconnected(p.first.key.first));
|
||||
if (p.first.refCount == 0) {
|
||||
// Actually close the service
|
||||
services.remove(p.first.name);
|
||||
services.remove(p.first.key);
|
||||
try {
|
||||
mRM.unbind(p.first.name);
|
||||
p.first.host.sm.unbind(p.first.key.first);
|
||||
} catch (RemoteException e) {
|
||||
Utils.err(TAG, e);
|
||||
}
|
||||
@ -280,10 +277,10 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
|
||||
}
|
||||
}
|
||||
|
||||
private boolean stopInternal(ComponentName name) {
|
||||
RemoteService s = services.remove(name);
|
||||
private void stopInternal(Pair<ComponentName, Boolean> key) {
|
||||
RemoteService s = services.remove(key);
|
||||
if (s == null)
|
||||
return false;
|
||||
return;
|
||||
|
||||
// Notify all connections
|
||||
Iterator<Map.Entry<ServiceConnection, Pair<RemoteService, Executor>>> it =
|
||||
@ -291,52 +288,34 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<ServiceConnection, Pair<RemoteService, Executor>> e = it.next();
|
||||
if (e.getValue().first.equals(s)) {
|
||||
e.getValue().second.execute(() -> e.getKey().onServiceDisconnected(name));
|
||||
disconnect(e);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void stop(Intent intent) {
|
||||
enforceMainThread();
|
||||
|
||||
ComponentName name = enforceIntent(intent);
|
||||
if (mRM == null) {
|
||||
// Start a new root process
|
||||
Runnable r = createStartRootProcessTask(name, CMDLINE_STOP_SERVICE);
|
||||
Shell.EXECUTOR.execute(r);
|
||||
Pair<ComponentName, Boolean> key = enforceIntent(intent);
|
||||
RemoteProcess p = key.second ? mDaemon : mRemote;
|
||||
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;
|
||||
}
|
||||
|
||||
if (!stopInternal(name))
|
||||
return;
|
||||
stopInternal(key);
|
||||
try {
|
||||
mRM.stop(name);
|
||||
p.sm.stop(key.first, -1, null);
|
||||
} catch (RemoteException e) {
|
||||
Utils.err(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void binderDied() {
|
||||
UiThreadHandler.run(() -> {
|
||||
if (mRM != null) {
|
||||
mRM.asBinder().unlinkToDeath(this, 0);
|
||||
mRM = null;
|
||||
}
|
||||
|
||||
// Notify all connections
|
||||
for (Map.Entry<ServiceConnection, Pair<RemoteService, Executor>> e
|
||||
: connections.entrySet()) {
|
||||
e.getValue().second.execute(() ->
|
||||
e.getKey().onServiceDisconnected(e.getValue().first.name));
|
||||
}
|
||||
connections.clear();
|
||||
services.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleMessage(@NonNull Message msg) {
|
||||
switch (msg.what) {
|
||||
@ -344,35 +323,81 @@ public class RootServiceManager implements IBinder.DeathRecipient, Handler.Callb
|
||||
IBinder b = ((Bundle) msg.obj).getBinder(BUNDLE_BINDER_KEY);
|
||||
if (b == null)
|
||||
return false;
|
||||
RemoteProcess p;
|
||||
try {
|
||||
b.linkToDeath(this, 0);
|
||||
p = new RemoteProcess(b);
|
||||
} catch (RemoteException e) {
|
||||
return false;
|
||||
}
|
||||
mRM = IRootServiceManager.Stub.asInterface(b);
|
||||
List<Runnable> tasks = pendingTasks;
|
||||
pendingTasks = null;
|
||||
if (tasks != null) {
|
||||
for (Runnable r : tasks) {
|
||||
r.run();
|
||||
if (msg.arg1 == 0) {
|
||||
mRemote = p;
|
||||
flags &= ~REMOTE_EN_ROUTE;
|
||||
} else {
|
||||
mDaemon = p;
|
||||
flags &= ~DAEMON_EN_ROUTE;
|
||||
}
|
||||
for (int i = pendingTasks.size() - 1; i >= 0; --i) {
|
||||
if (pendingTasks.get(i).run()) {
|
||||
pendingTasks.remove(i);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MSG_STOP:
|
||||
stopInternal((ComponentName) msg.obj);
|
||||
stopInternal(new Pair<>((ComponentName) msg.obj, msg.arg1 != 0));
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static class RemoteService {
|
||||
final ComponentName name;
|
||||
final IBinder binder;
|
||||
int refCount = 1;
|
||||
class RemoteProcess extends BinderHolder {
|
||||
|
||||
RemoteService(ComponentName name, IBinder binder) {
|
||||
this.name = name;
|
||||
this.binder = binder;
|
||||
final IRootServiceManager sm;
|
||||
|
||||
RemoteProcess(IBinder b) throws RemoteException {
|
||||
super(b);
|
||||
sm = IRootServiceManager.Stub.asInterface(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBinderDied() {
|
||||
if (mRemote == this)
|
||||
mRemote = null;
|
||||
if (mDaemon == this)
|
||||
mDaemon = null;
|
||||
|
||||
Iterator<RemoteService> sit = services.values().iterator();
|
||||
while (sit.hasNext()) {
|
||||
if (sit.next().host == this) {
|
||||
sit.remove();
|
||||
}
|
||||
}
|
||||
|
||||
Iterator<Map.Entry<ServiceConnection, Pair<RemoteService, Executor>>> it =
|
||||
connections.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<ServiceConnection, Pair<RemoteService, Executor>> e = it.next();
|
||||
if (e.getValue().first.host == this) {
|
||||
disconnect(e);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class RemoteService {
|
||||
final Pair<ComponentName, Boolean> key;
|
||||
final IBinder binder;
|
||||
final RemoteProcess host;
|
||||
int refCount = 1;
|
||||
|
||||
RemoteService(Pair<ComponentName, Boolean> key, IBinder binder, RemoteProcess host) {
|
||||
this.key = key;
|
||||
this.binder = binder;
|
||||
this.host = host;
|
||||
}
|
||||
}
|
||||
|
||||
interface BindTask {
|
||||
boolean run();
|
||||
}
|
||||
}
|
||||
|
@ -19,12 +19,13 @@ package com.topjohnwu.superuser.internal;
|
||||
import static com.topjohnwu.superuser.internal.RootServerMain.attachBaseContext;
|
||||
import static com.topjohnwu.superuser.internal.RootServerMain.getServiceName;
|
||||
import static com.topjohnwu.superuser.internal.RootServiceManager.BUNDLE_BINDER_KEY;
|
||||
import static com.topjohnwu.superuser.internal.RootServiceManager.BUNDLE_DEBUG_KEY;
|
||||
import static com.topjohnwu.superuser.internal.RootServiceManager.LOGGING_ENV;
|
||||
import static com.topjohnwu.superuser.internal.RootServiceManager.MSG_ACK;
|
||||
import static com.topjohnwu.superuser.internal.RootServiceManager.MSG_STOP;
|
||||
import static com.topjohnwu.superuser.internal.RootServiceManager.TAG;
|
||||
import static com.topjohnwu.superuser.internal.Utils.context;
|
||||
import static com.topjohnwu.superuser.internal.Utils.newArrayMap;
|
||||
import static com.topjohnwu.superuser.internal.Utils.newArraySet;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ComponentName;
|
||||
@ -39,8 +40,7 @@ import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Pair;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RestrictTo;
|
||||
@ -50,66 +50,66 @@ import com.topjohnwu.superuser.ipc.RootService;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||
public class RootServiceServer extends IRootServiceManager.Stub implements IBinder.DeathRecipient {
|
||||
public class RootServiceServer extends IRootServiceManager.Stub {
|
||||
|
||||
private static RootServiceServer mInstance;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static RootServiceServer getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new RootServiceServer(context);
|
||||
if (context instanceof Callable) {
|
||||
try {
|
||||
Pair p = (Pair) ((Callable) context).call();
|
||||
mInstance.broadcast((int) p.first, (String) p.second);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final FileObserver observer; /* A strong reference is required */
|
||||
private final Map<ComponentName, ServiceContainer> activeServices;
|
||||
private Messenger client;
|
||||
private boolean isDaemon = false;
|
||||
private final Map<ComponentName, ServiceContainer> activeServices = newArrayMap();
|
||||
private final SparseArray<ClientProcess> clients = new SparseArray<>();
|
||||
private final boolean isDaemon;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private RootServiceServer(Context context) {
|
||||
Shell.enableVerboseLogging = System.getenv(LOGGING_ENV) != null;
|
||||
Utils.context = Utils.getContextImpl(context);
|
||||
if (Build.VERSION.SDK_INT >= 19) {
|
||||
activeServices = new ArrayMap<>();
|
||||
} else {
|
||||
activeServices = new HashMap<>();
|
||||
}
|
||||
observer = new AppObserver(new File(context.getPackageCodePath()));
|
||||
observer.startWatching();
|
||||
if (context instanceof Callable) {
|
||||
try {
|
||||
Object[] objs = (Object[]) ((Callable) context).call();
|
||||
broadcast((int) objs[0], (String) objs[1]);
|
||||
isDaemon = (boolean) objs[2];
|
||||
if (isDaemon) {
|
||||
// Register ourselves as system service
|
||||
HiddenAPIs.addService(getServiceName(context.getPackageName()), this);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Expected Context to be Callable");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(Bundle bundle) {
|
||||
if (client != null)
|
||||
public void connect(IBinder binder, boolean debug) {
|
||||
ClientProcess c = clients.get(getCallingUid());
|
||||
if (c != null)
|
||||
return;
|
||||
|
||||
IBinder binder = bundle.getBinder(BUNDLE_BINDER_KEY);
|
||||
if (binder == null)
|
||||
return;
|
||||
final Messenger c;
|
||||
try {
|
||||
binder.linkToDeath(this, 0);
|
||||
c = new Messenger(binder);
|
||||
c = new ClientProcess(binder);
|
||||
} catch (RemoteException e) {
|
||||
Utils.err(TAG, e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bundle.getBoolean(BUNDLE_DEBUG_KEY, false)) {
|
||||
if (debug) {
|
||||
// ActivityThread.attach(true, 0) will set this to system_process
|
||||
HiddenAPIs.setAppName(context.getPackageName() + ":root");
|
||||
Utils.log(TAG, "Waiting for debugger to be attached...");
|
||||
@ -121,15 +121,18 @@ public class RootServiceServer extends IRootServiceManager.Stub implements IBind
|
||||
Utils.log(TAG, "Debugger attached!");
|
||||
}
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBinder(BUNDLE_BINDER_KEY, this);
|
||||
|
||||
Message m = Message.obtain();
|
||||
m.what = MSG_ACK;
|
||||
bundle = new Bundle();
|
||||
bundle.putBinder(BUNDLE_BINDER_KEY, this);
|
||||
m.arg1 = isDaemon ? 1 : 0;
|
||||
m.obj = bundle;
|
||||
try {
|
||||
c.send(m);
|
||||
client = c;
|
||||
} catch (RemoteException ignored) {
|
||||
c.m.send(m);
|
||||
clients.put(c.uid, c);
|
||||
} catch (RemoteException e) {
|
||||
Utils.err(TAG, e);
|
||||
} finally {
|
||||
m.recycle();
|
||||
}
|
||||
@ -137,6 +140,9 @@ public class RootServiceServer extends IRootServiceManager.Stub implements IBind
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
public void broadcast(int uid, String action) {
|
||||
// Use the UID argument iff caller is root
|
||||
uid = getCallingUid() == 0 ? uid : getCallingUid();
|
||||
Utils.log(TAG, "broadcast to uid=" + uid);
|
||||
Intent intent = RootServiceManager.getBroadcastIntent(context, action, this);
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
UserHandle h = UserHandle.getUserHandleForUid(uid);
|
||||
@ -149,9 +155,10 @@ public class RootServiceServer extends IRootServiceManager.Stub implements IBind
|
||||
@Override
|
||||
public IBinder bind(Intent intent) {
|
||||
IBinder[] b = new IBinder[1];
|
||||
int uid = getCallingUid();
|
||||
UiThreadHandler.runAndWait(() -> {
|
||||
try {
|
||||
b[0] = bindInternal(intent);
|
||||
b[0] = bindInternal(uid, intent);
|
||||
} catch (Exception e) {
|
||||
Utils.err(TAG, e);
|
||||
}
|
||||
@ -161,47 +168,55 @@ public class RootServiceServer extends IRootServiceManager.Stub implements IBind
|
||||
|
||||
@Override
|
||||
public void unbind(ComponentName name) {
|
||||
int uid = getCallingUid();
|
||||
UiThreadHandler.run(() -> {
|
||||
Utils.log(TAG, name.getClassName() + " unbind");
|
||||
stopService(name, false);
|
||||
stopService(uid, name, false);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(ComponentName name) {
|
||||
public void stop(ComponentName name, int uid, String action) {
|
||||
// Use the UID argument iff caller is root
|
||||
int clientUid = getCallingUid() == 0 ? uid : getCallingUid();
|
||||
UiThreadHandler.run(() -> {
|
||||
Utils.log(TAG, name.getClassName() + " stop");
|
||||
stopService(name, true);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void binderDied() {
|
||||
Messenger c = client;
|
||||
client = null;
|
||||
if (c != null)
|
||||
c.getBinder().unlinkToDeath(this, 0);
|
||||
UiThreadHandler.run(() -> {
|
||||
Utils.log(TAG, "Client process terminated");
|
||||
stopAllService(false);
|
||||
stopService(clientUid, name, true);
|
||||
if (action != null) {
|
||||
// If we aren't killed yet, send another broadcast
|
||||
broadcast(clientUid, action);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void selfStop(ComponentName name) {
|
||||
UiThreadHandler.run(() -> {
|
||||
ServiceContainer s = activeServices.get(name);
|
||||
if (s == null)
|
||||
return;
|
||||
|
||||
Utils.log(TAG, name.getClassName() + " selfStop");
|
||||
stopService(name, true);
|
||||
Messenger c = client;
|
||||
if (c != null) {
|
||||
Message m = Message.obtain();
|
||||
m.what = MSG_STOP;
|
||||
m.obj = name;
|
||||
|
||||
// Backup all users as we need to notify them
|
||||
Integer[] users = s.users.toArray(new Integer[0]);
|
||||
s.users.clear();
|
||||
stopService(-1, name, true);
|
||||
|
||||
// Notify all users
|
||||
for (int uid : users) {
|
||||
ClientProcess c = clients.get(uid);
|
||||
if (c == null)
|
||||
continue;
|
||||
Message msg = Message.obtain();
|
||||
msg.what = MSG_STOP;
|
||||
msg.arg1 = isDaemon ? 1 : 0;
|
||||
msg.obj = name;
|
||||
try {
|
||||
c.send(m);
|
||||
c.m.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Utils.err(TAG, e);
|
||||
} finally {
|
||||
m.recycle();
|
||||
msg.recycle();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -213,51 +228,51 @@ public class RootServiceServer extends IRootServiceManager.Stub implements IBind
|
||||
activeServices.put(service.getComponentName(), c);
|
||||
}
|
||||
|
||||
private IBinder bindInternal(Intent intent) throws Exception {
|
||||
private IBinder bindInternal(int uid, Intent intent) throws Exception {
|
||||
ClientProcess c = clients.get(uid);
|
||||
if (c == null)
|
||||
return null;
|
||||
|
||||
ComponentName name = intent.getComponent();
|
||||
|
||||
ServiceContainer c = activeServices.get(name);
|
||||
if (c == null) {
|
||||
ServiceContainer s = activeServices.get(name);
|
||||
if (s == null) {
|
||||
Class<?> clz = Class.forName(name.getClassName());
|
||||
Constructor<?> ctor = clz.getDeclaredConstructor();
|
||||
ctor.setAccessible(true);
|
||||
attachBaseContext.invoke(ctor.newInstance(), context);
|
||||
|
||||
// RootService should be registered after attachBaseContext
|
||||
c = activeServices.get(name);
|
||||
if (c == null) {
|
||||
s = activeServices.get(name);
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (c.binder != null) {
|
||||
if (s.binder != null) {
|
||||
Utils.log(TAG, name.getClassName() + " rebind");
|
||||
c.service.onRebind(c.intent);
|
||||
if (s.rebind)
|
||||
s.service.onRebind(s.intent);
|
||||
} else {
|
||||
Utils.log(TAG, name.getClassName() + " bind");
|
||||
c.binder = c.service.onBind(intent);
|
||||
c.intent = intent.cloneFilter();
|
||||
s.binder = s.service.onBind(intent);
|
||||
s.intent = intent.cloneFilter();
|
||||
}
|
||||
s.users.add(uid);
|
||||
|
||||
return c.binder;
|
||||
return s.binder;
|
||||
}
|
||||
|
||||
private void setAsDaemon() {
|
||||
if (!isDaemon) {
|
||||
// Register ourselves as system service
|
||||
HiddenAPIs.addService(getServiceName(context.getPackageName()), this);
|
||||
isDaemon = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void stopService(ComponentName className, boolean force) {
|
||||
ServiceContainer c = activeServices.get(className);
|
||||
if (c != null) {
|
||||
if (!c.service.onUnbind(c.intent) || force) {
|
||||
c.service.onDestroy();
|
||||
activeServices.remove(className);
|
||||
} else {
|
||||
setAsDaemon();
|
||||
private void stopService(int uid, ComponentName name, boolean destroy) {
|
||||
ServiceContainer s = activeServices.get(name);
|
||||
if (s != null) {
|
||||
s.users.remove(uid);
|
||||
if (s.users.isEmpty()) {
|
||||
s.rebind = s.service.onUnbind(s.intent);
|
||||
if (destroy || !isDaemon) {
|
||||
s.service.onDestroy();
|
||||
activeServices.remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (activeServices.isEmpty()) {
|
||||
@ -266,19 +281,25 @@ public class RootServiceServer extends IRootServiceManager.Stub implements IBind
|
||||
}
|
||||
}
|
||||
|
||||
private void stopAllService(boolean force) {
|
||||
private void stopServices(int uid) {
|
||||
Iterator<Map.Entry<ComponentName, ServiceContainer>> it =
|
||||
activeServices.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
ServiceContainer c = it.next().getValue();
|
||||
if (!c.service.onUnbind(c.intent) || force) {
|
||||
c.service.onDestroy();
|
||||
it.remove();
|
||||
} else {
|
||||
setAsDaemon();
|
||||
ServiceContainer s = it.next().getValue();
|
||||
if (uid < 0)
|
||||
s.users.clear();
|
||||
else
|
||||
s.users.remove(uid);
|
||||
|
||||
if (s.users.isEmpty()) {
|
||||
s.rebind = s.service.onUnbind(s.intent);
|
||||
if (uid < 0 || !isDaemon) {
|
||||
s.service.onDestroy();
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (force || activeServices.isEmpty()) {
|
||||
if (activeServices.isEmpty()) {
|
||||
// Terminate root process
|
||||
System.exit(0);
|
||||
}
|
||||
@ -300,15 +321,37 @@ public class RootServiceServer extends IRootServiceManager.Stub implements IBind
|
||||
if (event == DELETE_SELF || name.equals(path)) {
|
||||
UiThreadHandler.run(() -> {
|
||||
Utils.log(TAG, "App updated, terminate");
|
||||
stopAllService(true);
|
||||
stopServices(-1);
|
||||
System.exit(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ClientProcess extends BinderHolder {
|
||||
|
||||
final Messenger m;
|
||||
final int uid;
|
||||
|
||||
ClientProcess(IBinder b) throws RemoteException {
|
||||
super(b);
|
||||
m = new Messenger(b);
|
||||
uid = getCallingUid();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBinderDied() {
|
||||
Utils.log(TAG, "Client process terminated");
|
||||
clients.remove(uid);
|
||||
stopServices(uid);
|
||||
}
|
||||
}
|
||||
|
||||
static class ServiceContainer {
|
||||
RootService service;
|
||||
Intent intent;
|
||||
IBinder binder;
|
||||
boolean rebind;
|
||||
final Set<Integer> users = newArraySet();
|
||||
}
|
||||
}
|
||||
|
@ -80,6 +80,16 @@ import java.util.concurrent.Executor;
|
||||
*/
|
||||
public abstract class RootService extends ContextWrapper {
|
||||
|
||||
/**
|
||||
* Launch the service in "Daemon Mode".
|
||||
* <p>
|
||||
* Add this category in the intents passed to {@link #bind(Intent, ServiceConnection)},
|
||||
* {@link #bind(Intent, Executor, ServiceConnection)}, or
|
||||
* {@link #createBindTask(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.
|
||||
* @param intent identifies the service to connect to.
|
||||
|
Loading…
Reference in New Issue
Block a user