251 lines
6.7 KiB
JavaScript
251 lines
6.7 KiB
JavaScript
'use strict'
|
|
|
|
const path = require('path')
|
|
const FramedStream = require('framed-stream')
|
|
const PearRuntime = require('pear-runtime')
|
|
const ReadyResource = require('ready-resource')
|
|
const { SquidManager } = require('./lib/squid-manager')
|
|
const { createWorld, listWorlds, resolveWorldPaths } = require('./lib/worlds')
|
|
const { worldsRoot } = require('./lib/paths')
|
|
const { ensureDir } = require('./lib/worlds')
|
|
const { WorldTunnelHost, WorldTunnelClient } = require('./lib/world-tunnel')
|
|
const { loadOrCreateTunnelKeys } = require('./lib/tunnel-keys')
|
|
const { encodeInvite, decodeInvite, decodeKey } = require('./lib/invite')
|
|
|
|
/**
|
|
* Flying Jib Bare application shell.
|
|
* - Optional pear-runtime worker for OTA (hello-pear-bare pattern)
|
|
* - Squid + HyperDHT tunnels run in this process under Bare (ADR-0013)
|
|
*/
|
|
module.exports = class App extends ReadyResource {
|
|
constructor({ dir, appPath, updates, version, upgrade, name }) {
|
|
super()
|
|
this.dir = dir
|
|
this.appPath = appPath
|
|
this.updates = updates
|
|
this.version = version
|
|
this.upgrade = upgrade
|
|
this.name = name
|
|
this.IPC = null
|
|
this.pipe = null
|
|
this.squid = null
|
|
this.tunnelHost = null
|
|
this.tunnelClient = null
|
|
this.activeWorld = null
|
|
this._shuttingDown = false
|
|
}
|
|
|
|
_open() {
|
|
ensureDir(worldsRoot(this.dir))
|
|
ensureDir(path.join(this.dir, 'corestore'))
|
|
|
|
const enableWorker =
|
|
this.updates !== false && this.upgrade && !String(this.upgrade).includes('<YOUR_KEY')
|
|
|
|
if (enableWorker) {
|
|
try {
|
|
this.IPC = PearRuntime.run(require.resolve('./workers/main.js'), [
|
|
String(this.updates),
|
|
this.version,
|
|
this.upgrade,
|
|
this.name,
|
|
this.dir,
|
|
this.appPath || ''
|
|
])
|
|
this.pipe = new FramedStream(this.IPC)
|
|
this.pipe.on('data', (data) => this._onWorkerMessage(data))
|
|
this.pipe.on('error', (err) => this.emit('error', err))
|
|
this.IPC.on('error', (err) => this.emit('error', err))
|
|
} catch (err) {
|
|
this.emit('message', `[worker] not started: ${err.message}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
async _close() {
|
|
if (this.tunnelClient) {
|
|
await this.tunnelClient.close().catch(() => {})
|
|
this.tunnelClient = null
|
|
}
|
|
if (this.tunnelHost) {
|
|
await this.tunnelHost.close().catch(() => {})
|
|
this.tunnelHost = null
|
|
}
|
|
if (this.squid) {
|
|
await this.squid.close().catch(() => {})
|
|
this.squid = null
|
|
}
|
|
this.activeWorld = null
|
|
const pipe = this.pipe
|
|
const IPC = this.IPC
|
|
this.pipe = null
|
|
this.IPC = null
|
|
pipe?.destroy()
|
|
IPC?.destroy()
|
|
}
|
|
|
|
_onWorkerMessage(data) {
|
|
const message = data.toString()
|
|
if (message === 'updating') {
|
|
this.emit('updating')
|
|
return
|
|
}
|
|
if (message === 'updated') {
|
|
this.emit('updated')
|
|
this._sendWorker('pear:applyUpdate')
|
|
return
|
|
}
|
|
if (message === 'pear:updateApplied') {
|
|
this.emit('update-applied')
|
|
return
|
|
}
|
|
this.emit('message', message)
|
|
}
|
|
|
|
_sendWorker(message) {
|
|
if (this.pipe) this.pipe.write(message)
|
|
}
|
|
|
|
listWorlds() {
|
|
return listWorlds(this.dir)
|
|
}
|
|
|
|
createWorld(opts) {
|
|
return createWorld(this.dir, opts)
|
|
}
|
|
|
|
/**
|
|
* Start Squid for a world (loopback only).
|
|
* @param {{ world: string, port?: number, version?: string }} opts
|
|
*/
|
|
async startWorld(opts) {
|
|
if (this.squid) {
|
|
throw new Error('A world is already running; stop it first')
|
|
}
|
|
const { meta, anvil } = resolveWorldPaths(this.dir, opts.world)
|
|
const port = opts.port || 25565
|
|
const version = opts.version || meta.version
|
|
|
|
this.squid = new SquidManager({
|
|
worldFolder: anvil,
|
|
port,
|
|
version,
|
|
motd: meta.motd || `Flying Jib — ${meta.name}`,
|
|
logging: false
|
|
})
|
|
|
|
this.squid.on('error', (err) => this.emit('error', err))
|
|
this.squid.on('listening', (p) => {
|
|
this.emit('message', `Squid listening on 127.0.0.1:${p}`)
|
|
})
|
|
|
|
await this.squid.ready()
|
|
this.activeWorld = meta
|
|
return this.squid.status
|
|
}
|
|
|
|
/**
|
|
* Start Squid + HyperDHT host tunnel; return status + invite string.
|
|
* @param {{ world: string, port?: number, version?: string }} opts
|
|
*/
|
|
async hostWorld(opts) {
|
|
const squidStatus = await this.startWorld(opts)
|
|
const keys = loadOrCreateTunnelKeys(this.dir, this.activeWorld.id)
|
|
|
|
this.tunnelHost = new WorldTunnelHost({
|
|
localPort: squidStatus.port,
|
|
seed: keys.seed,
|
|
cap: keys.cap
|
|
})
|
|
this.tunnelHost.on('connection', () => {
|
|
this.emit('message', 'Remote peer connected via tunnel')
|
|
})
|
|
this.tunnelHost.on('error', (err) => this.emit('error', err))
|
|
await this.tunnelHost.ready()
|
|
|
|
const invite = encodeInvite({
|
|
type: 'private-world',
|
|
worldKey: keys.publicKeyZ32,
|
|
cap: keys.capZ32,
|
|
name: this.activeWorld.name,
|
|
mcVersion: this.activeWorld.version,
|
|
portHint: squidStatus.port
|
|
})
|
|
|
|
return {
|
|
squid: squidStatus,
|
|
tunnel: this.tunnelHost.status,
|
|
invite
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Join a private world via fj1. invite (client tunnel only; no local Squid).
|
|
* @param {{ invite: string, localPort?: number }} opts
|
|
*/
|
|
async joinWorld(opts) {
|
|
if (this.tunnelClient) {
|
|
throw new Error('Already joined a remote world; stop first')
|
|
}
|
|
const inv = decodeInvite(opts.invite)
|
|
const publicKey = decodeKey(inv.worldKey)
|
|
const cap = decodeKey(inv.cap)
|
|
|
|
this.tunnelClient = new WorldTunnelClient({
|
|
publicKey,
|
|
cap,
|
|
localPort: opts.localPort != null ? Number(opts.localPort) : 0
|
|
})
|
|
this.tunnelClient.on('error', (err) => this.emit('error', err))
|
|
await this.tunnelClient.ready()
|
|
|
|
return {
|
|
invite: inv,
|
|
tunnel: this.tunnelClient.status
|
|
}
|
|
}
|
|
|
|
async stopWorld() {
|
|
let stopped = false
|
|
if (this.tunnelClient) {
|
|
await this.tunnelClient.close().catch(() => {})
|
|
this.tunnelClient = null
|
|
stopped = true
|
|
}
|
|
if (this.tunnelHost) {
|
|
await this.tunnelHost.close().catch(() => {})
|
|
this.tunnelHost = null
|
|
stopped = true
|
|
}
|
|
if (this.squid) {
|
|
await this.squid.close().catch(() => {})
|
|
this.squid = null
|
|
stopped = true
|
|
}
|
|
this.activeWorld = null
|
|
return stopped
|
|
}
|
|
|
|
getStatus() {
|
|
return {
|
|
storage: this.dir,
|
|
world: this.activeWorld,
|
|
squid: this.squid ? this.squid.status : { running: false },
|
|
tunnelHost: this.tunnelHost ? this.tunnelHost.status : null,
|
|
tunnelClient: this.tunnelClient ? this.tunnelClient.status : null
|
|
}
|
|
}
|
|
|
|
async exit(code = 0) {
|
|
if (this._shuttingDown) return
|
|
this._shuttingDown = true
|
|
if (typeof Bare !== 'undefined') Bare.exitCode = code
|
|
try {
|
|
await this.stopWorld()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
await this.close()
|
|
}
|
|
}
|