Skip to content

ADFA-4330: Fix TermuxService shellManager NPE on OS service auto-restart#1413

Merged
Daniel-ADFA merged 4 commits into
stagefrom
ADFA-4330-termuxservice-shellmanager-npe
Jun 25, 2026
Merged

ADFA-4330: Fix TermuxService shellManager NPE on OS service auto-restart#1413
Daniel-ADFA merged 4 commits into
stagefrom
ADFA-4330-termuxservice-shellmanager-npe

Conversation

@fryanpan

@fryanpan fryanpan commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Jira Ticket: https://appdevforall.atlassian.net/browse/ADFA-4330
Sentry Issue: https://appdevforall-inc-9p.sentry.io/issues/APPDEVFORALL-BR

Reproduction Details

When the OS auto-restarts TermuxService after the app process was killed, TermuxApplication.onCreate() (which creates the TermuxShellManager singleton) never runs, so TermuxService.onCreate() reads a null shellManager and NPEs while building its foreground notification.

Stack Trace

RuntimeException: Unable to create service com.termux.app.TermuxService:
  java.lang.NullPointerException: Attempt to read from field
  'java.util.List com.termux.shared.termux.shell.TermuxShellManager.mTermuxSessions' on a null object reference
    at com.termux.app.TermuxService.getTermuxSessionsSize(TermuxService.java:889)
    at com.termux.app.TermuxService.buildNotification(TermuxService.java:802)
    at com.termux.app.TermuxService.runStartForeground(TermuxService.java:213)
    at com.termux.app.TermuxService.onCreate(TermuxService.java:127)

User Steps

User steps leading up to crash, based on Sentry breadcrumbs:

  • None — the crashing event has no UI breadcrumbs. This is an OS-initiated service restart (sticky service recreated after the app was killed in the background), not a user action.

Was able to reproduce in a unit test?

Yes.
TermuxServiceShellManagerNpeTest (:termux:termux-app, Robolectric) forces the static TermuxShellManager singleton to null (simulating the no-Application-init restart), drives the real TermuxService.onCreate() via ServiceController, and asserts getTermuxSessionsSize() returns 0 without NPE. Baseline: FAILS with the exact NPE path; branch: passes.

What Was Fixed

onCreate() uses TermuxShellManager.init(getApplicationContext()) (lazily creates the singleton) instead of getShellManager() (returns null when Application init never ran).

Testing

:termux:termux-app:testV8DebugUnitTest → green (red on baseline). Local CodeRabbit review: no findings. Reviewer note (local): TermuxShellManager.init() is unsynchronized while sibling mutators are synchronized — the new service path slightly widens a pre-existing init race; consider synchronizing init(). Add an @After resetting the static singleton for test hygiene.


Fixes APPDEVFORALL-BR

@fryanpan fryanpan marked this pull request as ready for review June 19, 2026 11:22
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 401f455e-40d9-4acd-b6be-3d06006eb80f

📥 Commits

Reviewing files that changed from the base of the PR and between 665eb9f and 945c85f.

📒 Files selected for processing (2)
  • termux/termux-app/src/main/java/com/termux/app/TermuxService.java
  • termux/termux-app/src/test/java/com/termux/app/TermuxServiceShellManagerNpeTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • termux/termux-app/src/main/java/com/termux/app/TermuxService.java
  • termux/termux-app/src/test/java/com/termux/app/TermuxServiceShellManagerNpeTest.java

📝 Walkthrough
  • Fixed a crash in TermuxService.onCreate() by lazily initializing TermuxShellManager with TermuxShellManager.init(getApplicationContext()) instead of assuming the singleton already exists.
  • Added a Robolectric regression test that reproduces the OS auto-restart path where TermuxApplication.onCreate() has not run, verifies the service no longer throws an NPE, and confirms sessions remain usable.
  • Risk: this change alters service startup behavior to initialize shared state on-demand; while intended, it should be monitored for any side effects during restart or low-memory recovery paths.

Walkthrough

TermuxService.onCreate() now initializes TermuxShellManager with init(getApplicationContext()) instead of reading the singleton directly, and a Robolectric test covers startup when the singleton starts as null.

Changes

TermuxShellManager NPE fix and regression test

Layer / File(s) Summary
TermuxService.onCreate lazy-init of TermuxShellManager
termux/termux-app/src/main/java/com/termux/app/TermuxService.java
Replaces TermuxShellManager.getShellManager() with TermuxShellManager.init(getApplicationContext()) and updates the comment about lazy singleton initialization after process restart.
Robolectric regression test for null singleton scenario
termux/termux-app/src/test/java/com/termux/app/TermuxServiceShellManagerNpeTest.java
Adds a Robolectric test class with a reflection-based @Before reset of TermuxShellManager.shellManager and a test that creates TermuxService, verifies the singleton is initialized, and checks getTermuxSessionsSize() returns 0.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested reviewers

  • Daniel-ADFA

