Skip to content

ADFA-3945: Handle reader-thread IOException on tar.xz extraction timeout#1412

Open
fryanpan wants to merge 1 commit into
stagefrom
ADFA-3945-tarxz-reader-thread-ioexception
Open

ADFA-3945: Handle reader-thread IOException on tar.xz extraction timeout#1412
fryanpan wants to merge 1 commit into
stagefrom
ADFA-3945-tarxz-reader-thread-ioexception

Conversation

@fryanpan

@fryanpan fryanpan commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

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

Reproduction Details

IdeArchiveServiceImpl.extractTarXzViaTermux ran Termux tar and drained the child's stdout on a separate reader thread (thread { process.inputStream.bufferedReader().useLines { … } }). On a 2-minute timeout the code calls destroyForcibly(), which closes the pipe; the reader thread's in-flight read() then throws InterruptedIOException on a bare thread with no handler → uncaught → app crash.

Stack Trace

InterruptedIOException: read interrupted by close() on another thread
  at libcore.io.Linux.read(Linux.java)
  ... (BufferedInputStream / StreamDecoder / BufferedReader.readLine) ...
  at com.itsaky.androidide.plugins.manager.services.IdeArchiveServiceImpl.extractTarXzViaTermux$lambda$0$0$1(Unknown Source:31)
  at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Unknown Source:2)

(thread: tar-xz-extract-output · device.class: high)

User Steps

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

  • Fires at app startup while a bundled component is being extracted (per related events, the NDK-installer plugin org.appdevforall.ndkinstaller.cgp). The extraction is force-killed (timeout/cancel) and the reader thread dies mid-read. (The crashing event itself has no UI breadcrumbs — it's a background reader thread.)

Was able to reproduce in a unit test?

No.
What remains after this fix is the ProcessBuilder/Termux path — extractTarXzViaTermux is a private fun on a final class and execs a hard-coded on-device tar binary ($TERMUX_BIN_PATH/tar) absent on any JVM test host, so start() can't run there. However, Option B removes the concurrency hazard entirely (no reader thread), so the failure class this ticket is about no longer exists to be triggered. Making the remaining exec path regression-testable would need an injectable process/command seam (@VisibleForTesting) — tracked as a follow-up. The change compiles cleanly (:plugin-manager:compileV8DebugKotlin BUILD SUCCESSFUL).

What Was Fixed

Root-cause fix (Hal's suggestion, "Option B"), not a symptom catch. Redirect the child's combined stdout/stderr to a temp file via ProcessBuilder.redirectOutput(logFile) and read it back after waitFor(), dropping the reader thread entirely:

  • With OS-side redirection there is no concurrent read() to be interrupted when destroyForcibly() closes the pipe — the InterruptedIOException-on-a-bare-thread crash cannot occur.
  • It also removes the latent full-pipe-buffer deadlock risk (the original reason a drainer thread existed).
  • destroyForcibly() on timeout is retained; the temp log is deleted after use.
  • Makes a future timeout/cancellation change safe — there's no race to reintroduce.

(Considered and rejected "Option A" — close stdout then destroyForcibly(): on Android, closing a stream under a blocked read is exactly what produces read interrupted by close(), so it wouldn't reliably give a clean EOF and would still need the catch.)

Testing

:plugin-manager:compileV8DebugKotlin → BUILD SUCCESSFUL. No automated runtime test (see above — the remaining exec path isn't JVM-testable; the bug class itself is removed by construction). Branch history was rewritten to a single clean commit implementing Option B.

Follow-up (separate): make the extraction timeout adaptive for slow devices + add progress/cancel UI + a retry path (the user-facing "extraction actually succeeds" improvement), and add an @VisibleForTesting process seam.


Fixes APPDEVFORALL-V0

@fryanpan fryanpan force-pushed the ADFA-3945-tarxz-reader-thread-ioexception branch from 9f4d410 to 1d4c800 Compare June 19, 2026 12:02
@fryanpan

Copy link
Copy Markdown
Contributor Author

⚠️ Heads-up for reviewers: approach changed + history rewritten (force-push).

This PR previously caught the reader-thread IOException (symptom fix). It now implements the root-cause fix (Hal's Option B): redirect tar's output to a temp file via ProcessBuilder.redirectOutput(), drop the reader thread, and read the log after waitFor(). No concurrent read → the InterruptedIOException-on-a-bare-thread crash can't occur; also removes a latent pipe-buffer deadlock. destroyForcibly() on timeout retained.

Rejected Option A (close-then-destroy): on Android, closing a stream under a blocked read is precisely what throws read interrupted by close(), so it wouldn't give a clean EOF.

Branch was rewritten to a single clean commit (1d4c800ee); the description is updated to match. Follow-up (separate): adaptive timeout + progress/retry UX, and an @VisibleForTesting process seam for regression testing.

@fryanpan fryanpan marked this pull request as ready for review June 19, 2026 16:56
…thread race)

The Termux tar extraction drained the child's stdout on a separate reader thread.
On a 2-minute timeout the code calls destroyForcibly(), which closes the pipe; the
reader thread's in-flight read() then throws InterruptedIOException on a bare thread
with no handler, crashing the app (Sentry APPDEVFORALL-V0).

Fix (root cause, not the symptom): redirect the child's combined stdout/stderr to a
temp file via ProcessBuilder.redirectOutput(), drop the reader thread entirely, and
read the log back after waitFor(). With OS-side redirection there is no concurrent
read to interrupt when the pipe is closed, and the process also can't deadlock on a
full pipe buffer. destroyForcibly() on timeout is retained; the temp log is deleted
after use.

This eliminates the entire bug class rather than catching the exception, and makes a
future timeout/cancellation change safe (no race to reintroduce). Per Hal's suggestion
on ADFA-3945.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@fryanpan fryanpan force-pushed the ADFA-3945-tarxz-reader-thread-ioexception branch from 1d4c800 to 847c79e Compare June 19, 2026 16:58
@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 26 minutes and 57 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: a8eded39-237e-40f6-bda3-27fa5598cef3

📥 Commits

Reviewing files that changed from the base of the PR and between 1d4c800 and 847c79e.

📒 Files selected for processing (1)
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeArchiveServiceImpl.kt
📝 Walkthrough

Walkthrough

IdeArchiveServiceImpl.extractTarXzViaTermux is refactored to redirect the Termux tar process's combined stdout/stderr to a temporary log file via ProcessBuilder.redirectOutput, replacing the prior background thread that drained the process input stream. Elapsed time is measured with measureTimeMillis, the log is read after completion, and the temp file is deleted in a cleanup step. A KDoc block documenting this approach is added.

Termux tar extraction output redirection

Layer / File(s) Summary
File-redirection execution and cleanup
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeArchiveServiceImpl.kt
Import of kotlin.concurrent.thread replaced with kotlin.system.measureTimeMillis; KDoc added explaining the log-file redirection and avoided pipe/concurrency hazard; execution block reworked to redirect combined stdout/stderr to a temp file, wrap start/wait/timeout in measureTimeMillis, and read the log post-completion; temp file deletion added as a trailing cleanup step.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A thread once chased the tar's loud stream,
Now a file collects each line and dream.
Redirected bytes land safe and neat,
No concurrency hazard left to beat.
Hop hop, the log is read and gone! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 accurately describes the core fix: handling the reader-thread IOException that occurred on tar.xz extraction timeout, which is the root cause addressed in this PR.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, providing reproduction details, root cause analysis, and explanation of the fix implemented.
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-3945-tarxz-reader-thread-ioexception

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.

@Daniel-ADFA Daniel-ADFA self-requested a review June 23, 2026 20:25
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