Skip to content

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

Open
fryanpan wants to merge 3 commits into
stagefrom
ADFA-4330-termuxservice-shellmanager-npe
Open

ADFA-4330: Fix TermuxService shellManager NPE on OS service auto-restart#1413
fryanpan wants to merge 3 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

Warning

Review limit reached

@fryanpan, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 27 minutes and 29 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7d004f88-3f17-4d2d-80ce-2db791672547

📥 Commits

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

📒 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
📝 Walkthrough

Walkthrough

TermuxService.onCreate() now calls TermuxShellManager.init(getApplicationContext()) instead of getShellManager() to ensure the singleton is lazily initialized after Android process auto-restart, preventing an NPE. A Robolectric regression test validates this by nulling the singleton via reflection before service creation.

Changes

TermuxShellManager NPE fix and regression test

Layer / File(s) Summary
TermuxService.onCreate lazy-init
termux/termux-app/src/main/java/com/termux/app/TermuxService.java
Replaces TermuxShellManager.getShellManager() with TermuxShellManager.init(getApplicationContext()) to lazily create the singleton when TermuxApplication.onCreate() has not run due to process auto-restart.
Robolectric regression test
termux/termux-app/src/test/java/com/termux/app/TermuxServiceShellManagerNpeTest.java
Adds TermuxServiceShellManagerNpeTest with a @Before hook that nulls out the static TermuxShellManager.shellManager field via reflection, and a @Test that creates TermuxService via Robolectric, then asserts the singleton is non-null and getTermuxSessionsSize() returns 0 without NPE.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested reviewers

  • Daniel-ADFA

Poem

🐇 A singleton slept through the restart, you see,
Its null caused a crash in the shell-manager tree.
Now init() stands guard when the Application won't run,
And a Robolectric test checks it's properly done.
No more NPE hops — the bunny says, "Fixed! ✨"

🚥 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 identifies the specific bug being fixed (TermuxService shellManager NPE) and its context (OS service auto-restart), directly corresponding to the main changeset.
Description check ✅ Passed The description comprehensively explains the issue, provides stack traces, reproduction steps, testing approach, and implementation details, all directly related to the changeset.
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 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 and usage tips.

@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 on lines +125 to +128
// 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());

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify that TermuxShellManager.init() is synchronized

# Search for the init method and surrounding lines to check for synchronized keyword
rg -A 5 "public static .*init\(" termux/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellManager.java

Repository: appdevforall/CodeOnTheGo

Length of output: 268


Add synchronization to TermuxShellManager.init() to prevent race conditions.

The init() method in TermuxShellManager is unsynchronized and exhibits a classic double-checked locking anti-pattern. If onCreate() is called concurrently (or from different lifecycle paths), multiple threads could pass the if (shellManager == null) check and create duplicate instances. Add synchronized to the method declaration or use volatile with a proper synchronized block:

public static synchronized TermuxShellManager init(`@NonNull` Context context) {
    if (shellManager == null)
        shellManager = new TermuxShellManager(context);
    return shellManager;
}
🤖 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/main/java/com/termux/app/TermuxService.java` around
lines 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.

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
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.

2 participants