Skip to content

Commit fa02c4d

Browse files
authored
fix: show full error when terminal installation fails (#2070)
1 parent 0455f86 commit fa02c4d

2 files changed

Lines changed: 167 additions & 113 deletions

File tree

src/components/terminal/terminalManager.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -442,12 +442,12 @@ class TerminalManager {
442442
const installResult = await Terminal.install(
443443
(message) => {
444444
// Remove stdout/stderr prefix for
445-
const cleanMessage = message.replace(/^(stdout|stderr)\s+/, "");
445+
const cleanMessage = this.formatInstallLog(message);
446446
installTerminal.component.write(`${cleanMessage}\r\n`);
447447
},
448-
(error) => {
448+
(...errorParts) => {
449449
// Remove stdout/stderr prefix
450-
const cleanError = error.replace(/^(stdout|stderr)\s+/, "");
450+
const cleanError = this.formatInstallLog(errorParts);
451451
installTerminal.component.write(
452452
`\x1b[31mError: ${cleanError}\x1b[0m\r\n`,
453453
);
@@ -458,21 +458,34 @@ class TerminalManager {
458458
if (installResult === true) {
459459
return { success: true };
460460
} else {
461+
const error =
462+
Terminal.lastInstallError ||
463+
"Terminal installation failed - process did not exit with code 0";
461464
return {
462465
success: false,
463-
error:
464-
"Terminal installation failed - process did not exit with code 0",
466+
error,
465467
};
466468
}
467469
} catch (error) {
468470
console.error("Terminal installation failed:", error);
469471
return {
470472
success: false,
471-
error: `Terminal installation failed: ${error.message}`,
473+
error: `Terminal installation failed: ${this.formatInstallLog(error)}`,
472474
};
473475
}
474476
}
475477

478+
formatInstallLog(value) {
479+
const values = Array.isArray(value) ? value : [value];
480+
const message = values
481+
.filter((entry) => entry != null)
482+
.map((entry) => Terminal.formatError(entry))
483+
.filter(Boolean)
484+
.join(" ");
485+
486+
return message.replace(/^(stdout|stderr)\s+/, "") || "Unknown error";
487+
}
488+
476489
/**
477490
* Create a terminal for showing installation progress
478491
* @returns {Promise<object>} Installation terminal instance

src/plugins/terminal/www/Terminal.js

Lines changed: 148 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -13,56 +13,55 @@ const Terminal = {
1313
system.getFilesDir(resolve, reject);
1414
});
1515

16-
if (installing) {
17-
return new Promise((resolve, reject) => {
18-
readAsset("init-alpine.sh", async (content) => {
19-
system.writeText(`${filesDir}/init-alpine.sh`, content, logger, err_logger);
20-
});
21-
22-
readAsset("rm-wrapper.sh", async (content) => {
23-
system.deleteFile(`${filesDir}/alpine/bin/rm`, logger, err_logger);
24-
system.writeText(`${filesDir}/alpine/bin/rm`, content, logger, err_logger);
25-
system.setExec(`${filesDir}/alpine/bin/rm`, true, logger, err_logger);
26-
});
27-
28-
readAsset("init-sandbox.sh", (content) => {
29-
system.writeText(`${filesDir}/init-sandbox.sh`, content, logger, err_logger);
30-
31-
Executor.start("sh", (type, data) => {
32-
logger(`${type} ${data}`);
16+
const [initAlpine, rmWrapper, initSandbox] = await Promise.all([
17+
readAsset("init-alpine.sh"),
18+
readAsset("rm-wrapper.sh"),
19+
readAsset("init-sandbox.sh"),
20+
]);
3321

34-
// Check for exit code during installation
35-
if (type === "exit") {
36-
resolve(data === "0");
37-
}
38-
}).then(async (uuid) => {
39-
await Executor.write(uuid, `source ${filesDir}/init-sandbox.sh ${installing ? "--installing" : ""}; exit`);
40-
}).catch((error) => {
41-
err_logger("Failed to start AXS:", error);
42-
resolve(false);
43-
});
44-
});
45-
});
46-
} else {
47-
readAsset("rm-wrapper.sh", async (content) => {
48-
system.deleteFile(`${filesDir}/alpine/bin/rm`, logger, err_logger);
49-
system.writeText(`${filesDir}/alpine/bin/rm`, content, logger, err_logger);
50-
system.setExec(`${filesDir}/alpine/bin/rm`, true, logger, err_logger);
51-
});
22+
await writeText(`${filesDir}/init-alpine.sh`, initAlpine);
23+
await writeText(`${filesDir}/init-sandbox.sh`, initSandbox);
5224

53-
readAsset("init-alpine.sh", async (content) => {
54-
system.writeText(`${filesDir}/init-alpine.sh`, content, logger, err_logger);
55-
});
25+
await deleteFile(`${filesDir}/alpine/bin/rm`).catch(() => {});
26+
await writeText(`${filesDir}/alpine/bin/rm`, rmWrapper);
27+
await setExec(`${filesDir}/alpine/bin/rm`, true);
5628

57-
readAsset("init-sandbox.sh", (content) => {
58-
system.writeText(`${filesDir}/init-sandbox.sh`, content, logger, err_logger);
29+
if (installing) {
30+
return new Promise((resolve, reject) => {
31+
let lastError = "";
5932

6033
Executor.start("sh", (type, data) => {
6134
logger(`${type} ${data}`);
35+
36+
if (type === "stderr" && data) {
37+
lastError = lastError ? `${lastError}\n${data}` : data;
38+
}
39+
40+
// Check for exit code during installation
41+
if (type === "exit") {
42+
const success = data === "0";
43+
if (!success) {
44+
this.lastInstallError = lastError
45+
? `Sandbox configuration failed with exit code ${data}: ${lastError}`
46+
: `Sandbox configuration failed with exit code ${data}`;
47+
}
48+
resolve(success);
49+
}
6250
}).then(async (uuid) => {
6351
await Executor.write(uuid, `source ${filesDir}/init-sandbox.sh ${installing ? "--installing" : ""}; exit`);
52+
}).catch((error) => {
53+
const message = `Failed to start AXS: ${formatError(error)}`;
54+
this.lastInstallError = message;
55+
err_logger(message);
56+
resolve(false);
6457
});
6558
});
59+
} else {
60+
Executor.start("sh", (type, data) => {
61+
logger(`${type} ${data}`);
62+
}).then(async (uuid) => {
63+
await Executor.write(uuid, `source ${filesDir}/init-sandbox.sh ${installing ? "--installing" : ""}; exit`);
64+
});
6665
}
6766
},
6867

@@ -104,6 +103,7 @@ const Terminal = {
104103
*/
105104
async install(logger = console.log, err_logger = console.error) {
106105
if (!(await this.isSupported())) return false;
106+
this.lastInstallError = "";
107107

108108
try {
109109
//cleanup before insatll
@@ -154,62 +154,26 @@ const Terminal = {
154154

155155

156156
logger("⬇️ Downloading sandbox filesystem...");
157-
await new Promise((resolve, reject) => {
158-
cordova.plugin.http.downloadFile(
159-
alpineUrl, {}, {},
160-
cordova.file.dataDirectory + "alpine.tar.gz",
161-
resolve, reject
162-
);
163-
});
157+
await downloadFile(alpineUrl, cordova.file.dataDirectory + "alpine.tar.gz", "Sandbox filesystem");
164158

165159
logger("⬇️ Downloading axs...");
166-
await new Promise((resolve, reject) => {
167-
cordova.plugin.http.downloadFile(
168-
axsUrl, {}, {},
169-
cordova.file.dataDirectory + "axs",
170-
resolve, reject
171-
);
172-
});
160+
await downloadFile(axsUrl, cordova.file.dataDirectory + "axs", "AXS");
173161

174162
const isFdroid = await Executor.execute("echo $FDROID");
175163
if (isFdroid === "true") {
176164
logger("🐧 F-Droid flavor detected, downloading additional files...");
177165
logger("⬇️ Downloading compatibility layer...");
178-
await new Promise((resolve, reject) => {
179-
cordova.plugin.http.downloadFile(
180-
prootUrl, {}, {},
181-
cordova.file.dataDirectory + "libproot-xed.so",
182-
resolve, reject
183-
);
184-
});
166+
await downloadFile(prootUrl, cordova.file.dataDirectory + "libproot-xed.so", "Compatibility layer");
185167

186168
logger("⬇️ Downloading supporting library...");
187-
await new Promise((resolve, reject) => {
188-
cordova.plugin.http.downloadFile(
189-
libTalloc, {}, {},
190-
cordova.file.dataDirectory + "libtalloc.so.2",
191-
resolve, reject
192-
);
193-
});
169+
await downloadFile(libTalloc, cordova.file.dataDirectory + "libtalloc.so.2", "Supporting library");
194170

195171
if (libproot != null) {
196-
await new Promise((resolve, reject) => {
197-
cordova.plugin.http.downloadFile(
198-
libproot, {}, {},
199-
cordova.file.dataDirectory + "libproot.so",
200-
resolve, reject
201-
);
202-
});
172+
await downloadFile(libproot, cordova.file.dataDirectory + "libproot.so", "proot loader");
203173
}
204174

205175
if (libproot32 != null) {
206-
await new Promise((resolve, reject) => {
207-
cordova.plugin.http.downloadFile(
208-
libproot32, {}, {},
209-
cordova.file.dataDirectory + "libproot32.so",
210-
resolve, reject
211-
);
212-
});
176+
await downloadFile(libproot32, cordova.file.dataDirectory + "libproot32.so", "32-bit proot loader");
213177
}
214178

215179
}
@@ -218,39 +182,37 @@ const Terminal = {
218182

219183
logger("📁 Setting up directories...");
220184

221-
await new Promise((resolve, reject) => {
222-
system.mkdirs(`${filesDir}/.downloaded`, resolve, reject);
223-
});
185+
await ensureDir(`${filesDir}/.downloaded`);
224186

225187
const alpineDir = `${filesDir}/alpine`;
226188

227-
await new Promise((resolve, reject) => {
228-
system.mkdirs(alpineDir, resolve, reject);
229-
});
189+
await ensureDir(alpineDir);
230190

231191
logger("📦 Extracting sandbox filesystem...");
232192
await Executor.execute(`tar --no-same-owner -xf ${filesDir}/alpine.tar.gz -C ${alpineDir}`);
233193

234194
logger("⚙️ Applying basic configuration...");
235-
system.writeText(`${alpineDir}/etc/resolv.conf`, `nameserver 8.8.4.4 \nnameserver 8.8.8.8`);
195+
await writeText(`${alpineDir}/etc/resolv.conf`, `nameserver 8.8.4.4 \nnameserver 8.8.8.8`);
236196

237-
readAsset("rm-wrapper.sh", async (content) => {
238-
system.deleteFile(`${alpineDir}/bin/rm`, logger, err_logger);
239-
system.writeText(`${alpineDir}/bin/rm`, content, logger, err_logger);
240-
system.setExec(`${alpineDir}/bin/rm`, true, logger, err_logger);
241-
});
197+
const rmWrapper = await readAsset("rm-wrapper.sh");
198+
await deleteFile(`${alpineDir}/bin/rm`).catch(() => {});
199+
await writeText(`${alpineDir}/bin/rm`, rmWrapper);
200+
await setExec(`${alpineDir}/bin/rm`, true);
242201

243202
logger("✅ Extraction complete");
244-
await new Promise((resolve, reject) => {
245-
system.mkdirs(`${filesDir}/.extracted`, resolve, reject);
246-
});
203+
await ensureDir(`${filesDir}/.extracted`);
247204

248205
logger("⚙️ Updating sandbox enviroment...");
249206
const installResult = await this.startAxs(true, logger, err_logger);
207+
if (!installResult) {
208+
throw new Error(this.lastInstallError || "Sandbox configuration failed.");
209+
}
250210
return installResult;
251211

252212
} catch (e) {
253-
err_logger("Installation failed:", e);
213+
const message = formatError(e);
214+
this.lastInstallError = message;
215+
err_logger(`Installation failed: ${message}`);
254216
console.error("Installation failed:", e);
255217
return false;
256218
}
@@ -439,20 +401,99 @@ const Terminal = {
439401
reject(result);
440402
}
441403
});
442-
}
404+
},
405+
406+
formatError
443407
};
444408

445409

446410
function readAsset(assetPath, callback) {
447411
const assetUrl = "file:///android_asset/" + assetPath;
448412

449-
window.resolveLocalFileSystemURL(assetUrl, fileEntry => {
450-
fileEntry.file(file => {
451-
const reader = new FileReader();
452-
reader.onloadend = () => callback(reader.result);
453-
reader.readAsText(file);
454-
}, console.error);
455-
}, console.error);
413+
const promise = new Promise((resolve, reject) => {
414+
window.resolveLocalFileSystemURL(assetUrl, fileEntry => {
415+
fileEntry.file(file => {
416+
const reader = new FileReader();
417+
reader.onloadend = () => resolve(reader.result);
418+
reader.onerror = () => reject(reader.error || new Error(`Failed to read ${assetPath}`));
419+
reader.readAsText(file);
420+
}, reject);
421+
}, reject);
422+
});
423+
424+
if (callback) {
425+
promise.then(callback).catch(console.error);
426+
}
427+
428+
return promise;
429+
}
430+
431+
function fileExists(path) {
432+
return new Promise((resolve, reject) => {
433+
system.fileExists(path, false, (result) => {
434+
resolve(result == 1);
435+
}, reject);
436+
});
437+
}
438+
439+
async function ensureDir(path) {
440+
if (await fileExists(path)) return;
441+
442+
await new Promise((resolve, reject) => {
443+
system.mkdirs(path, resolve, reject);
444+
});
445+
}
446+
447+
function writeText(path, content) {
448+
return new Promise((resolve, reject) => {
449+
system.writeText(path, content, resolve, reject);
450+
});
451+
}
452+
453+
function deleteFile(path) {
454+
return new Promise((resolve, reject) => {
455+
system.deleteFile(path, resolve, reject);
456+
});
457+
}
458+
459+
function setExec(path, executable) {
460+
return new Promise((resolve, reject) => {
461+
system.setExec(path, executable, resolve, reject);
462+
});
463+
}
464+
465+
function downloadFile(url, destination, label) {
466+
return new Promise((resolve, reject) => {
467+
cordova.plugin.http.downloadFile(
468+
url, {}, {},
469+
destination,
470+
resolve,
471+
(error) => reject(new Error(`${label} download failed: ${formatError(error)}`))
472+
);
473+
});
474+
}
475+
476+
function formatError(error) {
477+
if (error == null) return "Unknown error";
478+
if (error instanceof Error) return error.message || String(error);
479+
if (typeof error === "string") return error || "Unknown error";
480+
if (typeof error === "object") {
481+
const parts = [];
482+
if (error.status != null) parts.push(`status ${error.status}`);
483+
if (error.error) parts.push(String(error.error));
484+
if (error.message) parts.push(String(error.message));
485+
if (error.exception) parts.push(String(error.exception));
486+
if (error.url) parts.push(`URL: ${error.url}`);
487+
if (parts.length) return parts.join(" - ");
488+
489+
try {
490+
return JSON.stringify(error);
491+
} catch (jsonError) {
492+
return String(error);
493+
}
494+
}
495+
496+
return String(error);
456497
}
457498

458-
module.exports = Terminal;
499+
module.exports = Terminal;

0 commit comments

Comments
 (0)