From 7cff93419d14668b0b6ba8320e25f85314de5879 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 26 Sep 2026 00:03:26 +0200 Subject: [PATCH] Android: fix app hanging on the loading screen (Node start only returns when Node exits, wait for whenReady instead), build the native bridge with 16 KB page alignment, show an error instead of an endless spinner Co-Authored-By: Claude Opus 5.5 --- mobile/build.js | 9 +++++++++ mobile/dev/electron-dev.js | 4 +++- mobile/dev/preload.js | 4 +++- mobile/web/app.js | 33 +++++++++++++++++++++++++-------- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/mobile/build.js b/mobile/build.js index 41c1cbf..e52e68e 100644 --- a/mobile/build.js +++ b/mobile/build.js @@ -37,7 +37,16 @@ function copy(from, to) { fs.cpSync(from, to, { recursive: true }); } +// capacitor-nodejs baut seine Brückenbibliothek mit 4-KB-Seitenausrichtung; neuere Geräte (z. B. Pixel mit +// 16-KB-Speicherseiten) laden sie dann nicht. Linker-Option einmalig ergänzen. +function patchPlugin() { + const f = path.join(__dirname, 'node_modules', 'capacitor-nodejs', 'android', 'CMakeLists.txt'); + const s = fs.readFileSync(f, 'utf8'); + if (!s.includes('max-page-size=16384')) fs.writeFileSync(f, `${s}\ntarget_link_options(native-lib PRIVATE "-Wl,-z,max-page-size=16384")\n`); +} + function buildWeb() { + patchPlugin(); fs.rmSync(WWW, { recursive: true, force: true }); copy(path.join(__dirname, 'web'), WWW); const nm = path.join(ROOT, 'node_modules', '@xterm'); diff --git a/mobile/dev/electron-dev.js b/mobile/dev/electron-dev.js index 670ef61..ab47576 100644 --- a/mobile/dev/electron-dev.js +++ b/mobile/dev/electron-dev.js @@ -26,8 +26,10 @@ ipcMain.on('dev:send', (_e, name, args) => fromWeb.emit(name, ...args)); ipcMain.handle('dev:start', (_e, env) => { Object.assign(process.env, env, { DATADIR: bridge.getDataPath() }); require('fs').mkdirSync(process.env.DATADIR, { recursive: true }); - require(path.join(WWW, 'nodejs', 'index.js')); + setTimeout(() => { started = true; require(path.join(WWW, 'nodejs', 'index.js')); }, 500); }); +let started; +ipcMain.handle('dev:ready', async () => { while (!started) await new Promise((r) => setTimeout(r, 100)); }); ipcMain.handle('dev:native', (_e, method, arg) => { if (method === 'vaultKey') return { key: crypto.createHash('sha256').update(app.getPath('userData')).digest('base64') }; if (method === 'info') return { device: 'Dev Phone', locale: process.env.MRTERM_DEV_LOCALE || 'en-US', version: require('../../package.json').version }; diff --git a/mobile/dev/preload.js b/mobile/dev/preload.js index e5e13d4..1885e8e 100644 --- a/mobile/dev/preload.js +++ b/mobile/dev/preload.js @@ -6,7 +6,9 @@ const native = (m) => (arg) => ipcRenderer.invoke('dev:native', m, arg); contextBridge.exposeInMainWorld('DevBridge', { NodeJS: { - start: (o) => ipcRenderer.invoke('dev:start', o.env), + // Wie auf Android: start() kehrt erst zurück, wenn Node beendet wird + start: (o) => { ipcRenderer.invoke('dev:start', o.env); return new Promise(() => {}); }, + whenReady: () => ipcRenderer.invoke('dev:ready'), send: ({ eventName, args }) => ipcRenderer.send('dev:send', eventName, args), addListener: (name, fn) => { if (!listeners.has(name)) listeners.set(name, new Set()); listeners.get(name).add(fn); return { remove: () => listeners.get(name).delete(fn) }; }, }, diff --git a/mobile/web/app.js b/mobile/web/app.js index c839cd4..5c042b2 100644 --- a/mobile/web/app.js +++ b/mobile/web/app.js @@ -22,10 +22,10 @@ const api = { return new Promise((resolve, reject) => { const id = ++seq; waits.set(id, { resolve, reject }); - NodeJS.send({ eventName: 'call', args: [id, ch, args] }); + Promise.resolve(NodeJS.send({ eventName: 'call', args: [id, ch, args] })).catch((e) => { waits.delete(id); reject(e); }); }); }, - send(ch, ...args) { NodeJS.send({ eventName: 'send', args: [ch, args] }); }, + send(ch, ...args) { NodeJS.send({ eventName: 'send', args: [ch, args] }).catch?.((e) => console.error(e)); }, on(ch, fn) { if (!events.has(ch)) events.set(ch, new Set()); events.get(ch).add(fn); return () => events.get(ch).delete(fn); }, }; NodeJS.addListener('reply', ({ args: [id, ok, value] }) => { @@ -174,10 +174,15 @@ async function boot() { const info = await Native.info().catch(() => ({ device: 'Android', locale: navigator.language, version: '0.0.0' })); S.info = info; I18N.setLanguage('auto', info.locale); - const ready = new Promise((r) => { const off = NodeJS.addListener('ready', () => r()); void off; }); + const ready = new Promise((r) => NodeJS.addListener('ready', () => r(true))); const { key } = await Native.vaultKey(); - await NodeJS.start({ env: { MRTERM_VAULT_KEY: key, MRTERM_DEVICE: info.device, MRTERM_LOCALE: info.locale, MRTERM_VERSION: info.version } }); - await Promise.race([ready, new Promise((r) => setTimeout(r, 8000))]); + // start() kehrt erst zurück, wenn Node beendet wird – daher nicht darauf warten, sondern auf whenReady() + NodeJS.start({ env: { MRTERM_VAULT_KEY: key, MRTERM_DEVICE: info.device, MRTERM_LOCALE: info.locale, MRTERM_VERSION: info.version } }) + .catch((e) => { if (!/already been started/.test(e.message)) bootError(`Node.js: ${e.message}`); }); + const timeout = (ms, what) => new Promise((_, rej) => setTimeout(() => rej(new Error(what)), ms)); + await Promise.race([whenReady(), timeout(30000, 'The Node.js engine did not start.')]); + // Backend meldet sich mit 'ready'; nach einem Neuladen der Oberfläche läuft es schon → Ping genügt + await Promise.race([ready, api.call('app:version'), timeout(20000, 'The MrTerm backend did not respond.')]); try { Object.assign(S.bio, await Native.bioAvailable()); } catch {} Native.multicast({ on: true }).catch(() => {}); $('#boot').hidden = true; @@ -1015,6 +1020,18 @@ window.mrtermBack = onBack; // wird von MainActivity bei der Zurück-Taste aufge // MainActivity meldet die Bildschirmtastatur (WebView wird dann um ihre Höhe verkleinert) window.mrtermIme = (open) => { document.body.classList.toggle('kbd-open', open); setTimeout(fitViewport, 50); }; -boot().catch((e) => { - $('#boot').innerHTML = `
${esc(e.message || e)}
`; -}); +// whenReady() wartet, bis die Node-Engine Nachrichten annimmt (vorher lehnt send() ab) +async function whenReady() { + for (;;) { + try { await NodeJS.whenReady(); return; } catch (e) { + if (!/not been started/.test(e.message)) throw e; + await new Promise((r) => setTimeout(r, 200)); + } + } +} +function bootError(msg) { + const b = $('#boot'); + if (b.hidden) return toast(msg, 'error'); + b.innerHTML = `
${esc(msg)}
Android ${esc(navigator.userAgent.match(/Android ([\d.]+)/)?.[1] || '')} · ${esc(S.info?.device || '')} · ${esc(S.info?.version || '')}
`; +} +boot().catch((e) => bootError(e.message || String(e)));