Skip to content

feat: improve terminal install, recovery, and proot compatibility#1926

Open
Ebola-Chan-bot wants to merge 12 commits intoAcode-Foundation:mainfrom
Ebola-Chan-bot:feat/terminal-install-recovery
Open

feat: improve terminal install, recovery, and proot compatibility#1926
Ebola-Chan-bot wants to merge 12 commits intoAcode-Foundation:mainfrom
Ebola-Chan-bot:feat/terminal-install-recovery

Conversation

@Ebola-Chan-bot
Copy link
Contributor

  • switch terminal setup to a staged install pipeline with cache markers, native download actions, and selective uninstall behavior so repeated installs and repairs avoid unnecessary downloads
  • replace process-only startup checks with HTTP readiness probing, add automatic repair when axs is running but not reachable, and retry terminal creation after refreshing the embedded axs binary on PTY open failures
  • improve Alpine bootstrap scripts by hardening package checks, moving command-not-found handling through /bin/sh, exposing the acode CLI as shell functions, enabling allow-any-origin for local requests, disabling proot seccomp, and removing --sysvipc for kernel stability
  • unify terminal default settings and validation for font options, ligatures, image support, and letter spacing, and improve terminal resize and mount behavior around fit timing and observer updates
  • persist terminal sessions and active tabs, clean up failed terminal tabs more aggressively, and automatically recover from relocation or symbol resolution failures by reinstalling the runtime when needed
  • expose copyAsset from the system bridge for debug axs refresh flows, keep Android executor download support wired through the JS bridge, reduce noisy auth logging for expected unauthenticated states, and update package-lock.json to reflect the integrated dependency state

- switch terminal setup to a staged install pipeline with cache markers, native download actions, and selective uninstall behavior so repeated installs and repairs avoid unnecessary downloads
- replace process-only startup checks with HTTP readiness probing, add automatic repair when axs is running but not reachable, and retry terminal creation after refreshing the embedded axs binary on PTY open failures
- improve Alpine bootstrap scripts by hardening package checks, moving command-not-found handling through /bin/sh, exposing the acode CLI as shell functions, enabling allow-any-origin for local requests, disabling proot seccomp, and removing --sysvipc for kernel stability
- unify terminal default settings and validation for font options, ligatures, image support, and letter spacing, and improve terminal resize and mount behavior around fit timing and observer updates
- persist terminal sessions and active tabs, clean up failed terminal tabs more aggressively, and automatically recover from relocation or symbol resolution failures by reinstalling the runtime when needed
- expose copyAsset from the system bridge for debug axs refresh flows, keep Android executor download support wired through the JS bridge, reduce noisy auth logging for expected unauthenticated states, and update package-lock.json to reflect the integrated dependency state
Copilot AI review requested due to automatic review settings March 7, 2026 15:56
@github-actions github-actions bot added the enhancement New feature or request label Mar 7, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the terminal runtime lifecycle (install/start/repair/uninstall) to be more incremental and resilient, while improving terminal settings validation and runtime behavior across font/resize/session recovery paths.

