diff --git a/termux/termux-app/src/main/java/com/termux/app/TermuxService.java b/termux/termux-app/src/main/java/com/termux/app/TermuxService.java index 9b5d11d2d0..b08aac6b8e 100644 --- a/termux/termux-app/src/main/java/com/termux/app/TermuxService.java +++ b/termux/termux-app/src/main/java/com/termux/app/TermuxService.java @@ -122,7 +122,10 @@ public void onCreate() { // load and TermuxActivity handles reloads mProperties = TermuxAppSharedProperties.getProperties(); - mShellManager = TermuxShellManager.getShellManager(); + // Use init() instead of getShellManager() so the singleton is lazily created if the + // OS auto-restarted the service after process death (TermuxApplication.onCreate did not + // run, leaving the static singleton null and causing an NPE in buildNotification()). + mShellManager = TermuxShellManager.init(getApplicationContext()); runStartForeground(); diff --git a/termux/termux-app/src/test/java/com/termux/app/TermuxServiceShellManagerNpeTest.java b/termux/termux-app/src/test/java/com/termux/app/TermuxServiceShellManagerNpeTest.java new file mode 100644 index 0000000000..c9c9ad8ca9 --- /dev/null +++ b/termux/termux-app/src/test/java/com/termux/app/TermuxServiceShellManagerNpeTest.java @@ -0,0 +1,69 @@ +package com.termux.app; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import com.termux.shared.termux.shell.TermuxShellManager; + +import java.lang.reflect.Field; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.android.controller.ServiceController; + +/** + * Repro for ADFA-4330: when the OS auto-restarts {@link TermuxService} after process death, + * {@code TermuxApplication.onCreate()} does NOT run, so the static + * {@link TermuxShellManager} singleton is still {@code null}. + * + *
On the pre-fix baseline, {@code TermuxService.onCreate()} assigned + * {@code mShellManager = TermuxShellManager.getShellManager()} (which returns the null + * singleton), then immediately called {@code runStartForeground() -> buildNotification() -> + * getTermuxSessionsSize()}, dereferencing the null {@code mShellManager} and throwing a + * {@link NullPointerException}. + * + *
The fix changes that assignment to {@code TermuxShellManager.init(applicationContext)}, + * which lazily creates the singleton, so the service starts cleanly. + * + *
This test simulates the auto-restart by forcing the static singleton back to {@code null}
+ * before creating the service, then asserts the service comes up and
+ * {@link TermuxService#getTermuxSessionsSize()} returns 0 instead of NPE-ing.
+ */
+@RunWith(RobolectricTestRunner.class)
+public class TermuxServiceShellManagerNpeTest {
+
+ /**
+ * Reset the static singleton to null to mimic a fresh process where
+ * TermuxApplication.onCreate() (which would normally call init()) never ran.
+ */
+ @Before
+ public void clearShellManagerSingleton() throws Exception {
+ Field f = TermuxShellManager.class.getDeclaredField("shellManager");
+ f.setAccessible(true);
+ f.set(null, null);
+ }
+
+ /** Service onCreate() with a null shell-manager singleton must not NPE and must expose usable sessions. */
+ @Test
+ public void onCreateWithNullSingleton_doesNotNpe_andSessionsAreUsable() {
+ // Sanity: the auto-restart precondition — singleton is null going in.
+ assertEquals(null, TermuxShellManager.getShellManager());
+
+ // Drive the REAL service lifecycle. On stage this throws NullPointerException inside
+ // onCreate() -> runStartForeground() -> buildNotification() -> getTermuxSessionsSize().
+ ServiceController