Skip to content

ADFA-2829 | Move initialization tasks to background threads#940

Merged
jatezzz merged 5 commits intostagefrom
fix/ADFA-2829-termux-executor-threading
Feb 12, 2026
Merged

ADFA-2829 | Move initialization tasks to background threads#940
jatezzz merged 5 commits intostagefrom
fix/ADFA-2829-termux-executor-threading

Conversation

@jatezzz
Copy link
Collaborator

@jatezzz jatezzz commented Feb 6, 2026

Description

This PR optimizes the application startup and lifecycle handling by moving heavy initialization tasks and disk I/O operations off the main thread.

Key Changes:

  • TermuxExecutor: Introduced a helper class to manage background execution and main thread callbacks.
  • TermuxActivity: Moved JNI loading, Shared Preferences loading, and crash log checking to background threads. Implemented a callback mechanism to finalize UI setup (completeOnCreate) once preferences are loaded.
  • TerminalSession: Explicitly provided the Main Looper to the handler to ensure compatibility and prevent strict mode violations.
  • File I/O: Moved font/color property loading and directory creation (TerminalActivity) to background threads.
  • Synchronization: Added synchronization to TermuxSharedProperties to ensure thread safety during asynchronous loading.

Details

Before changes

Screen.Recording.2026-02-06.at.5.02.34.PM.mov
Screenshot 2026-02-06 at 5 02 14 PM

After changes

Screen.Recording.2026-02-06.at.4.50.53.PM.mov
Screenshot 2026-02-06 at 4 50 34 PM

Ticket

ADFA-2829

Observation

This change significantly reduces the work done on the main thread during onCreate and onResume. Please verify that the terminal initializes correctly and that no race conditions occur during the first run or configuration changes.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors startup and threading: adds a single-thread background executor, offloads disk/JNI/font I/O to background threads, marshals UI updates back to the main thread, tightens lifecycle guards, binds a handler to an explicit Looper, and converts one directory creation to a coroutine on Dispatchers.IO.

Changes

Cohort / File(s) Summary
Executor utility
termux/termux-shared/src/main/java/com/termux/shared/termux/TermuxExecutor.java
Adds TermuxExecutor with executeInBackground(), executeOnMain(), and execute() to centralize background and main-thread task orchestration.
Activity startup & lifecycle
termux/termux-app/src/main/java/com/termux/app/TermuxActivity.java
Loads preferences and preloads JNI on a background thread, introduces completeOnCreate(Bundle), adds lifecycle guards, and uses TermuxExecutor to marshal background↔main work.
Terminal Activity coroutine
termux/termux-app/src/main/java/com/itsaky/androidide/activities/TerminalActivity.kt
Wraps Environment.mkdirIfNotExists(Environment.TMP_DIR) in a lifecycleScope coroutine on Dispatchers.IO, making directory creation asynchronous.
Session client font/color I/O
termux/termux-app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java
Moves font/color loading to background via TermuxExecutor.executeInBackground() and applies UI updates via executeOnMain(). Replaces direct AlertDialog/UI calls with main-thread marshaling. Adds overload addNewSession(boolean,String).
Handler & properties concurrency
termux/termux-emulator/src/main/java/com/termux/terminal/TerminalSession.java, termux/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxSharedProperties.java
Binds main handler explicitly to Looper.getMainLooper(); adds corresponding constructor. Makes mSharedProperties volatile, changes property-loading to build local object then assign inside synchronized block, and marks getInternalPropertyValue() synchronized.

Sequence Diagram(s)

sequenceDiagram
    participant Activity as TermuxActivity
    participant Executor as TermuxExecutor
    participant Main as MainThread
    participant Disk as Disk/Preferences
    participant JNI as JNI Loader

    Activity->>Executor: executeInBackground(load prefs + preload JNI)
    Executor->>Disk: read preferences from disk
    Executor->>JNI: Class.forName("com.termux.terminal.JNI")
    Executor-->>Main: executeOnMain(completeOnCreate(savedInstanceState))
    Main->>Activity: completeOnCreate(...)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • Daniel-ADFA

Poem

🐰
I hop through threads where JNI and prefs play,
I stash a temp dir in Dispatchers.IO today.
Executor hums, I send UI back to light,
Fonts and colors load by moon and then by bright. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective of the changeset: moving initialization tasks to background threads.
Description check ✅ Passed The description comprehensively explains the changes, key modifications, and provides before/after performance comparisons with supporting evidence.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/ADFA-2829-termux-executor-threading

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
termux/termux-app/src/main/java/com/termux/app/TermuxActivity.java (1)

