feat: improve terminal install, recovery, and proot compatibility#1926
feat: improve terminal install, recovery, and proot compatibility#1926Ebola-Chan-bot wants to merge 12 commits intoAcode-Foundation:mainfrom
Conversation
Ebola-Chan-bot
commented
Mar 7, 2026
- 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
There was a problem hiding this comment.
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 SummaryThis 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 (
Confidence Score: 2/5
Important Files Changed
Sequence DiagramsequenceDiagram
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
|
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.
695dec4 to
778c3e3
Compare
|
@greptileai review it |
This comment has been minimized.
This comment has been minimized.
|
🔴 (Workflow Trigger stopped), Your On-Demand Preview Release/build for #1926. For Owners: Please Click here to view that github actions |
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.
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.
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.
170774b to
5084651
Compare
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.
- 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
|
@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. |
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.
Thank you. Should be fixed now. |
| 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] | ||
| ); | ||
| }); | ||
| } |
There was a problem hiding this comment.
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:
| 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] | |
| ); | |
| }); | |
| } |
|
|
||
| // 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"); | ||
| } |
There was a problem hiding this comment.
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 === falseConsider short-circuiting immediately when repair fails:
| // 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))) { |