// The worklet's entry: Bare, inside the Android app. Runs the SAME controller as
// stortron (cthuloid-web's createApp) with the page files embedded, because a
// packed bundle has no files on disk, and tells the React Native side where the
// page is.
//
// IPC is newline-delimited JSON over BareKit.IPC (bytes both ways).
// Worklet -> shell:
// { t: 'log', msg } progress, also printed to the console (logcat)
// { t: 'ready', url } the page is being served -- point the WebView here
// { t: 'fatal', error } the controller could not start
// Shell -> worklet: the lamp finder's events (lib/ipc-discovery.js).
//
// Storage arrives as the ONE argv entry. Nothing else is ever positional: in a
// later version the APK and the bundle can differ, and a new positional argument
// handed to an old bundle is read as something else (favela's argv trap).
/* global BareKit, Bare */
import fs from 'bare-fs'
import path from 'bare-path'
import { createApp } from 'cthuloid-web/lib/app.js'
import assets from 'cthuloid-web/lib/assets.generated.js'
import { storageFromArgv } from './argv.js'
import { IpcDiscovery } from './ipc-discovery.js'
const IPC = typeof BareKit !== 'undefined' ? BareKit.IPC : null
// Lamps found by the shell's NsdManager arrive here over IPC.
export const discovery = new IpcDiscovery()
if (IPC) {
let buf = Buffer.alloc(0)
// 'data', not read(cb): BareKit.IPC is a streamx Duplex (favela-surface-android).
// Split on the newline BYTE, then decode: a chunk can end mid-character.
IPC.on('data', (data) => {
buf = Buffer.concat([buf, Buffer.from(data)])
let i
while ((i = buf.indexOf(10)) !== -1) {
const line = buf.subarray(0, i).toString()
buf = buf.subarray(i + 1)
let m
try { m = JSON.parse(line) } catch { continue }
if (discovery.handle(m)) console.log('[cthuloid] ' + line)
}
})
}
function send (msg) {
const line = JSON.stringify(msg)
console.log('[cthuloid] ' + line)
if (IPC) {
try { IPC.write(Buffer.from(line + '\n')) } catch { /* the UI went away; keep serving */ }
}
}
// Reuse last run's port, so a restarted worklet serves the URL the WebView knows.
function lastPort (storage) {
try {
const p = JSON.parse(fs.readFileSync(path.join(storage, 'port.json'), 'utf8')).port
return Number.isInteger(p) && p > 0 && p < 65536 ? p : 0
} catch {
return 0
}
}
async function listenPreferring (storage, port) {
const configDir = path.join(storage, 'cthuloid-web')
let app = null
try {
app = createApp({ configDir, port, assets, discovery })
return { app, ...(await app.listen()) }
} catch (err) {
if (port === 0) throw err
// Stop the half-started one, or its rig stays subscribed to discovery.
try { await app.close() } catch {}
send({ t: 'log', msg: 'port ' + port + ' busy (' + err.message + '), taking any' })
app = createApp({ configDir, port: 0, assets, discovery })
return { app, ...(await app.listen()) }
}
}
export async function start (storage) {
fs.mkdirSync(storage, { recursive: true })
send({ t: 'log', msg: 'starting the controller' })
const { app, host, port } = await listenPreferring(storage, lastPort(storage))
try { fs.writeFileSync(path.join(storage, 'port.json'), JSON.stringify({ port })) } catch {}
const url = 'http://' + host + ':' + port + '/'
send({ t: 'ready', url })
return { app, url }
}
const storage = typeof Bare !== 'undefined' ? storageFromArgv(Bare.argv) : null
if (storage && !globalThis.CTHULOID_NO_AUTOSTART) {
start(storage).catch((err) => send({ t: 'fatal', error: (err && err.stack) || String(err) }))
} else if (!globalThis.CTHULOID_NO_AUTOSTART) {
// Never silent again: say why nothing started (0.1.0 hung with no output at all).
send({ t: 'fatal', error: 'no storage directory in argv: ' + JSON.stringify(typeof Bare !== 'undefined' ? Bare.argv : null) })
}