246-307: ⚠️ Potential issue | 🟠 Major

completeOnCreate replays onStart logic but not onResume — lifecycle callbacks may be permanently skipped.

When the background preferences load is slow, the lifecycle proceeds: onStartonResume → both return early because mPreferences == null. When completeOnCreate finally runs on the main thread, it replays onStart logic (Lines 302-306) via the if (mIsVisible) guard, but onResume logic is never replayed. This means:

  • mTermuxTerminalSessionActivityClient.onResume() (bell sound pool setup) is skipped.
  • mTermuxTerminalViewClient.onResume() is skipped.
  • Crash notification check is skipped.
  • feedbackButtonManager.loadFabPosition() is skipped.

These will only run the next time the activity is resumed (e.g., after switching away and back), which may never happen.

Proposed fix — also replay onResume logic in completeOnCreate
     if (mIsVisible) {
          if (mTermuxTerminalSessionActivityClient != null) mTermuxTerminalSessionActivityClient.onStart();
          if (mTermuxTerminalViewClient != null) mTermuxTerminalViewClient.onStart();
          if (mPreferences.isTerminalMarginAdjustmentEnabled()) addTermuxActivityRootViewGlobalLayoutListener();
+
+         // Replay onResume logic that was skipped while mPreferences was null
+         if (mTermuxTerminalSessionActivityClient != null) mTermuxTerminalSessionActivityClient.onResume();
+         if (mTermuxTerminalViewClient != null) mTermuxTerminalViewClient.onResume();
+         TermuxExecutor.executeInBackground(() -> TermuxCrashUtils.notifyAppCrashFromCrashLogFile(this, LOG_TAG));
+         if (feedbackButtonManager != null) feedbackButtonManager.loadFabPosition();
     }
🤖 Fix all issues with AI agents
In
`@termux/termux-app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java`:
- Around line 495-523: The background task mutates shared UI/state on a worker
thread; move all color and UI mutations into the main-thread callback: keep file
I/O and props loading in the TermuxExecutor.executeInBackground lambda (load
Properties and determine newTypeface), then inside TermuxExecutor.executeOnMain
run TerminalColors.COLOR_SCHEME.updateWith(props),
session.getEmulator().mColors.reset() (guard nulls), call
updateBackgroundColor(), and set the typeface via
mActivity.getTerminalView().setTypeface(newTypeface); preserve the existing
null/finishing checks and exception handling around these operations.

In `@termux/termux-app/src/main/java/com/termux/app/TermuxActivity.java`:
- Around line 226-244: The savedInstanceState Bundle is captured and used
asynchronously which risks it being recycled; before calling
TermuxExecutor.executeInBackground extract whatever values completeOnCreate
needs (or create a lightweight copy/serializable Map) from savedInstanceState on
the current thread, then pass those extracted values into the lambda and finally
call completeOnCreate with the extracted data on the main thread; update
references around TermuxExecutor.executeInBackground, completeOnCreate, and the
savedInstanceState capture so mPreferences/mIsInvalidState logic remains the
same but no longer relies on the original Bundle after the async gap.
- Around line 697-703: onCreateNewSession is dispatching addNewSession to a
background thread (via TermuxExecutor.executeInBackground) but addNewSession (in
TermuxTerminalSessionActivityClient) performs UI work; move that call to the
main thread instead — either remove TermuxExecutor.executeInBackground and call
mTermuxTerminalSessionActivityClient.addNewSession(...) directly, or replace
executeInBackground with the main-thread helper (e.g.
TermuxExecutor.executeOnMain) so the entire addNewSession body runs on the UI
thread.

