| … | … | @@ -152,6 +152,19 @@ if (cfg.swarm) { |
| 152 | 152 | const shutdown = async () => { try { await node.close() } catch {} ; process.exit(0) } |
| 153 | 153 | process.on('SIGINT', shutdown) |
| 154 | 154 | process.on('SIGTERM', shutdown) |
| 155 | ||
| 156 | // Register the control socket with pgh-sidecar. Without this the sidecar has no udsPath for us, so it | |
| 157 | // cannot push `release-ready` at all — hpp was installed by autoupdate and then just kept running the | |
| 158 | // old code until someone restarted it by hand. Registration also makes `pgh app status` resolve us. | |
| 159 | // Best-effort and non-fatal: the daemon's job is serving the DID, not being manageable. | |
| 160 | try { | |
| 161 | const { maintainSidecarRegistration } = await import('./lib/sidecar.js') | |
| 162 | const ctl = join(cfg.storage, 'control.sock') | |
| 163 | maintainSidecarRegistration({ udsPath: ctl, name: process.env.PGH_APP_NAME || 'hyper-proto-peer', runtime: 'bare', version: null }) // the sidecar treats version as informational; reading package.json here is not worth a failure path | |
| 164 | console.log(JSON.stringify({ msg: 'sidecar registration armed', uds: ctl })) | |
| 165 | } catch (e) { | |
| 166 | console.error(JSON.stringify({ msg: 'sidecar registration unavailable (non-fatal)', error: e.message })) | |
| 167 | } | |
| 155 | 168 | } |
| 156 | 169 | } else { |
| 157 | 170 | const vaultPath = join(cfg.storage, 'peer-identity.json') |
| … | … | @@ -6,6 +6,7 @@ |
| 6 | 6 | import crypto from 'hypercore-crypto' |
| 7 | 7 | import b4a from 'b4a' |
| 8 | 8 | import fs from 'bare-fs' |
| 9 | import process from 'bare-process' // for the OTA respawn exit — Bare has no global process | |
| 9 | 10 | import Hyperswarm from 'hyperswarm' |
| 10 | 11 | import ProtomuxWakeup from 'protomux-wakeup' |
| 11 | 12 | import Protomux from 'protomux' |
| … | … | @@ -494,7 +495,13 @@ export async function bootSwarmDevice ({ storage, deviceMnemonic, did, signingKe |
| 494 | 495 | // stall on an un-converged base's base.update), so a co-located client (favela-web) can ALWAYS reach the |
| 495 | 496 | // device over local IPC even while convergence lags. Owner-gated by the filesystem → no writer-allowlist. |
| 496 | 497 | try { |
| 497 | udsServing = await serveDeviceUds(device, { uds: join(ctlDir, 'control.sock'), blobs }) | |
| 498 | // OTA respawn: a clean close, then exit 75 for the supervisor. Exiting the PROCESS is the daemon's | |
| 499 | // call, not the UDS module's, so it is injected here. | |
| 500 | udsServing = await serveDeviceUds(device, { | |
| 501 | uds: join(ctlDir, 'control.sock'), | |
| 502 | blobs, | |
| 503 | onRelease: () => { Promise.resolve(this.close()).catch(() => {}).then(() => process.exit(75)).catch(() => process.exit(75)) } | |
| 504 | }) | |
| 498 | 505 | console.log(JSON.stringify({ msg: 'device UDS control surface', uds: join(ctlDir, 'control.sock') })) |
| 499 | 506 | } catch (e) { console.error(JSON.stringify({ msg: 'UDS control surface failed (swarm surface still up)', error: e.message })) } |
| 500 | 507 | const allow = Array.isArray(writers) ? writers.map((w) => String(w).toLowerCase()).filter(Boolean) : null |
| … | … | @@ -10,10 +10,26 @@ import b4a from 'b4a' |
| 10 | 10 | import { Service } from 'hyper-svc' |
| 11 | 11 | import { conflictDiag } from './conflict-watch.js' |
| 12 | 12 | |
| 13 | export async function serveDeviceUds (device, { uds, blobs = null } = {}) { | |
| 13 | export async function serveDeviceUds (device, { uds, blobs = null, onRelease = null } = {}) { | |
| 14 | 14 | if (!uds) throw new Error('serveDeviceUds: uds path required') |
| 15 | 15 | const svc = new Service({ name: 'hyper-proto-peer', uds }) |
| 16 | 16 | |
| 17 | // OTA — the app half of the push contract. The sidecar performs the FULL install (tree + dep closure + | |
| 18 | // materialize) and then calls this; our only job is to exit cleanly so the supervisor respawns onto the | |
| 19 | // new tree. Injected rather than exiting here: this module serves a device and must not own the | |
| 20 | // process lifecycle. | |
| 21 | // | |
| 22 | // ⚠ NOT pgh-runtime's fetch path — it fetches root trees WITHOUT dep closures, which produces a broken | |
| 23 | // install (the MODULE_NOT_FOUND respawn loop that took the membrane down on 2026-07-31). | |
| 24 | if (onRelease) { | |
| 25 | svc.handle('release-ready', async (ctx, params = {}) => { | |
| 26 | const ver = (params && params.version) || '?' | |
| 27 | console.log(JSON.stringify({ msg: 'ota: release-ready — sidecar installed; respawning', version: ver })) | |
| 28 | setImmediate(() => { try { onRelease(ver) } catch {} }) | |
| 29 | return { ok: true, willRespawn: true } | |
| 30 | }) | |
| 31 | } | |
| 32 | ||
| 17 | 33 | svc.handle('get-state', async () => device.state()) |
| 18 | 34 | // conflict-diag: hypercore conflicts seen SINCE BOOT. A conflict freezes a core and stops it |
| 19 | 35 | // replicating while the served view stays correct, so a forked node looks healthy on every other |
| … | … | @@ -0,0 +1,56 @@ |
| 1 | // pgh-sidecar registration — makes the daemon a first-class pgh app. Registering our UDS | |
| 2 | // with the sidecar lets `app-lookup` resolve us (so `pgh app status atproto-bridge` and the | |
| 3 | // readiness probe work) and survives the boot race (sidecar socket may appear after us). | |
| 4 | // Mirrors pear-git-v3/lib/sidecar-wait.js + server.js registerWithSidecar. | |
| 5 | ||
| 6 | import fs from '#node:fs' | |
| 7 | import path from '#node:path' | |
| 8 | import os from '#node:os' | |
| 9 | import process from '#node:process' | |
| 10 | ||
| 11 | export function sidecarSockPath () { | |
| 12 | return process.env.PGH_SIDECAR_SOCK || path.join(os.homedir(), '.pgh', 'svc.sock') | |
| 13 | } | |
| 14 | ||
| 15 | // Poll for the sidecar socket (it may be created after this daemon boots — the boot race). | |
| 16 | export async function waitForSidecarSock ({ timeoutMs = 120000, intervalMs = 1000, shouldStop = () => false } = {}) { | |
| 17 | const sock = sidecarSockPath() | |
| 18 | const deadline = Date.now() + timeoutMs | |
| 19 | while (!fs.existsSync(sock)) { | |
| 20 | if (shouldStop()) return false | |
| 21 | if (Date.now() >= deadline) return false | |
| 22 | await new Promise((r) => setTimeout(r, intervalMs)) | |
| 23 | } | |
| 24 | return true | |
| 25 | } | |
| 26 | ||
| 27 | // app-register (last-writer-wins, idempotent) our live UDS + pid with the sidecar. | |
| 28 | export async function registerWithSidecar ({ udsPath, name, runtime = 'bare', version = null }) { | |
| 29 | const sidecarSock = sidecarSockPath() | |
| 30 | if (!fs.existsSync(sidecarSock)) return { registered: false, reason: 'sidecar not running' } | |
| 31 | const installName = name || process.env.PGH_APP_NAME || 'atproto-bridge' | |
| 32 | try { | |
| 33 | const { Client } = await import('hyper-svc') | |
| 34 | const c = await Client.connectUds(sidecarSock) | |
| 35 | await c.call('app-register', { name: installName, udsPath, pid: process.pid, runtime, version }) | |
| 36 | await c.close() | |
| 37 | return { registered: true, name: installName } | |
| 38 | } catch (err) { | |
| 39 | return { registered: false, reason: err.message } | |
| 40 | } | |
| 41 | } | |
| 42 | ||
| 43 | // Register once the sidecar is up, then re-register on an interval (the sidecar keeps its | |
| 44 | // registry in memory, so a sidecar restart silently drops us). Returns a stop() fn. | |
| 45 | export function maintainSidecarRegistration ({ udsPath, name, runtime, version, intervalMs = 30000 } = {}) { | |
| 46 | let timer = null | |
| 47 | let stopped = false | |
| 48 | ;(async () => { | |
| 49 | const ready = await waitForSidecarSock({ shouldStop: () => stopped }) | |
| 50 | if (stopped) return | |
| 51 | if (ready) await registerWithSidecar({ udsPath, name, runtime, version }) | |
| 52 | timer = setInterval(() => { registerWithSidecar({ udsPath, name, runtime, version }) }, intervalMs) | |
| 53 | if (timer.unref) timer.unref() | |
| 54 | })() | |
| 55 | return () => { stopped = true; if (timer) clearInterval(timer) } | |
| 56 | } |
| … | … | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "hyper-proto-peer", |
| 3 | "version": "0.3.99", | |
| 3 | "version": "0.3.100", | |
| 4 | 4 | "description": "Self-custody, P2P-native atproto peer — holds the user's key + content as Hypercores and authors+signs its own repo. The source of truth in the peer/bridge split (Model B); atproto-bridge projects it to bsky.", |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "lib/peer.js", |