Changes:

  • Reworks Alpine/AXS installation into a staged pipeline using cache markers and a new native download action, plus adds partial vs full uninstall paths.
  • Improves runtime readiness/recovery (HTTP probing, repair flows, debug-only axs refresh, and in-place recovery from corrupted rootfs/symbol errors).
  • Unifies and hardens terminal settings/runtime updates (letter spacing validation/clamping, font handling, and resize behavior changes).

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/settings/terminalSettings.js Adds letterSpacing validation/clamping and optional “delete download cache” during uninstall.
src/plugins/terminal/www/Terminal.js Implements staged install with markers, cache invalidation, repair helpers, and adds uninstallFull/resetConfigured.
src/plugins/terminal/www/Executor.js Adds JS bridge support for a new native download action with optional progress messages.
src/plugins/terminal/src/android/Executor.java Wires the new “download” action through the main executor plugin.
src/plugins/terminal/src/android/DownloadHelper.java Introduces native HTTP download implementation with progress reporting.
src/plugins/terminal/src/android/BackgroundExecutor.java Wires the new “download” action through the background executor plugin.
src/plugins/terminal/scripts/init-sandbox.sh Adjusts proot args/env for broader kernel compatibility (seccomp off, sysvipc removed).
src/plugins/terminal/scripts/init-alpine.sh Hardens package checks, improves command-not-found behavior, shifts acode CLI to bash functions, and adjusts axs launch flags.
src/plugins/system/www/plugin.js Exposes copyAsset over the system JS bridge.
src/plugins/system/android/com/foxdebug/system/System.java Implements native copyAsset action.
src/lib/auth.js Reduces noisy logging for expected unauthenticated states.
src/components/terminal/terminalManager.js Moves install check to after mount for streaming logs into the same tab; adds in-place recovery flow.
src/components/terminal/terminalDefaults.js Clamps/normalizes persisted terminal letterSpacing.
src/components/terminal/terminal.js Improves fontFamily handling, resize syncing, readiness probing/repair behavior, and reconnect cleanup.
package-lock.json Updates lockfile metadata to reflect dependency graph changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR substantially overhauls the Acode terminal subsystem across the full stack (shell scripts, Java, and JS) to improve install reliability, startup robustness, and proot compatibility. Key improvements include a staged install pipeline with cache markers to skip redundant downloads/extraction, HTTP-based AXS readiness probing replacing PID-only liveness checks, automatic in-place rootfs repair on libc/readline corruption, proot hardening (PROOT_NO_SECCOMP, removal of --sysvipc), a native DownloadHelper with proper redirect handling and progress reporting, and unified letterSpacing validation across settings and defaults.

  • Critical bug in Executor.js: download() resolves its Promise prematurely on the first Java progress callback when onProgress is not provided. F-Droid builds (proot/talloc downloads omit the callback) will proceed to extraction with incomplete files.
  • init-alpine.sh fallback mirror hardcodes Alpine v3.21: If the installed rootfs is any other Alpine version the retry will always silently fail; the version should be read from /etc/alpine-release.
  • Terminal.js startAxs(installing=true) — fire-and-forget script writes: init-alpine.sh and rm-wrapper.sh are written via callback-based readAsset without sequencing, creating a race where proot may source stale scripts.
  • createSession() repair path: When repairOk === false, pollAxs(30) still runs unconditionally, wasting 30 seconds on a timeout that is guaranteed to fail before throwing the error.

Confidence Score: 2/5

  • Not safe to merge — the download() early-resolution bug will silently break F-Droid installs, and the fallback mirror version mismatch can cause package install failures.
  • The Executor.js download() early-resolution is a correctness bug that affects every F-Droid download without a progress callback (proot, talloc, libproot variants). The Promise resolves on the first Java progress tick, so await Executor.download(...) returns before the file is fully written, creating a race that leads either to a corrupt proot binary being used or a tar extraction failure (mitigated only by the retry path). The hardcoded Alpine mirror version is a separate functional issue on non-v3.21 rootfs builds. These are both in newly introduced code paths.
  • src/plugins/terminal/www/Executor.js (critical download bug), src/plugins/terminal/scripts/init-alpine.sh (hardcoded Alpine version), src/plugins/terminal/www/Terminal.js (fire-and-forget script writes), src/components/terminal/terminal.js (repair poll waste on failed repair)

Important Files Changed