Poem

🐇 A sleepy singleton woke up at last,
After restart-time shadows and crashes had passed.
init() gave it life, and the tests held the key,
Now Termux hops onward, NPE-free!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: a TermuxService shellManager NPE during OS auto-restart.
Description check ✅ Passed The description is directly related to the changes and accurately explains the crash, fix, and test coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ADFA-4330-termuxservice-shellmanager-npe

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
termux/termux-app/src/test/java/com/termux/app/TermuxServiceShellManagerNpeTest.java (1)

42-47: ⚡ Quick win

Add an @After method to ensure proper cleanup even if the test fails.

The @Before hook correctly resets the singleton, but if the test fails before line 67, controller.destroy() won't execute, potentially leaving resources uncleaned. Add an @After method to guarantee cleanup runs after every test.

♻️ Proposed refactor to add `@After` cleanup
+    private ServiceController<TermuxService> controller;
+
     /**
      * 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);
     }
 
+    `@After`
+    public void cleanup() {
+        if (controller != null) {
+            controller.destroy();
+            controller = 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<TermuxService> controller =
+        controller =
             Robolectric.buildService(TermuxService.class).create();
         TermuxService service = controller.get();
 
         // After a clean onCreate(), the shell manager must be wired up and queryable.
         assertNotNull("mShellManager must be initialized after onCreate()",
             TermuxShellManager.getShellManager());
         assertEquals("A freshly created service manages zero sessions",
             0, service.getTermuxSessionsSize());
-
-        controller.destroy();
     }

Also applies to: 67-67

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@termux/termux-app/src/test/java/com/termux/app/TermuxServiceShellManagerNpeTest.java`
around lines 42 - 47, Add an `@After` method to guarantee resource cleanup after
every test execution, regardless of whether the test passes or fails. This
method should contain the same cleanup logic that is currently at line 67
(controller.destroy()), ensuring that resources are properly released even if
the test throws an exception before reaching that line. This complements the
existing `@Before` method clearShellManagerSingleton() to provide comprehensive
test lifecycle management.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@termux/termux-app/src/main/java/com/termux/app/TermuxService.java`:
- Around line 125-128: The TermuxShellManager.init() method lacks
synchronization, creating a race condition where multiple threads could bypass
the null check and create duplicate singleton instances. Add the synchronized
keyword to the init() method declaration to ensure thread-safe access to the
singleton creation logic. This will prevent multiple threads from concurrently
executing the method and creating competing instances of the TermuxShellManager.

---

Nitpick comments:
In
`@termux/termux-app/src/test/java/com/termux/app/TermuxServiceShellManagerNpeTest.java`:
- Around line 42-47: Add an `@After` method to guarantee resource cleanup after
every test execution, regardless of whether the test passes or fails. This
method should contain the same cleanup logic that is currently at line 67
(controller.destroy()), ensuring that resources are properly released even if
the test throws an exception before reaching that line. This complements the
existing `@Before` method clearShellManagerSingleton() to provide comprehensive
test lifecycle management.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d6111333-1495-4bac-b5e3-5dbf392c89f1

📥 Commits

Reviewing files that changed from the base of the PR and between 8082c92 and 665eb9f.

📒 Files selected for processing (2)
  • termux/termux-app/src/main/java/com/termux/app/TermuxService.java
  • termux/termux-app/src/test/java/com/termux/app/TermuxServiceShellManagerNpeTest.java

Comment thread termux/termux-app/src/main/java/com/termux/app/TermuxService.java
fryanpan and others added 3 commits June 19, 2026 09:58
Sentry APPDEVFORALL-BR. Use TermuxShellManager.init() instead of
getShellManager() so the singleton exists after OS service restart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n auto-restart

Reproduces the OS service auto-restart scenario where TermuxApplication.onCreate()
never ran, leaving the static TermuxShellManager singleton null. Drives the real
TermuxService.onCreate() via Robolectric; on the pre-fix baseline this NPEs in
onCreate -> runStartForeground -> buildNotification -> getTermuxSessionsSize when
mShellManager is null. Passes on the fix (init() lazily creates the singleton).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@fryanpan fryanpan force-pushed the ADFA-4330-termuxservice-shellmanager-npe branch from 665eb9f to a495d61 Compare June 19, 2026 16:58
@Daniel-ADFA Daniel-ADFA merged commit eabb240 into stage Jun 25, 2026
2 checks passed
@Daniel-ADFA Daniel-ADFA deleted the ADFA-4330-termuxservice-shellmanager-npe branch June 25, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants