3636 * Class with utility function sto launch Java code running as a root as a daemon
3737 *
3838 * @see #getLaunchScript(Context, Class, String, String, String[], String)
39- * @see #daemonize(String, int)
39+ * @see #daemonize(String, int, boolean, OnExitListener )
4040 * @see #run()
4141 * @see #exit()
4242 */
@@ -122,6 +122,14 @@ public static List<String> getLaunchScript(Context context, Class<?> clazz, Stri
122122 /** Registered interfaces */
123123 private static final List <RootIPC > ipcs = new ArrayList <RootIPC >();
124124
125+ /** Called before termination */
126+ public interface OnExitListener {
127+ void onExit ();
128+ }
129+
130+ /** Stored by daemonize(), called by exit() */
131+ private static volatile OnExitListener onExitListener = null ;
132+
125133 /**
126134 * Makes sure there is only a single daemon running with this code parameter. This should
127135 * be one of the first calls in your process to be run as daemon, just after setting up logging
@@ -145,9 +153,11 @@ public static List<String> getLaunchScript(Context context, Class<?> clazz, Stri
145153 *
146154 * @param packageName Package name of the app. BuildConfig.APPLICATION_ID can generally be used.
147155 * @param code User-value, should be unique per daemon process
156+ * @param surviveFrameworkRestart If false (recommended), automatically terminate if the Android framework restarts
157+ * @param exitListener Callback called before the daemon exists either due to a newer daemon version being started or {@link #exit()} being called, or null
148158 */
149159 @ SuppressLint ("PrivateApi" )
150- public static void daemonize (String packageName , int code ) {
160+ public static void daemonize (String packageName , int code , boolean surviveFrameworkRestart , OnExitListener exitListener ) {
151161 String id = packageName + "#" + String .valueOf (code ) + "#daemonize" ;
152162
153163 File apk = new File (System .getenv ("CLASSPATH" ));
@@ -178,7 +188,34 @@ public static void daemonize(String packageName, int code) {
178188 }
179189
180190 // If we reach this, there either was no previous daemon, or it was outdated
181- Logger .ep (LOG_PREFIX , "Installing service" );
191+ Logger .dp (LOG_PREFIX , "Installing service" );
192+ onExitListener = exitListener ;
193+
194+ if (!surviveFrameworkRestart ) {
195+ /* We link to Android's activity service, which lives in system_server. If the
196+ framework is restarted, for example through stop/start in a root shell, this
197+ service will die and we will be notified.
198+
199+ Obviously when setting surviveFrameworkRestart to true, things you do in
200+ your own code may still cause this process to terminate when the framework
201+ dies, we're just not doing it automatically. */
202+
203+ IBinder activityService = (IBinder )mGetService .invoke (null , Context .ACTIVITY_SERVICE );
204+ if (activityService != null ) {
205+ try {
206+ activityService .linkToDeath (new IBinder .DeathRecipient () {
207+ @ Override
208+ public void binderDied () {
209+ exit ();
210+ }
211+ }, 0 );
212+ } catch (RemoteException e ) {
213+ // already dead
214+ exit ();
215+ }
216+ }
217+ }
218+
182219 mAddService .invoke (null , id , new IRootDaemonIPC .Stub () {
183220 @ Override
184221 public String getVersion () {
@@ -211,7 +248,7 @@ public void broadcast() {
211248 * <br>
212249 * Use this method instead of librootjava's 'new RootIPC()' constructor when running as daemon.
213250 *
214- * @param packageName Package name of the app. Use the same value as used when calling {@link #daemonize(String, int)}.
251+ * @param packageName Package name of the app. Use the same value as used when calling {@link #daemonize(String, int, boolean, OnExitListener )}.
215252 * @param ipc Binder object to wrap and send out
216253 * @param code User-value, should be unique per Binder
217254 * @return RootIPC instance
@@ -239,7 +276,8 @@ public static RootIPC register(String packageName, IBinder ipc, int code) {
239276 * the main() implementation. The initial Binder broadcasts and the connections themselves
240277 * are handled in background threads created by the RootIPC instances created when
241278 * {@link #register(String, IBinder, int)} is called, and re-broadcasting those interfaces
242- * is done by the internal Binder interface registered by {@link #daemonize(String, int)}.<br>
279+ * is done by the internal Binder interface registered by
280+ * {@link #daemonize(String, int, boolean, OnExitListener)}.<br>
243281 * <br>
244282 * This method never returns!
245283 */
@@ -263,15 +301,23 @@ public static void run() {
263301 * RemoteException on the other end.
264302 */
265303 public static void exit () {
304+ /* We do not return from the run() call but immediately exit, so if this method is called
305+ from inside a Binder interface method implementation, the process has died before the
306+ IPC call to terminate completes on the other end. This triggers a RemoteException so the
307+ other end can easily verify this process has terminated. It also prevents a
308+ race-condition between the old service dying and new service registering. Additionally it
309+ saves us from having to use another synchronizer to cope with a termination request
310+ coming in from another daemon launch before run() is actually called. */
311+
266312 Logger .dp (LOG_PREFIX , "Exiting" );
267313
268- /* We do not return from the run() call but immediately exit, so the process has died
269- before the IPC call to terminate completes on the other end. This triggers a
270- RemoteException so the other end can easily verify this process has terminated. It
271- also prevents a race-condition between the old service dying and new service registering.
272- Additionally it saves us from having to use another synchronizer to cope with a
273- termination request coming in from another daemon launch before run() is actually
274- called. */
314+ try {
315+ if ( onExitListener != null ) {
316+ onExitListener . onExit ();
317+ }
318+ } catch ( Exception e ) {
319+ Logger . ex ( e );
320+ }
275321
276322 try {
277323 /* Unlike when using RootJava.getLaunchScript(), RootDaemon.getLaunchScript() does
0 commit comments