Filename Overview
src/plugins/terminal/www/Executor.js New download() method has a critical bug: the Promise resolves prematurely on the first Java progress callback when onProgress is not provided, causing F-Droid proot/talloc downloads to complete early while the file is still being written.
src/plugins/terminal/www/Terminal.js Staged install pipeline (download/extract/configure phases with cache markers) is a solid improvement, but readAsset write callbacks for init-alpine.sh/rm-wrapper.sh are fire-and-forget and may race with shell startup. New uninstall()/uninstallFull() split and resetConfigured() are clean.
src/components/terminal/terminal.js HTTP readiness probing (replacing PID-only liveness checks), PTY error JSON parsing, attachAddon disposal on reconnect, and relocation-sniff logic are well-reasoned improvements. Repair path wastes 30 s polling when repair itself fails.
src/components/terminal/terminalManager.js In-place rootfs corruption recovery via onCrashData is well-structured; now properly guards for window.Terminal.uninstall availability. Install-after-mount pattern and forceReinstall flag are clean. ResizeObserver guard (width/height < 10) prevents spurious fits.
src/plugins/terminal/scripts/init-alpine.sh File-stat package checks and proot-aware command_not_found_handle are good improvements. acode CLI as a bash function elegantly avoids shebang/execve issues. Fallback mirror hardcodes v3.21 which will fail for rootfs images on any other Alpine release.
src/plugins/terminal/src/android/DownloadHelper.java New download helper correctly uses getContentLengthLong(), handles HTTPS→HTTP downgrade prevention, null-checks the Location header, and uses try-with-resources for stream cleanup. Partial file is not deleted on mid-stream I/O failure, but the JS install retry handles this gracefully.

Sequence Diagram

sequenceDiagram
    participant UI as TerminalManager
    participant TC as TerminalComponent
    participant TJS as Terminal.js (www)
    participant AXS as AXS HTTP Server
    participant Java as DownloadHelper (Java)

    UI->>TC: createTerminal()
    TC->>TC: mount()
    TC->>UI: checkAndInstallTerminal()
    alt Not installed
        UI->>TJS: install()
        TJS->>Java: download(alpineUrl, onProgress)
        Java-->>TJS: progress callbacks (keepCallback=true)
        Java-->>TJS: success(dst) — final resolve
        TJS->>Java: download(axsUrl, onProgress)
        Java-->>TJS: progress callbacks
        Java-->>TJS: success(dst)
        Note over TJS,Java: F-Droid only — no onProgress!
        TJS->>Java: download(prootUrl)
        Java-->>TJS: ⚠ first progress → Promise resolves early
        TJS->>TJS: writeText(.download-manifest)
        TJS->>TJS: mkdirs(.downloaded)
        TJS->>TJS: tar extract
        TJS->>TJS: startAxs(installing=true)
        TJS-->>UI: install OK
    end
    TC->>TC: connectToSession()
    TC->>TJS: isAxsRunning()
    TJS-->>TC: true/false
    alt AXS not running
        TC->>TJS: startAxs(false)
    end
    TC->>AXS: pollAxs() — GET /
    AXS-->>TC: HTTP 200 (ready)
    TC->>AXS: POST /terminals
    AXS-->>TC: pid
    TC->>AXS: WebSocket /terminals/{pid}
    AXS-->>TC: connected
    Note over TC: Sniff first 15s of WS messages for relocation errors
    alt Relocation error detected
        TC->>UI: onCrashData("relocation_error")
        UI->>TJS: Terminal.uninstall()
        UI->>UI: checkAndInstallTerminal(forceReinstall=true)
        UI->>TC: connectToSession() — fresh session
    end
Loading

