Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/plugins/terminal/www/Terminal.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,40 @@ const Terminal = {
* @returns {Promise<boolean>} - Returns true if installation completes with exit code 0, void if not installing
*/
async startAxs(installing = false, logger = console.log, err_logger = console.error,failsafe = false) {
// Guard against callers (core terminal UI, or third-party plugins such as
// AI copilots that shell out to run `npm`/`node`) invoking startAxs before
// the Alpine sandbox has ever been provisioned on this device. Without this
// check, init-sandbox.sh does not exist yet and the caller sees a confusing
// "No such file or directory" + unknown node/npm versions instead of the
// sandbox being set up automatically.
if (!this._provisioning && !(await this.isInstalled())) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Setup calls delete shared files

Two concurrent startAxs(true) calls can both read _provisioning as false before awaiting isInstalled(). Both then call install(). Each install deletes the same sandbox before extracting it, so one can remove files the other needs and make setup fail.

Store and share a pending installation promise before the first await, and keep the internal configuration call separate from outside callers.

if (!installing) {
const message = "Linux sandbox is not installed yet. Open Terminal once (or let the plugin install it) before running commands.";
err_logger(message);
throw new Error(message);
}

// install() finishes by calling startAxs(true, ...) itself to apply the
// final sandbox configuration, at a point where isInstalled() is still
// false (the .configured marker is written by init-sandbox.sh during
// that very call). The _provisioning flag prevents that internal call
// from re-entering install() and recursing forever.
logger("Linux sandbox not found, installing it first...");
this._provisioning = true;
let installed;
try {
installed = await this.install(logger, err_logger);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 First-run setup runs twice

The normal first-run UI calls Terminal.install() directly, but install() never sets _provisioning. Its final startAxs(true) reaches this branch while .configured is still missing and starts another full install. The second install deletes the files just extracted, extracts them again, and repeats every download on F-Droid.

Set the guard for the whole install() call, or let its final configuration call bypass provisioning.

} finally {
this._provisioning = false;
}
if (!installed) {
const message = this.lastInstallError || "Failed to install the Linux sandbox required to run this command.";
err_logger(message);
return false;
}
return true;
}

const filesDir = await new Promise((resolve, reject) => {
system.getFilesDir(resolve, reject);
});
Expand Down
Loading