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 = `
