'use strict' // One SDL/C Cable client and one Cabal Core JavaScript client share a local // Hyperswarm bootstrap node, discover one another, and exchange chat records. const assert = require('assert') const { spawn } = require('child_process') const crypto = require('crypto') const dgram = require('dgram') const fs = require('fs') const os = require('os') const path = require('path') const readline = require('readline') const { startHyperswarm, loadCabalCore } = require('./cable_hyperswarm') const binary = path.resolve(process.argv[2] || 'build/linux/test_cable_thang') const { Cabal, requireCabal } = loadCabalCore() const DHT = requireCabal('hyperdht') const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cable-js-c-live-')) const roomKey = crypto.randomBytes(32).toString('hex') const address = 'cabal://' + roomKey const cHome = path.join(root, 'c-client') const jsHome = path.join(root, 'js-client') const cLines = [] const cWaiters = [] let cClient = null let cabal = null let bootstrapper = null let relay = null function freeUdpPort () { return new Promise((resolve, reject) => { const socket = dgram.createSocket('udp4') socket.once('error', reject) socket.bind(0, '127.0.0.1', () => { const port = socket.address().port socket.close(err => err ? reject(err) : resolve(port)) }) }) } function ready (instance) { return new Promise((resolve, reject) => instance.ready(err => err ? reject(err) : resolve())) } function waitFor (predicate, description, timeoutMs = 90000) { const started = Date.now() return new Promise((resolve, reject) => { const timer = setInterval(() => { if (Date.now() - started > timeoutMs) { clearInterval(timer) reject(new Error('timeout waiting for ' + description)) return } try { const value = predicate() if (value) { clearInterval(timer); resolve(value) } } catch (err) { clearInterval(timer); reject(err) } }, 50) }) } function cLine (line) { cLines.push(line) process.stdout.write('C> ' + line + '\n') for (let i = cWaiters.length - 1; i >= 0; i--) { if (cWaiters[i].predicate(line)) { const waiter = cWaiters.splice(i, 1)[0] clearTimeout(waiter.timer) waiter.resolve(line) } } } function waitC (predicate, description, timeoutMs = 90000) { const existing = cLines.find(predicate) if (existing) return Promise.resolve(existing) return new Promise((resolve, reject) => { const waiter = { predicate, resolve, reject, timer: null } waiter.timer = setTimeout(() => { const index = cWaiters.indexOf(waiter) if (index >= 0) cWaiters.splice(index, 1) reject(new Error('timeout waiting for C client ' + description)) }, timeoutMs) cWaiters.push(waiter) }) } async function main () { fs.mkdirSync(cHome, { recursive: true }) fs.mkdirSync(jsHome, { recursive: true }) const port = await freeUdpPort() const relayPort = await freeUdpPort() const jsPort = await freeUdpPort() const cPort = await freeUdpPort() bootstrapper = DHT.bootstrapper(port, '127.0.0.1', { host: '127.0.0.1' }) await bootstrapper.ready() relay = new DHT({ bootstrap: ['127.0.0.1:' + port], host: '127.0.0.1', port: relayPort, ephemeral: false, firewalled: false }) await relay.ready() const endpoint = '127.0.0.1:' + relayPort process.env.CABLE_BOOTSTRAP = endpoint process.env.CABLE_DHT_HOST = '127.0.0.1' process.env.CABLE_DHT_ADVERTISE_HOST = '127.0.0.1' process.env.CABLE_WAIT_DHT = '1' console.log('Local Hyperswarm bootstrap listening at ' + endpoint) cabal = Cabal(jsHome, roomKey) await ready(cabal) if (process.env.CABLE_DEBUG) { const key = await new Promise((resolve, reject) => cabal.getLocalKey((err, value) => err ? reject(err) : resolve(value))) console.log('JS test storage: ' + jsHome) console.log('JS Cabal user key: ' + key) } const jsPeers = new Set() const jsMessages = [] cabal.on('peer-added', key => { jsPeers.add(key); console.log('JS peer up: ' + key) }) cabal.on('peer-dropped', key => jsPeers.delete(key)) cabal.kcore.api.messages.events.on('message', message => { const value = message && message.value const content = value && value.content if (value && value.type === 'chat/text' && content && typeof content.text === 'string') { jsMessages.push(content.text) console.log('JS chat: ' + content.text) } }) cClient = spawn(binary, [address], { cwd: process.cwd(), env: { ...process.env, HOME: cHome, SDL_VIDEODRIVER: 'dummy', SDL_AUDIODRIVER: 'dummy', CABLE_BOOTSTRAP: endpoint, CABLE_DHT_HOST: '127.0.0.1', CABLE_DHT_PORT: String(cPort) }, stdio: ['pipe', 'pipe', 'inherit'] }) cClient.stdin.on('error', () => {}) cClient.once('error', err => { for (const waiter of cWaiters) { clearTimeout(waiter.timer); waiter.reject(err) } cWaiters.length = 0 }) readline.createInterface({ input: cClient.stdout }).on('line', cLine) const cReady = await waitC(line => line.startsWith('C_READY '), 'S7-backed Cabal room startup') const cFeedKey = cReady.slice('C_READY '.length) await startHyperswarm(cabal, { host: '127.0.0.1', port: jsPort, advertiseHost: '127.0.0.1', waitForBootstrap: true }) await Promise.all([ waitFor(() => jsPeers.size > 0, 'C client discovered by JS Cabal Core'), waitC(line => line === 'PEER_UP', 'JS client discovered by C Cable') ]) const jsKey = await new Promise((resolve, reject) => cabal.getLocalKey((err, key) => err ? reject(err) : resolve(key))) assert.notStrictEqual(jsKey, cFeedKey, 'independent Cabal users must have distinct feed keys') assert(jsPeers.has(cFeedKey), 'JS Cabal Core must identify the C client feed') await new Promise((resolve, reject) => cabal.publishNick('js-peer', err => err ? reject(err) : resolve())) await new Promise((resolve, reject) => cabal.publish({ type: 'chat/text', content: { channel: 'default', text: 'message from cabal js' } }, err => err ? reject(err) : resolve())) await waitC(line => line.includes('message from cabal js'), 'JS-to-C message delivery') cClient.stdin.write('SEND message from sdl c\n') await waitFor(() => jsMessages.includes('message from sdl c'), 'C-to-JS Hypercore feed replication') console.log('C SDL / Cabal JS Hyperswarm discovery and bidirectional chat: PASS') } main().catch(err => { console.error(err.stack || err); process.exitCode = 1 }).finally(async () => { if (cClient && cClient.exitCode === null) { try { cClient.stdin.write('QUIT\n') } catch (_) {} await Promise.race([new Promise(resolve => cClient.once('exit', resolve)), new Promise(resolve => setTimeout(resolve, 2000))]) if (cClient.exitCode === null) cClient.kill('SIGTERM') } if (cabal) { if (cabal._swarm && cabal._swarm.__shutdown) { try { await cabal._swarm.__shutdown() } catch (_) {} } try { await cabal.close() } catch (_) {} } if (relay) { try { await relay.destroy() } catch (_) {} } if (bootstrapper) { try { await bootstrapper.destroy() } catch (_) {} } const resolved = path.resolve(root) if (resolved.startsWith(path.resolve(os.tmpdir()) + path.sep) && path.basename(resolved).startsWith('cable-js-c-live-')) fs.rmSync(resolved, { recursive: true, force: true }) })