Comments Outside Diff (2)

  1. src/plugins/terminal/scripts/init-alpine.sh, line 1083-1090 (link)

    Hardcoded Alpine v3.21 in fallback mirror may not match actual rootfs version

    The fallback mirror URLs are pinned to Alpine v3.21:

    echo "https://mirrors.tuna.tsinghua.edu.cn/alpine/v3.21/main" > /etc/apk/repositories
    echo "https://mirrors.tuna.tsinghua.edu.cn/alpine/v3.21/community" >> /etc/apk/repositories

    If the installed rootfs is any other Alpine release (e.g. v3.20 or v3.22), apk add against the v3.21 repos will fail or install incompatible packages. The original /etc/apk/repositories is restored afterwards, so this only affects the single retry attempt, but the retry will always fail for users on a different Alpine version, silently swallowing the "retrying with mirror" hint.

    The actual version can be derived from the filesystem itself:

    _alpine_ver=$(cat /etc/alpine-release 2>/dev/null | cut -d. -f1,2)
    if [ -n "$_alpine_ver" ]; then
        echo "https://mirrors.tuna.tsinghua.edu.cn/alpine/v${_alpine_ver}/main" > /etc/apk/repositories
        echo "https://mirrors.tuna.tsinghua.edu.cn/alpine/v${_alpine_ver}/community" >> /etc/apk/repositories
    fi
  2. src/plugins/terminal/www/Terminal.js, line 19-63 (link)

    Fire-and-forget init-alpine.sh write races with shell startup

    readAsset("init-alpine.sh", ...) and readAsset("rm-wrapper.sh", ...) (lines 21–29) both invoke system.writeText inside their callbacks without awaiting them. The shell process is launched inside the third readAsset("init-sandbox.sh", ...) callback on line 34, which itself is fired asynchronously. There is no guarantee that init-alpine.sh has finished writing to disk before source "${filesDir}/init-sandbox.sh" --installing is executed and proot starts sourcing init-alpine.sh.

    On a slow device or when Android's I/O scheduler delays the writeText flush, proot may source a stale init-alpine.sh left by the previous install. Since this PR changes init-alpine.sh to always be overwritten (the guard if [ ! -e ... ] was removed for initrc), a stale script being sourced is now more likely to produce surprising behaviour than before.

    The core pattern here is that readAsset is a callback-based API, and chaining three of them without sequencing them means the order of execution is not guaranteed. A sequential write chain before launching the executor would eliminate the race.

Last reviewed commit: de0c4a2

Tighten terminal startup probing and relocation-error sniffing so review feedback does not regress startup reliability on older WebView builds.

Harden Android-side download and asset copy helpers with safer redirect handling, proper resource cleanup, and threaded asset extraction.

Quote shell paths consistently, refine bash availability detection in init-alpine.sh, and keep the allow-any-origin choice documented until axs exposes a real origin allowlist.
@Ebola-Chan-bot Ebola-Chan-bot force-pushed the feat/terminal-install-recovery branch 2 times, most recently from 695dec4 to 778c3e3 Compare March 8, 2026 02:52
@bajrangCoder
Copy link
Member

@greptileai review it

@bajrangCoder bajrangCoder added the CI: RUN ON-DEMAND PREVIEW RELEASES Triggers an on-demand preview build for this pull request via CI workflow. label Mar 8, 2026
@github-actions github-actions bot removed the CI: RUN ON-DEMAND PREVIEW RELEASES Triggers an on-demand preview build for this pull request via CI workflow. label Mar 8, 2026
@github-actions

This comment has been minimized.

@github-actions
Copy link

github-actions bot commented Mar 8, 2026

🔴 (Workflow Trigger stopped), Your On-Demand Preview Release/build for #1926.
status: failure


For Owners: Please Click here to view that github actions

@RohitKushvaha01 RohitKushvaha01 self-assigned this Mar 8, 2026
@RohitKushvaha01 RohitKushvaha01 marked this pull request as draft March 8, 2026 05:00
埃博拉酱 added 2 commits March 8, 2026 15:51
Include DownloadHelper.java in the terminal plugin's Android source-file list so Cordova prepare brings it into the generated platform project and CI Java compilation can resolve the helper references.

Document why redirect handling does not enforce same-host redirects for signed GitHub release assets, while still restricting redirects to HTTP(S) targets and blocking HTTPS downgrade.

Clarify restored terminal behavior by printing an explicit restored-session notice when reconnecting to an existing PTY, so missing MOTD is not misdiagnosed as a startup regression.
Await the download manifest write before creating the .downloaded marker so cache state cannot observe a downloaded marker without the matching manifest payload on disk.

