feat: terminal fail safe#2144
Conversation
Greptile SummaryThis PR adds a "failsafe mode" terminal setting that launches the axs server directly via the Android system linker, bypassing proot — intended for devices where proot is broken. To enable the failsafe path it also restructures
Confidence Score: 2/5The normal (non-failsafe) terminal path is broken: axs is never started and the pid file is never written, so every session open fails after a 10-second polling timeout. The refactored init-alpine.sh removed the two lines that made the default terminal work — writing the pid and launching the axs HTTP server — replacing them with a bare exec /bin/bash that has no connection to the axs session infrastructure. Additionally, the failsafe mode's process-management model writes the wrong PID, leaving axs as an orphan after every terminal stop. These two issues together mean neither code path leaves the terminal in a reliably working state. src/plugins/terminal/scripts/init-alpine.sh and src/plugins/terminal/www/Terminal.js (failsafe command block) need the most attention before merging. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[createSession called] --> B{isAxsRunning?}
B -- No --> C[startAxs called]
C --> D{failsafeMode?}
D -- Yes / Failsafe --> E["linker axs -c 'sh -i'\n(no proot)"]
E --> F["echo $$(sh PID) > pid"]
F --> G[axs HTTP server running]
D -- No / Normal --> H[source init-sandbox.sh]
H --> I["proot → init-alpine.sh"]
I --> J["exec /bin/bash --rcfile /initrc\n(NEW: no axs, no pid written ⚠️)"]
J --> K["isAxsRunning polls 10×\n→ always false ⚠️"]
K --> L["Toast: Failed to start AXS\nfetch to localhost fails ⚠️"]
B -- Yes --> M[POST /terminals]
G --> M
M --> N[Terminal session open]
subgraph stopAxs
O["kill -KILL (sh PID)"] --> P[sh dies]
P --> Q["axs orphaned, still on port ⚠️"]
end
Reviews (1): Last reviewed commit: "feat: terminal fail safe" | Re-trigger Greptile |
| if [ "$1" = "--installing" ]; then | ||
| touch "$PREFIX/.configured" | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Fallback to bash if no command specified | ||
| exec /bin/bash --rcfile /initrc |
There was a problem hiding this comment.
axs server and pid file no longer started in normal mode
The old init-alpine.sh wrote echo "$$" > "$PREFIX/pid" and then launched "$PREFIX/axs" -c "bash --rcfile /initrc -i" in the no-args (normal terminal start) branch. The new code replaces both with exec /bin/bash --rcfile /initrc, which runs bash inside proot but never starts the axs HTTP server.
createSession() in terminal.js connects to http://localhost:{port}/terminals — which only works when axs is running. With no pid file, isAxsRunning() always returns false, so startAxs is called on every session open; the 10-second polling loop exhausts, a toast fires, and then the fetch to the axs endpoint fails. The terminal is effectively broken in the default (non-failsafe) mode after this change.
| }).then(async (uuid) => { | ||
| await Executor.write(uuid, `source ${filesDir}/init-sandbox.sh ${installing ? "--installing" : ""}; exit`); | ||
| if (failsafeMode) { | ||
| await Executor.write(uuid, `chmod +x ${filesDir}/axs; echo $$ > ${filesDir}/pid; ${linker} ${filesDir}/axs -c "sh -i"; exit`); |
There was a problem hiding this comment.
Unquoted
${filesDir} will break on paths containing spaces. All three uses should be double-quoted within the shell string.
| await Executor.write(uuid, `chmod +x ${filesDir}/axs; echo $$ > ${filesDir}/pid; ${linker} ${filesDir}/axs -c "sh -i"; exit`); | |
| await Executor.write(uuid, `chmod +x "${filesDir}/axs"; echo $$ > "${filesDir}/pid"; ${linker} "${filesDir}/axs" -c "sh -i"; exit`); |
| if (failsafeMode) { | ||
| await Executor.write(uuid, `chmod +x ${filesDir}/axs; echo $$ > ${filesDir}/pid; ${linker} ${filesDir}/axs -c "sh -i"; exit`); | ||
| } else { |
There was a problem hiding this comment.
Failsafe mode: axs process is orphaned when
stopAxs() is called
echo $$ > ${filesDir}/pid stores the PID of the outer sh shell (started by Executor.start). stopAxs() then does kill -KILL $(cat $PREFIX/pid), which kills that outer shell. However, the ${linker} ${filesDir}/axs child process is not in the same process group and is not covered by --kill-on-exit (that flag is proot-specific). On Android/Linux, killing a parent with SIGKILL orphans its children — axs survives.
On the next createSession(), isAxsRunning() finds the old sh PID dead and returns false, triggering another startAxs. The new axs instance attempts to bind the same port while the old orphaned instance still holds it, causing the new start to fail silently. Users end up with leaked axs processes accumulating across terminal restarts.
Consider writing the axs PID directly — e.g., start axs in the background, capture $!, write that to the pid file, and wait for it — so stopAxs kills the process that actually owns the port.
| if (key === "failsafeMode") { | ||
| toast("Restart terminal to apply changes"); | ||
| } |
There was a problem hiding this comment.
Hardcoded English toast and missing
info-failsafeMode in non-English locales
toast("Restart terminal to apply changes") is not passed through the strings i18n map, so non-English users see English text. A corresponding key (e.g., "restart terminal to apply changes") should be added to all locale files and referenced via strings[...] here.
Additionally, every non-English locale file in this PR only received the "failsafe mode" key; none received "info-failsafeMode". The setting's info field will render as undefined for all non-English users.
|
Wow, Mini Termux-like. |
Closes #2084