OverviewFilesCommitsIssuesDocsReleasesPeers

cthuloid / android / lib / worklet.js

98 lines · javascript ref:
// 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) })
}