Fail the in-place corrupted-rootfs recovery path immediately when the terminal uninstall API is unavailable, instead of pretending the rootfs was removed and continuing into a misleading reinstall sequence.
@Ebola-Chan-bot Ebola-Chan-bot marked this pull request as ready for review March 8, 2026 08:09
Update terminal session startup so createSession always waits for the axs HTTP endpoint to become reachable even when a stale or still-booting PID already exists. This removes the race where POST /terminals could run before the embedded server was actually ready during slow startup or crash recovery.

Replace the fragile PTY open error substring detection with structured JSON parsing so normal output that happens to contain overlapping text does not trigger the binary refresh and retry path.

Clarify in init-alpine.sh that the temporary allow-any-origin switch cannot be tightened from the shell wrapper because Origin validation must be implemented inside axs itself.
埃博拉酱 added 2 commits March 8, 2026 19:37
Adjust the init-alpine.sh comment so it distinguishes upstream Cordova's default https://localhost origin from this repo's build pipeline, which rewrites the scheme to http for ws:// terminal sockets.

Also fix the remaining terminal.js formatting issue that was flagged by Biome in CI.
Update the init-alpine.sh comment around axs --allow-any-origin so it matches the current build behavior.

Remove the outdated claim that the build pipeline rewrites Cordova scheme to http and document the actual reason this stopgap remains in place until axs supports a narrower origin or auth gate.
@Ebola-Chan-bot Ebola-Chan-bot force-pushed the feat/terminal-install-recovery branch from 170774b to 5084651 Compare March 8, 2026 16:55
Keep Android resource post-processing compatible with the current Cordova Android output while preserving the existing Android 14 build path.

Remove the allow-any-origin flag from the bundled terminal startup script for the current runtime validation build.

Add detailed bundled AXS copy failure logging so LAN debug output shows the exact asset-copy error instead of a generic fallback message.
埃博拉酱 added 2 commits March 9, 2026 21:24
- restore the AXS launch section in init-alpine.sh to the earlier source state without the explanatory allow-any-origin comments
- keep the default source startup command as plain axs invocation so allow-any-origin is no longer baked into source
- leave dynamic allow-any-origin handling to the outer build/deploy script when LAN debug injection is explicitly enabled
… feat/terminal-install-recovery

# Conflicts:
#	src/settings/terminalSettings.js
@RohitKushvaha01
Copy link
Member

RohitKushvaha01 commented Mar 10, 2026

@Ebola-Chan-bot I tested this PR on my device and found a big problem the AXS binary gets deleted after the download, and the whole terminal ends up broken.

@RohitKushvaha01 RohitKushvaha01 marked this pull request as draft March 10, 2026 06:04
Remove the debug-only asset refresh flow from the terminal plugin and terminal session startup.

Keep AXS acquisition on the normal download path so debug and release share the same runtime behavior.

This also drops the PTY retry path that depended on replacing the local binary from bundled assets.
@Ebola-Chan-bot
Copy link
Contributor Author

Ebola-Chan-bot commented Mar 10, 2026

@Ebola-Chan-bot I tested this PR on my device and found a big problem the AXS binary gets deleted after the download, and the whole terminal ends up broken.

Thank you. Should be fixed now.

