Replace ad-hoc console output with a structured logger (pretty/JSON, levels, redaction, optional file rotation), a clean startup banner with Docker/feature probes, graceful shutdown signals, and slow-RPC warnings.
This commit is contained in:
+127
-41
@@ -2,8 +2,7 @@
|
||||
* peardock server entry point.
|
||||
*
|
||||
* HyperDHT listener + protomux-rpc API for remote Docker management.
|
||||
*
|
||||
* Clients connect with the server public key (printed on start / stored as SERVER_PUBLIC_KEY).
|
||||
* Clients connect with the server public key printed at startup.
|
||||
*/
|
||||
import DHT from 'hyperdht'
|
||||
import b4a from 'b4a'
|
||||
@@ -23,18 +22,34 @@ import {
|
||||
restoreTunnelsFromDisk,
|
||||
} from './services/holesail-tunnels.js'
|
||||
import { restoreSchedules } from './services/schedules.js'
|
||||
import { docker } from './services/docker.js'
|
||||
import logger from './utils/logger.js'
|
||||
import { isSwarmEnabled } from './handlers/swarm.js'
|
||||
import { isPluginsEnabled } from './handlers/plugins.js'
|
||||
|
||||
const { keyPair, publicKeyHex } = loadOrCreateKeyPair()
|
||||
const bootStarted = Date.now()
|
||||
const log = logger.child('server')
|
||||
|
||||
/** @type {{ keyPair: any, publicKeyHex: string }} */
|
||||
let keyPair
|
||||
let publicKeyHex
|
||||
try {
|
||||
;({ keyPair, publicKeyHex } = loadOrCreateKeyPair())
|
||||
} catch (err) {
|
||||
log.error('Failed to load server keypair', { error: err.message })
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const dht = new DHT()
|
||||
const server = dht.createServer()
|
||||
|
||||
server.on('connection', (socket) => {
|
||||
const peerId = socket.remotePublicKey ? b4a.toString(socket.remotePublicKey, 'hex') : null
|
||||
// Only hard-reject revoked peers here; allowlist + invite redeem run at handshake
|
||||
const peerId = socket.remotePublicKey
|
||||
? b4a.toString(socket.remotePublicKey, 'hex')
|
||||
: null
|
||||
|
||||
if (peerId && isPeerRevoked(peerId)) {
|
||||
logger.warn('Rejected revoked peer', { peerId: peerId.slice(0, 12) })
|
||||
log.warn('Rejected revoked peer', { peerId: peerId.slice(0, 12) })
|
||||
try {
|
||||
socket.destroy()
|
||||
} catch {
|
||||
@@ -49,79 +64,150 @@ server.on('connection', (socket) => {
|
||||
cleanupSession(s)
|
||||
peers.remove(s.id)
|
||||
recordPeerDisconnect()
|
||||
logger.info('Peer disconnected', { peerId: s.id.slice(0, 12) })
|
||||
log.info('Peer disconnected', {
|
||||
peerId: s.id.slice(0, 12),
|
||||
role: s.role,
|
||||
peers: peers.size,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
registerAllHandlers(session)
|
||||
peers.add(session)
|
||||
recordPeerConnect()
|
||||
logger.info('Peer connected', {
|
||||
log.info('Peer connected', {
|
||||
peerId: session.id.slice(0, 12),
|
||||
role: session.role,
|
||||
peers: peers.size,
|
||||
})
|
||||
})
|
||||
|
||||
server.on('error', (err) => {
|
||||
log.error('HyperDHT server error', { error: err.message, code: err.code })
|
||||
})
|
||||
|
||||
/**
|
||||
* Probe Docker socket for the boot banner.
|
||||
* @returns {Promise<{ ok: boolean, apiVersion?: string, os?: string, error?: string }>}
|
||||
*/
|
||||
async function probeDocker() {
|
||||
try {
|
||||
const version = await docker.version()
|
||||
return {
|
||||
ok: true,
|
||||
apiVersion: version.ApiVersion || version.apiVersion || undefined,
|
||||
os: version.Os || version.os || undefined,
|
||||
}
|
||||
} catch (err) {
|
||||
return { ok: false, error: err.message || String(err) }
|
||||
}
|
||||
}
|
||||
|
||||
await server.listen(keyPair)
|
||||
|
||||
logger.info('peardock server listening on HyperDHT')
|
||||
console.log('')
|
||||
console.log('═══════════════════════════════════════════════════════════')
|
||||
console.log(' peardock server ready')
|
||||
console.log(` Public key (paste into the client):`)
|
||||
console.log(` ${publicKeyHex}`)
|
||||
if (isHolesailEnabled()) {
|
||||
const hs = holesailStatus()
|
||||
console.log(
|
||||
` Holesail tunnels: ${hs.available ? 'enabled' : 'enabled but package missing'} (max ${hs.max})`
|
||||
)
|
||||
} else {
|
||||
console.log(' Holesail tunnels: off (ENABLE_HOLESAIL=0)')
|
||||
const dockerStatus = await probeDocker()
|
||||
const hs = isHolesailEnabled() ? holesailStatus() : null
|
||||
const bootMs = Date.now() - bootStarted
|
||||
|
||||
logger.banner([
|
||||
'peardock server ready',
|
||||
'',
|
||||
'Public key (paste into the Pear client):',
|
||||
publicKeyHex,
|
||||
'',
|
||||
`Docker: ${dockerStatus.ok ? `ok · API ${dockerStatus.apiVersion || '?'} · ${dockerStatus.os || '?'}` : `unavailable · ${dockerStatus.error || 'socket error'}`}`,
|
||||
`Holesail: ${
|
||||
!isHolesailEnabled()
|
||||
? 'off (ENABLE_HOLESAIL=0)'
|
||||
: hs?.available
|
||||
? `on · max ${hs.max} tunnels`
|
||||
: 'on but package missing'
|
||||
}`,
|
||||
`Swarm RPC: ${isSwarmEnabled() ? 'on' : 'off'} · Plugins: ${isPluginsEnabled() ? 'on' : 'off'}`,
|
||||
`Log: ${logger.format} · level ${['error', 'warn', 'info', 'debug'][logger.level] || 'info'}`,
|
||||
`Boot ${bootMs}ms · pid ${process.pid} · Node ${process.version}`,
|
||||
])
|
||||
|
||||
log.info('Listening on HyperDHT', {
|
||||
publicKey: publicKeyHex.slice(0, 16) + '…',
|
||||
docker: dockerStatus.ok,
|
||||
bootMs,
|
||||
})
|
||||
|
||||
if (!dockerStatus.ok) {
|
||||
log.warn('Docker socket not reachable — RPC that needs Docker will fail until dockerd is up', {
|
||||
error: dockerStatus.error,
|
||||
})
|
||||
}
|
||||
console.log('═══════════════════════════════════════════════════════════')
|
||||
console.log('')
|
||||
|
||||
startDockerEventStream()
|
||||
startStatsBroadcast()
|
||||
|
||||
// Recreate persisted Holesail tunnels (same hs:// keys when possible)
|
||||
if (isHolesailEnabled()) {
|
||||
restoreTunnelsFromDisk().catch((err) => {
|
||||
logger.warn('Tunnel restore failed', { error: err.message })
|
||||
})
|
||||
restoreTunnelsFromDisk()
|
||||
.then((r) => {
|
||||
if (r?.restored || r?.failed) {
|
||||
log.info('Restored Holesail tunnels', r)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
log.warn('Tunnel restore failed', { error: err.message })
|
||||
})
|
||||
}
|
||||
|
||||
// Interval maintenance jobs (system prune, etc.)
|
||||
try {
|
||||
restoreSchedules()
|
||||
log.debug('Schedules restored')
|
||||
} catch (err) {
|
||||
logger.warn('Schedule restore failed', { error: err.message })
|
||||
log.warn('Schedule restore failed', { error: err.message })
|
||||
}
|
||||
|
||||
async function shutdown() {
|
||||
console.log('[INFO] Server shutting down…')
|
||||
let shuttingDown = false
|
||||
|
||||
async function shutdown(signal = 'shutdown') {
|
||||
if (shuttingDown) return
|
||||
shuttingDown = true
|
||||
log.info('Shutting down', { signal, peers: peers.size, uptimeSec: Math.round(process.uptime()) })
|
||||
|
||||
stopStatsBroadcast()
|
||||
stopDockerEventStream()
|
||||
|
||||
try {
|
||||
await closeAllTunnels()
|
||||
} catch {
|
||||
// ignore
|
||||
} catch (err) {
|
||||
log.warn('Error closing tunnels', { error: err.message })
|
||||
}
|
||||
|
||||
peers.clear()
|
||||
|
||||
try {
|
||||
await server.close()
|
||||
} catch {
|
||||
// ignore
|
||||
} catch (err) {
|
||||
log.debug('server.close', { error: err.message })
|
||||
}
|
||||
|
||||
try {
|
||||
await dht.destroy()
|
||||
} catch {
|
||||
// ignore
|
||||
} catch (err) {
|
||||
log.debug('dht.destroy', { error: err.message })
|
||||
}
|
||||
process.exit(0)
|
||||
|
||||
log.info('Bye')
|
||||
// Allow log flush
|
||||
setTimeout(() => process.exit(0), 50).unref?.()
|
||||
}
|
||||
|
||||
gracefulGoodbye(shutdown)
|
||||
process.on('SIGINT', shutdown)
|
||||
process.on('SIGTERM', shutdown)
|
||||
gracefulGoodbye(() => shutdown('goodbye'))
|
||||
process.on('SIGINT', () => shutdown('SIGINT'))
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'))
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
log.error('Uncaught exception', { error: err.message, stack: err.stack })
|
||||
shutdown('uncaughtException')
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
const msg = reason instanceof Error ? reason.message : String(reason)
|
||||
const stack = reason instanceof Error ? reason.stack : undefined
|
||||
log.error('Unhandled rejection', { error: msg, stack })
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user