In
`@termux/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxSharedProperties.java`:
- Around line 46-60: The field mSharedProperties can be updated inside
loadTermuxPropertiesFromDisk but is read by getProperties, getPropertyValue, and
getInternalProperties without synchronization, allowing readers to see a stale
reference; mark the field mSharedProperties as volatile to establish the
required happens-before visibility so updates made in
loadTermuxPropertiesFromDisk are visible to unsynchronized readers (leave
getInternalPropertyValue's synchronized usage as-is); update the declaration of
mSharedProperties accordingly.

In
`@termux/termux-shared/src/main/java/com/termux/shared/termux/TermuxExecutor.java`:
- Around line 20-30: The execute method in TermuxExecutor currently lacks a
null-check for the backgroundTask parameter; update TermuxExecutor.execute to
validate backgroundTask before submitting to backgroundExecutor (throw
IllegalArgumentException or return early) and ensure you still post
mainThreadCallback via mainHandler in the finally block only if
mainThreadCallback != null; reference the execute(...) method, the
backgroundTask and mainThreadCallback parameters, and the
backgroundExecutor/mainHandler usage when locating and fixing the code.
🧹 Nitpick comments (4)
termux/termux-shared/src/main/java/com/termux/shared/termux/TermuxExecutor.java (1)

9-9: Single-thread executor serializes all background work across the app.

Multiple callers submit independent tasks (JNI preload, preferences loading, properties loading, crash-log checking, font/color loading, session creation). With a single-thread executor, all of these are serialized — a slow JNI Class.forName will delay preferences loading, which in turn delays completeOnCreate. Consider using a small fixed-size thread pool (e.g., Executors.newFixedThreadPool(2)) or at least a cached thread pool so truly independent tasks can overlap.

termux/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxSharedProperties.java (1)

54-59: Move logging calls outside the synchronized block.

dumpPropertiesToLog() and dumpInternalPropertiesToLog() perform potentially slow I/O-backed logging while holding the intrinsic lock. Since the field assignment is already complete by line 56, move the dumps outside the synchronized block to reduce lock contention.

Proposed fix
     synchronized (this) {
         mPropertiesFile = propertiesFile;
         mSharedProperties = loadedProperties;
-        dumpPropertiesToLog();
-        dumpInternalPropertiesToLog();
     }
+    dumpPropertiesToLog();
+    dumpInternalPropertiesToLog();
termux/termux-app/src/main/java/com/termux/app/TermuxActivity.java (2)

548-555: reloadProperties fires a background reload that is redundant with the constructor's synchronous load.

mProperties is initialized at Line 217 via TermuxAppSharedProperties.getProperties(), and the constructor of TermuxSharedProperties already calls loadTermuxPropertiesFromDisk() synchronously (Line 40 of TermuxSharedProperties.java). Then reloadProperties() on Line 218 immediately queues another background load. The first background load is redundant.

Also, the main-thread callback mTermuxTerminalViewClient.onReloadProperties() is guarded by a null check, but since mTermuxTerminalViewClient is set later in completeOnCreate (via setTermuxTerminalViewAndClients()), this callback will always be a no-op during the initial call from onCreate. Consider whether reloadProperties() should only be called when an actual reload is needed (e.g., from reloadActivityStyling).


608-618: executeOnMain always re-posts even when already on the main thread — introduces unnecessary asynchrony.

setCurrentSession and onResetTerminalSession are wrapped with TermuxExecutor.executeOnMain(...), meaning they are always deferred to the next main-thread message loop iteration, even when called from the main thread. This changes the execution from synchronous to asynchronous, which can introduce subtle ordering issues (e.g., a session switch not yet visible when the next line of the caller executes).

Consider posting only when not already on the main thread:

Proposed conditional dispatch
+private static void runOnMainThread(Runnable task) {
+    if (Looper.myLooper() == Looper.getMainLooper()) {
+        task.run();
+    } else {
+        TermuxExecutor.executeOnMain(task);
+    }
+}
+
 `@Override`
 public void setCurrentSession(TerminalSession session) {
-    TermuxExecutor.executeOnMain(() -> super.setCurrentSession(session));
+    runOnMainThread(() -> super.setCurrentSession(session));
 }

@jatezzz jatezzz force-pushed the fix/ADFA-2829-termux-executor-threading branch from a930c08 to 0b8e569 Compare February 9, 2026 13:14
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@termux/termux-app/src/main/java/com/termux/app/TermuxActivity.java`:
- Around line 547-556: reloadActivityStyling currently calls reloadProperties()
which performs asynchronous disk I/O via TermuxExecutor.execute, then
immediately reads mProperties on the main thread causing a race and stale
values; change reloadProperties()/its caller so the styling reads occur only
after the background load completes: either make reloadProperties accept a
completion Runnable (or return a Future) and invoke reloadActivityStyling’s
property reads from that completion callback, or move the code in
reloadActivityStyling that accesses mProperties into the main-thread callback
currently passed to TermuxExecutor.execute (the lambda that calls
mTermuxTerminalViewClient.onReloadProperties), ensuring all reads of mProperties
happen after mProperties.loadTermuxPropertiesFromDisk() finishes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
termux/termux-app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java (1)

363-391: ⚠️ Potential issue | 🟠 Major

Inconsistent main-thread marshaling in addNewSession.

setCurrentSession(newTerminalSession) at line 387 directly performs UI operations (attaching sessions, scrolling the list, updating background color) without executeOnMain, while the dialog (line 368) and drawer close (line 389) are wrapped. This is inconsistent:

  • If addNewSession is always called on the main thread, the two executeOnMain wrappers are unnecessary and subtly change timing (deferring execution to the next handler message).
  • If it may be called off the main thread, then setCurrentSession must also be wrapped, otherwise it will crash.

Additionally, the executeOnMain lambda at line 368 lacks an activity-state guard (mActivity.isFinishing()), which can cause a BadTokenException if the activity finishes before the posted Runnable executes.

Proposed fix — add guard to dialog lambda
 TermuxExecutor.executeOnMain(() -> {
+    if (mActivity == null || mActivity.isFinishing()) return;
     new AlertDialog.Builder(mActivity).setTitle(R.string.title_max_terminals_reached).setMessage(R.string.msg_max_terminals_reached)
             .setPositiveButton(android.R.string.ok, null).show();
 });
🧹 Nitpick comments (2)
termux/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxSharedProperties.java (2)

46-60: Move logging outside the synchronized block to reduce lock-hold time.

dumpPropertiesToLog() and dumpInternalPropertiesToLog() iterate over all properties, build strings, and write logs — all while holding the monitor. This unnecessarily blocks concurrent readers (e.g., getInternalPropertyValue) during what could be a slow I/O path.

Since loadedProperties is a local variable that won't be mutated by other threads, you can safely log after releasing the lock:

Suggested fix
     synchronized (this) {
         mPropertiesFile = propertiesFile;
         mSharedProperties = loadedProperties;
-        dumpPropertiesToLog();
-        dumpInternalPropertiesToLog();
     }
+    dumpPropertiesToLog();
+    dumpInternalPropertiesToLog();

Note: dumpPropertiesToLog calls getProperties(true) which reads the volatile mSharedProperties, so it will see the just-published value without needing the lock.


157-179: synchronized on the whole method is correct for the compound read, but beware of disk I/O under the lock.

The synchronized is needed for the check-then-act on lines 160–164 — good. However, when cached is false (line 177), mSharedProperties.getProperty(key, false) reads directly from the properties file while holding the intrinsic lock on this. This blocks all other getInternalPropertyValue callers (and loadTermuxPropertiesFromDisk) for the duration of that disk read.

If the cached == false path is rarely used, this is acceptable. If it's called frequently, consider reading the property outside the lock:

Possible restructuring
 public synchronized Object getInternalPropertyValue(String key, boolean cached) {
+    if (!cached) {
+        // Read from disk outside the lock to avoid blocking other callers
+        String propValue = mSharedProperties.getProperty(key, false);
+        return getInternalTermuxPropertyValueFromValue(mContext, key, propValue);
+    }
     Object value;
-    if (cached) {
-        value = mSharedProperties.getInternalProperty(key);
-        if (value != null || mSharedProperties.getInternalProperties().containsKey(key)) {
-            return value;
-        } else {
-            value = getInternalTermuxPropertyValueFromValue(mContext, key, null);
-            Logger.logWarn(LOG_TAG, "The value for \"" + key + "\" not found in SharedProperties cache, force returning default value: `" + value +  "`");
-            return value;
-        }
-    } else {
-        return getInternalTermuxPropertyValueFromValue(mContext, key, mSharedProperties.getProperty(key, false));
-    }
+    value = mSharedProperties.getInternalProperty(key);
+    if (value != null || mSharedProperties.getInternalProperties().containsKey(key)) {
+        return value;
+    }
+    value = getInternalTermuxPropertyValueFromValue(mContext, key, null);
+    Logger.logWarn(LOG_TAG, "The value for \"" + key + "\" not found in SharedProperties cache, force returning default value: `" + value +  "`");
+    return value;
 }

Actually, the non-cached path doesn't need the lock at all since it just reads from disk and derives a value — there's no compound check on shared state. Moving it before the synchronized (or splitting into two methods) would eliminate unnecessary contention.

@jatezzz jatezzz requested a review from a team February 9, 2026 19:44
@jatezzz jatezzz force-pushed the fix/ADFA-2829-termux-executor-threading branch from d2cbe97 to 54a1b7d Compare February 10, 2026 13:51
@jatezzz jatezzz force-pushed the fix/ADFA-2829-termux-executor-threading branch from 54a1b7d to 1b8d0cd Compare February 11, 2026 13:17
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
termux/termux-app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java (1)

363-391: ⚠️ Potential issue | 🟠 Major

addNewSession is called from a background thread but performs thread-unsafe operations.

This method is invoked via TermuxExecutor.executeInBackground from TermuxActivity.onCreateNewSession (line 707). While the dialog (line 368) and drawer close (line 389) are now correctly dispatched to the main thread, these operations still run on the background thread without thread-safety guarantees:

  • Line 367: service.getTermuxSessionsSize() — reads mutable service state
  • Line 373: mActivity.getCurrentSession() — reads the terminal view's current session (UI state)
  • Line 383: service.createTermuxSession(...) — mutates the sessions list

The safest fix is to run addNewSession entirely on the main thread and remove the executeInBackground wrapper in TermuxActivity.onCreateNewSession, since this method doesn't perform heavy I/O.

Proposed fix in TermuxActivity.onCreateNewSession (lines 704-710)
 protected void onCreateNewSession(boolean isFailsafe, String sessionName, String workingDirectory) {
     if (mTermuxTerminalSessionActivityClient == null) return;
-
-    TermuxExecutor.executeInBackground(() -> {
-        mTermuxTerminalSessionActivityClient.addNewSession(isFailsafe, sessionName, workingDirectory);
-    });
+    mTermuxTerminalSessionActivityClient.addNewSession(isFailsafe, sessionName, workingDirectory);
 }

And the executeOnMain wrappers for the dialog and drawer can then be removed as they'd already be on the main thread:

Simplify addNewSession once it runs on main thread
         if (service.getTermuxSessionsSize() >= MAX_SESSIONS) {
-            TermuxExecutor.executeOnMain(() -> {
-                new AlertDialog.Builder(mActivity).setTitle(R.string.title_max_terminals_reached).setMessage(R.string.msg_max_terminals_reached)
-                        .setPositiveButton(android.R.string.ok, null).show();
-            });
+            new AlertDialog.Builder(mActivity).setTitle(R.string.title_max_terminals_reached).setMessage(R.string.msg_max_terminals_reached)
+                    .setPositiveButton(android.R.string.ok, null).show();
         } else {
             ...
-            TermuxExecutor.executeOnMain(() -> mActivity.getDrawer().closeDrawers());
+            mActivity.getDrawer().closeDrawers();
         }
🤖 Fix all issues with AI agents
In
`@termux/termux-shared/src/main/java/com/termux/shared/termux/TermuxExecutor.java`:
- Around line 12-13: executeInBackground currently forwards task directly to
backgroundExecutor.execute without validating null; add the same null-check
behavior used in execute() so that if task (the parameter to
executeInBackground) is null you skip calling backgroundExecutor.execute (and
optionally log or return) instead of letting backgroundExecutor.execute throw a
NullPointerException; locate the executeInBackground method in TermuxExecutor
and mirror the guard used in execute() to validate the Runnable before invoking
backgroundExecutor.execute.
🧹 Nitpick comments (3)
termux/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxSharedProperties.java (1)

46-60: Clean local-variable-then-swap pattern for thread-safe loading.

Building loadedProperties locally and assigning it atomically inside the synchronized block ensures the published object is fully initialized. The volatile write in line 56 provides happens-before to unsynchronized readers.

One minor observation: dumpPropertiesToLog() and dumpInternalPropertiesToLog() (lines 57–58) are called inside the synchronized(this) block. These methods call getProperties(true) and getInternalProperties() which read mSharedProperties — this works correctly since the reference was just assigned. However, the logging could be slow if the properties map is large, which would hold the monitor longer than necessary. Consider moving the dump calls outside the synchronized block.

♻️ Move logging outside the synchronized block
     synchronized (this) {
         mPropertiesFile = propertiesFile;
         mSharedProperties = loadedProperties;
-        dumpPropertiesToLog();
-        dumpInternalPropertiesToLog();
     }
+    dumpPropertiesToLog();
+    dumpInternalPropertiesToLog();

Since mSharedProperties is volatile, readers outside the lock already see the latest reference, and the dump methods only read the reference — they don't need to hold the lock.

termux/termux-shared/src/main/java/com/termux/shared/termux/TermuxExecutor.java (2)

8-14: Single-thread executor serializes all background work — potential bottleneck.

Executors.newSingleThreadExecutor() means every call to executeInBackground and execute is queued behind all previous tasks. If any task blocks (e.g., slow disk I/O on first launch, font loading), subsequent tasks (JNI preload, preferences loading, crash log check) will be delayed.

This is a reasonable simplification to avoid concurrency bugs, but be aware that the serialized queue could negate some of the startup-time benefits of moving work off the main thread. If startup latency is still a concern, consider a small fixed-size pool (e.g., 2–3 threads) or at least document this design choice.


25-32: Uncaught exceptions in executeInBackground are silently swallowed.

When a task submitted via executeInBackground throws an unchecked exception, the single-thread executor catches it internally, terminates that thread, and creates a replacement. The exception is never logged or propagated. The execute method has the same issue for the background task (though the finally block correctly ensures the callback runs).

Consider wrapping with a try-catch that at minimum logs the failure:

Proposed fix for executeInBackground
 public static void executeInBackground(Runnable task) {
+    if (task == null) return;
-    backgroundExecutor.execute(task);
+    backgroundExecutor.execute(() -> {
+        try {
+            task.run();
+        } catch (Exception e) {
+            android.util.Log.e("TermuxExecutor", "Background task failed", e);
+        }
+    });
 }

@jatezzz jatezzz force-pushed the fix/ADFA-2829-termux-executor-threading branch from 1b8d0cd to 604f411 Compare February 11, 2026 13:52
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
termux/termux-app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java (1)

363-391: ⚠️ Potential issue | 🟠 Major

Thread-safety concern: addNewSession body still runs on a background thread.

This method is invoked from TermuxActivity.onCreateNewSession via TermuxExecutor.executeInBackground. While the UI operations (AlertDialog, closeDrawers, setCurrentSession) have been correctly marshaled to the main thread, several intermediate operations still execute on the background thread:

  • Line 364: mActivity.getTermuxService() — reads mutable activity state
  • Line 367: service.getTermuxSessionsSize() — reads session list
  • Line 373: mActivity.getCurrentSession() — accesses TerminalView from a background thread (View access off the UI thread)
  • Line 383: service.createTermuxSession(...) — modifies the sessions list

The createTermuxSession call may be appropriate on background if it does I/O, but the View access on Line 373 and unsynchronized reads of the session list could introduce subtle races. Consider wrapping the entire addNewSession body in executeOnMain and only offloading the actual subprocess creation if needed.

🤖 Fix all issues with AI agents
In
`@termux/termux-shared/src/main/java/com/termux/shared/termux/TermuxExecutor.java`:
- Around line 17-19: The method executeOnMain currently calls
mainHandler.post(task) without guarding against a null Runnable which causes a
NullPointerException; update executeOnMain to check for a null task (like the
existing executeInBackground/execute methods) and return early (or log) if task
is null instead of calling mainHandler.post, ensuring mainHandler.post is only
invoked with a non-null Runnable.
🧹 Nitpick comments (2)
termux/termux-shared/src/main/java/com/termux/shared/termux/TermuxExecutor.java (1)

9-9: Single-thread executor serializes all background work — verify this is intentional.

newSingleThreadExecutor() means all background tasks across the entire app (JNI preload, preferences loading, font/color loading, crash log checking, session creation) are queued and run sequentially. If any task blocks or runs long, all subsequent tasks are delayed.

If parallelism is desired for independent tasks (e.g., JNI preload shouldn't block preferences loading), consider a small fixed thread pool. If strict ordering is intentional (e.g., to avoid concurrent disk access), then this is fine.

termux/termux-app/src/main/java/com/termux/app/TermuxActivity.java (1)

554-563: Stale property reads in reloadActivityStyling after async reloadProperties.

reloadProperties() (Line 554) dispatches loadTermuxPropertiesFromDisk() to a background thread, but reloadActivityStyling() (Line 1045) calls it and then immediately reads mProperties on Lines 1050 and 1055. Since the background task hasn't completed yet, these reads return the old values.

If this is an accepted design trade-off (properties are consistent but possibly stale until the callback fires), the behavior is safe but the UI may briefly show outdated styling until the next styling trigger. Consider chaining the styling reads into the main-thread callback of reloadProperties for correctness, or document this as intentional.

Also applies to: 1045-1056

@jatezzz jatezzz force-pushed the fix/ADFA-2829-termux-executor-threading branch from 604f411 to 442f435 Compare February 12, 2026 13:17
@jatezzz jatezzz force-pushed the fix/ADFA-2829-termux-executor-threading branch from 442f435 to 36e29c3 Compare February 12, 2026 14:01
@jatezzz jatezzz merged commit a6fbd01 into stage Feb 12, 2026
2 checks passed
@jatezzz jatezzz deleted the fix/ADFA-2829-termux-executor-threading branch February 12, 2026 14:08
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