@Ebola-Chan-bot Ebola-Chan-bot marked this pull request as ready for review March 10, 2026 10:38
Comment on lines +196 to +217
download(url, dst, onProgress) {
return new Promise((resolve, reject) => {
exec(
(msg) => {
if (onProgress && typeof msg === "string") {
try {
const data = JSON.parse(msg);
if (data.type === "progress") {
onProgress(data);
return;
}
} catch (_) {}
}
resolve(msg);
},
reject,
this.ExecutorType,
"download",
[url, dst]
);
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

download() Promise resolves early when onProgress is not provided

The success callback only skips resolve(msg) for progress messages when onProgress is truthy. When onProgress is undefined (the parameter is optional), the guard if (onProgress && typeof msg === "string") evaluates to false for the entire block, so resolve(msg) is reached immediately on the first Java progress callback — which arrives long before the download is done.

Java sends PluginResult(Status.OK, progress.toString()) with setKeepCallback(true) for every 500ms progress tick. Because the JS success callback fires for each of these, the Promise resolves on the very first tick carrying a JSON progress string like {"type":"progress","downloaded":65536,...} instead of the destination path.

In practice this affects every call that omits the progress callback:

  • Executor.download(prootUrl, ...) (line ~1812 in Terminal.js)
  • Executor.download(libTalloc, ...) (line ~1818)
  • Executor.download(libproot, ...) (line ~1822)
  • Executor.download(libproot32, ...) (line ~1825)

For these F-Droid downloads, await Executor.download(...) returns almost immediately after the first progress tick, while the actual file write is still ongoing in Java. The install pipeline then proceeds to the marker-creation and extraction steps with potentially incomplete files, causing silent corruption or a tar extraction failure on the subsequent step (which is caught and retried, but wastes an entire install cycle).

Fix: always detect and drop progress messages regardless of whether onProgress is set:

Suggested change
download(url, dst, onProgress) {
return new Promise((resolve, reject) => {
exec(
(msg) => {
if (onProgress && typeof msg === "string") {
try {
const data = JSON.parse(msg);
if (data.type === "progress") {
onProgress(data);
return;
}
} catch (_) {}
}
resolve(msg);
},
reject,
this.ExecutorType,
"download",
[url, dst]
);
});
}
download(url, dst, onProgress) {
return new Promise((resolve, reject) => {
exec(
(msg) => {
if (typeof msg === "string") {
try {
const data = JSON.parse(msg);
if (data.type === "progress") {
onProgress?.(data);
return;
}
} catch (_) {}
}
resolve(msg);
},
reject,
this.ExecutorType,
"download",
[url, dst]
);
});
}

Comment on lines 656 to 676

// If AXS still not running after retries, throw error
if (!(await Terminal.isAxsRunning())) {
toast("Failed to start AXS server after multiple attempts");
//throw new Error("Failed to start AXS server after multiple attempts");
// Re-run installing flow to repair packages / config
const repairOk = await Terminal.startAxs(
true,
console.log,
console.error,
);
if (repairOk) {
// Start AXS again after repair
await Terminal.startAxs(false, () => {}, console.error);
}

if (!(await pollAxs(30))) {
// Still broken — clear .configured so next open re-triggers install
try {
await Terminal.resetConfigured();
} catch (_) {
/* ignore */
}
throw new Error("Failed to start AXS server after repair attempt");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Repair path polls for 30 seconds even when repairOk is false

When Terminal.startAxs(true, ...) returns false (the repair install itself failed), the if (repairOk) guard correctly skips starting AXS, but pollAxs(30) is still called unconditionally on the next line. Since AXS was never started, the poll will always time out, wasting 30 seconds before throwing the error.

if (repairOk) {
    await Terminal.startAxs(false, () => {}, console.error);
}

if (!(await pollAxs(30))) { // still runs even when repairOk === false

Consider short-circuiting immediately when repair fails:

Suggested change
// If AXS still not running after retries, throw error
if (!(await Terminal.isAxsRunning())) {
toast("Failed to start AXS server after multiple attempts");
//throw new Error("Failed to start AXS server after multiple attempts");
// Re-run installing flow to repair packages / config
const repairOk = await Terminal.startAxs(
true,
console.log,
console.error,
);
if (repairOk) {
// Start AXS again after repair
await Terminal.startAxs(false, () => {}, console.error);
}
if (!(await pollAxs(30))) {
// Still broken — clear .configured so next open re-triggers install
try {
await Terminal.resetConfigured();
} catch (_) {
/* ignore */
}
throw new Error("Failed to start AXS server after repair attempt");
}
if (repairOk) {
// Start AXS again after repair
await Terminal.startAxs(false, () => {}, console.error);
} else {
// Repair install itself failed; no point polling
try {
await Terminal.resetConfigured();
} catch (_) {
/* ignore */
}
throw new Error("Failed to repair terminal environment");
}
if (!(await pollAxs(30))) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants