OverviewFilesCommitsIssuesDocsReleasesPeers

mirall-cli / cli / capstone-xmachine.js

116 lines · javascript ref:
// P1 CROSS-MACHINE capstone: one `mirall daemon` per machine, driven through the real
// `mirall` CLI, mirroring a folder stortron → tabertron over the public DHT.
//   stortron: serves an owned folder      tabertron (here): mirrors it
// Run from tabertron: bare cli/capstone-xmachine.js
const fs = require('bare-fs')
const path = require('bare-path')
const { spawn } = require('bare-subprocess')

const REPO = path.join(__dirname, '..')
const BARE = Bare.argv[0]
const MIRALL = path.join(REPO, 'bin', 'mirall.js')
const ST_DIR = '~/mirall-p0'

const ST = { sock: '/tmp/mcap/st.sock', store: '/tmp/mcap/st-store', dl: '/tmp/mcap/st-dl', share: '/tmp/mcap/st-share' }
const TB = { sock: '/tmp/mcap/tb.sock', store: '/tmp/mcap/tb-store', dl: '/tmp/mcap/tb-dl', mirror: '/tmp/mcap/tb-mirror' }

let failed = false
function ok (cond, msg) { console.log((cond ? '  ok   ' : '  FAIL ') + msg); if (!cond) failed = true }
function sleep (ms) { return new Promise((r) => { const t = setTimeout(r, ms); t.unref && t.unref() }) }

function run (cmd, args, { timeout = 90000 } = {}) {
  return new Promise((resolve) => {
    const child = spawn(cmd, args)
    let out = '', err = ''
    child.stdout.on('data', (d) => { out += d })
    child.stderr.on('data', (d) => { err += d })
    const to = setTimeout(() => { try { child.kill() } catch {} }, timeout); to.unref && to.unref()
    child.on('exit', (code) => { clearTimeout(to); resolve({ code, out, err }) })
    child.on('error', (e) => { clearTimeout(to); resolve({ code: -1, out, err: err + String(e) }) })
  })
}
function sshQuote (s) { return "'" + s.replace(/'/g, "'\\''") + "'" }
function storSh (cmdline) { return run('ssh', ['stortron', `bash -lc ${sshQuote('cd ' + ST_DIR + ' && ' + cmdline)}`]) }
function storMirall (argstr) { return storSh('./node_modules/.bin/bare bin/mirall.js ' + argstr) }
function tabMirall (args) { return run(BARE, [MIRALL, ...args]) }
function parse (res, label) {
  if (res.code !== 0) throw new Error(label + ' failed (code ' + res.code + '): ' + (res.err.trim() || res.out.trim()).slice(0, 300))
  try { return JSON.parse(res.out) } catch { throw new Error(label + ' bad JSON: ' + res.out.slice(0, 200)) }
}
async function waitUntil (name, fn, { ms = 120000, every = 3000 } = {}) {
  const start = Date.now()
  for (;;) {
    try { if (await fn()) return } catch {}
    if (Date.now() - start > ms) throw new Error('timeout waiting for: ' + name)
    await sleep(every)
  }
}

let tbDaemon = null
async function cleanup () {
  console.log('--- cleanup ---')
  try { await storSh("pkill -f '/tmp/mcap/st.sock' || true") } catch {}
  if (tbDaemon) { try { tbDaemon.kill() } catch {} }
}

;(async () => {
  try {
    // Fresh dirs on both machines (/tmp/mcap is per-machine).
    await storSh('rm -rf /tmp/mcap && mkdir -p ' + ST.share)
    fs.rmSync('/tmp/mcap', { recursive: true, force: true })
    for (const d of [TB.store, TB.dl, TB.mirror]) fs.mkdirSync(d, { recursive: true })

    // Boot one daemon per machine.
    await storSh(`setsid nohup ./node_modules/.bin/bare bin/mirall.js daemon --storage ${ST.store} --downloads ${ST.dl} --sock ${ST.sock} >/tmp/mcap/daemon.log 2>&1 & echo started`)
    tbDaemon = spawn(BARE, [MIRALL, 'daemon', '--storage', TB.store, '--downloads', TB.dl, '--sock', TB.sock], { stdio: 'ignore' })

    await waitUntil('tabertron daemon sock', async () => fs.existsSync(TB.sock), { ms: 40000, every: 1000 })
    await waitUntil('stortron daemon sock', async () => (await storSh(`test -S ${ST.sock} && echo Y`)).out.includes('Y'), { ms: 40000, every: 2000 })
    ok(true, 'one mirall daemon up on each machine')

    // --- stortron: become a peer + serve a folder ---
    const stPub = parse(await storMirall(`profile set --name Stortron --sock ${ST.sock}`), 'stor profile:set').publicKey
    const spaceId = parse(await storMirall(`space create --name Photos --sock ${ST.sock}`), 'stor space:create').spaceId
    const inviteRes = await storMirall(`space invite --space ${spaceId} --sock ${ST.sock}`)
    if (inviteRes.code !== 0) throw new Error('stor space:invite failed: ' + inviteRes.err)
    const invite = inviteRes.out.trim() // the CLI prints the raw invite string (not JSON)
    const shareId = parse(await storMirall(`share create --space ${spaceId} --name Photos --sock ${ST.sock}`), 'stor share:create').id
    await storSh(`printf 'hello cross-machine mirror' > ${ST.share}/hello.txt && seq 1 5000 > ${ST.share}/data.bin`)
    parse(await storMirall(`share mount --space ${spaceId} --share ${shareId} --path ${ST.share} --sock ${ST.sock}`), 'stor share:mount')
    ok(true, 'stortron serves an owned folder (hello.txt + data.bin) in space ' + spaceId)

    // --- tabertron: join + mirror ---
    parse(await tabMirall(['profile', 'set', '--name', 'Tabertron', '--sock', TB.sock]), 'tab profile:set')
    parse(await tabMirall(['space', 'join', '--invite', invite, '--sock', TB.sock]), 'tab space:join')
    ok(true, 'tabertron joined the space')

    // Seeing the peer's share proves the two daemons connected over the DHT + replicated the catalog.
    await waitUntil('tabertron sees stortron\'s share', async () => {
      const r = await tabMirall(['share', 'list', '--space', spaceId, '--sock', TB.sock])
      if (r.code !== 0) return false
      try { return JSON.parse(r.out).some((s) => s.id === shareId) } catch { return false }
    }, { ms: 150000, every: 3000 })
    ok(true, 'tabertron sees the share (cross-machine connection + catalog replicated)')

    parse(await tabMirall(['mirror', '--space', spaceId, '--owner', stPub, '--share', shareId, '--path', TB.mirror, '--sock', TB.sock]), 'tab mirror')
    await waitUntil('files materialize on tabertron', async () => fs.existsSync(path.join(TB.mirror, 'hello.txt')) && fs.existsSync(path.join(TB.mirror, 'data.bin')), { ms: 150000, every: 3000 })

    // --- verify bytes, cross-machine ---
    const txt = fs.readFileSync(path.join(TB.mirror, 'hello.txt')).toString()
    ok(txt === 'hello cross-machine mirror', 'hello.txt content matches across machines')
    const stSha = (await storSh(`sha256sum ${ST.share}/data.bin`)).out.trim().split(/\s+/)[0]
    const tbSha = (await run('sha256sum', [path.join(TB.mirror, 'data.bin')])).out.trim().split(/\s+/)[0]
    ok(stSha && stSha === tbSha, 'data.bin sha256 matches across machines (' + (stSha || '?').slice(0, 12) + '…)')

    console.log(failed
      ? 'CAPSTONE FAIL'
      : 'CAPSTONE PASS: stortron served a folder; tabertron mirrored it over the DHT; bytes match — one mirall daemon per machine, all via the CLI')
  } catch (err) {
    console.log('CAPSTONE FAIL:', err.message)
    failed = true
  } finally {
    await cleanup()
  }
  Bare.exit(failed ? 1 : 0)
})()