From fcb42bc4d7a898768b4636520f169357a5c86ba3 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Mon, 14 Apr 2025 08:27:45 -0700 Subject: [PATCH] Use SIGHUP to reload config except PORT which requires relaunching the server. Refactor CLI to spawn server as child process and get port from OS dynamically using IPC. Fix fast exit by handling SIGINT and SIGTERM ourselves. --- docs/nano-pow.1 | 18 +++++------ src/bin/cli.ts | 36 +++++++++++++++------ src/bin/server.ts | 80 ++++++++++++++++++++++++++++++----------------- 3 files changed, 87 insertions(+), 47 deletions(-) diff --git a/docs/nano-pow.1 b/docs/nano-pow.1 index 998dfef..524e5de 100644 --- a/docs/nano-pow.1 +++ b/docs/nano-pow.1 @@ -25,15 +25,6 @@ If \fB--validate\fR is used, the original work value is returned instead along w \fB--server\fR Start work server (see SERVER below). Must be the first argument in order to be recognized. .TP -\fB\-h\fR, \fB\-\-help\fR -Show this help dialog and exit. -.TP -\fB\-\-debug\fR -Enable additional logging output. -.TP -\fB\-\-benchmark\fR \fICOUNT\fR -Generate work for the specified number of random hashes. -.TP \fB\-b\fR, \fB\-\-batch\fR Format final output of all hashes as a JSON array instead of incrementally returning an object for each result as soon as it is calculated. .TP @@ -45,6 +36,15 @@ Increase demand on GPU processing. Must be between 1 and 32 inclusive. .TP \fB\-v\fR, \fB\-\-validate\fR \fIWORK\fR Check an existing work value instead of searching for one. If you pass multiple blockhashes, the work value will be validated against each one. +.TP +\fB\-h\fR, \fB\-\-help\fR +Show this help dialog and exit. +.TP +\fB\-\-debug\fR +Enable additional logging output. +.TP +\fB\-\-benchmark\fR \fICOUNT\fR +Generate work for the specified number of random hashes. .SH SERVER Calling \fBnano-pow\fR with the \fI--server\fR option will start the NanoPow work server in a detached process. diff --git a/src/bin/cli.ts b/src/bin/cli.ts index 6cb38db..bf8720b 100755 --- a/src/bin/cli.ts +++ b/src/bin/cli.ts @@ -2,6 +2,7 @@ //! SPDX-FileCopyrightText: 2025 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later +import { spawn } from 'node:child_process' import { getRandomValues } from 'node:crypto' import { createInterface } from 'node:readline/promises' import type { WorkGenerateResponse, WorkValidateResponse } from '#types' @@ -9,8 +10,8 @@ import type { WorkGenerateResponse, WorkValidateResponse } from '#types' process.title = 'NanoPow CLI' delete process.env.NANO_POW_DEBUG -process.env.NANO_POW_EFFORT = '' -process.env.NANO_POW_PORT = '5041' +delete process.env.NANO_POW_EFFORT +delete process.env.NANO_POW_PORT function log (...args: any[]): void { if (process.env.NANO_POW_DEBUG) console.log(new Date(Date.now()).toLocaleString(), 'NanoPow', args) @@ -42,16 +43,15 @@ BLOCKHASH is a 64-character hexadecimal string. Multiple blockhashes must be sep Prints the result as a Javascript object to standard output as soon as it is calculated. If using --batch, results are printed only after all BLOCKHASH(es) have be processed. If using --validate, results will also include validity properties. - - -h, --help show this dialog - --debug enable additional logging output - --benchmark generate work for specified number of random hashes - -b, --batch process all data before returning final results as array -d, --difficulty override the minimum difficulty value -e, --effort increase demand on GPU processing -v, --validate check an existing work value instead of searching for one + -h, --help show this dialog + --debug enable additional logging output + --benchmark generate work for specified number of random hashes + If validating a nonce, it must be a 16-character hexadecimal value. Effort must be a decimal number between 1-32. Difficulty must be a hexadecimal string between 1-FFFFFFFFFFFFFFFF. @@ -135,7 +135,19 @@ for (const stdinErr of stdinErrors) { // Initialize server log('Starting NanoPow CLI') -await import('./server.js') +const server = spawn(process.execPath, [new URL(import.meta.resolve('./server.js')).pathname], { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] }) +const port = await new Promise((resolve, reject): void => { + server.on('message', (msg: { type: string, port: number }): void => { + if (msg.type === 'listening') { + if (msg.port != null) { + log(`Server listening on port ${msg.port}`) + resolve(msg.port) + } else { + reject('Server failed to provide port') + } + } + }) +}) // Execution must be sequential else GPU cannot map to CPU and will throw const results: (WorkGenerateResponse | WorkValidateResponse)[] = [] @@ -146,7 +158,7 @@ for (const hash of hashes) { try { body.hash = hash const kill = setTimeout(() => aborter.abort(), 60000) - const response = await fetch(`http://localhost:${process.env.NANO_POW_PORT}`, { + const response = await fetch(`http://localhost:${port}`, { method: 'POST', body: JSON.stringify(body), signal: aborter.signal @@ -167,4 +179,8 @@ if (isBatch && !isBenchmark) console.log(results) if (process.env.NANO_POW_DEBUG || isBenchmark) { console.log(end - start, 'ms total |', (end - start) / hashes.length, 'ms avg') } -process.exit(0) +server.on('close', code => { + log(`Server closed with exit code ${code}`) + process.exit(code) +}) +server.kill() diff --git a/src/bin/server.ts b/src/bin/server.ts index dacacdc..0a98ed7 100755 --- a/src/bin/server.ts +++ b/src/bin/server.ts @@ -2,53 +2,72 @@ //! SPDX-FileCopyrightText: 2025 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -import { launch, Browser, Page } from 'puppeteer' +import { launch } from 'puppeteer' import { subtle } from 'node:crypto' -import { lookup } from 'node:dns/promises' import { readFile, unlink, writeFile } from 'node:fs/promises' import * as http from 'node:http' -import { homedir, hostname } from 'node:os' +import { AddressInfo } from 'node:net' +import { homedir } from 'node:os' import { join } from 'node:path' import type { NanoPowOptions, WorkGenerateRequest, WorkGenerateResponse, WorkValidateRequest, WorkValidateResponse } from '#types' +/** +* Override console logging to provide an informative prefix for each entry and +* to only output when debug mode is enabled. +*/ +function log (...args: any[]): void { + if (CONFIG.DEBUG) console.log(new Date(Date.now()).toLocaleString(), 'NanoPow', args) +} + process.title = 'NanoPow Server' const MAX_REQUEST_SIZE = 1024 const MAX_BODY_SIZE = 158 -let DEBUG: boolean = !!(process.env.NANO_POW_DEBUG || false) -let EFFORT: number = +(process.env.NANO_POW_EFFORT || 8) -let PORT: number = +(process.env.NANO_POW_PORT || 5040) - -let browser: Browser -let page: Page - -function log (...args: any[]): void { - if (DEBUG) console.log(new Date(Date.now()).toLocaleString(), 'NanoPow', args) +const CONFIG = { + DEBUG: false, + EFFORT: 8, + PORT: 5040 } -async function loadConfig () { - const contents = await readFile(join(homedir(), '.nano-pow', 'config'), 'utf-8') +/** +* Loads the server configuration, preferring environment variables over the +* config file in the `.nano-pow` directory, and falling back to default values +* if no values are provided. Port will load initially, but changing it while the +* server is running will require a server restart to take effect. +*/ +async function loadConfig (): Promise { + let contents = null + try { + contents = await readFile(join(homedir(), '.nano-pow', 'config'), 'utf-8') + } catch (err) { + log('Config file not found') + } if (typeof contents === 'string') { - const config = contents.split('\n') - for (const line of config) { + for (const line of contents.split('\n')) { const debugMatch = line.match(/^[ \t]*debug[ \t]*(true|false)[ \t]*(#.*)?$/i) if (Array.isArray(debugMatch)) { - DEBUG = !!(process.env.NANO_POW_DEBUG) || debugMatch?.[1] === 'true' || false + CONFIG.DEBUG = debugMatch[1] === 'true' } - const effortMatch = line.match(/^[ \t]*effort[ \t]*(\d{1,2})[ \t]*(#.*)?$/i) if (Array.isArray(effortMatch)) { - EFFORT = +(process.env.NANO_POW_EFFORT || effortMatch?.[1] || 8) + CONFIG.EFFORT = +effortMatch[1] } const portMatch = line.match(/^[ \t]*port[ \t]*(\d{1,5})[ \t]*(#.*)?$/i) if (Array.isArray(portMatch)) { - PORT = +(process.env.NANO_POW_PORT || portMatch?.[1] || 5040) + CONFIG.PORT = +portMatch[1] } } } + CONFIG.DEBUG = !!(process.env.NANO_POW_DEBUG) || CONFIG.DEBUG + CONFIG.EFFORT = +(process.env.NANO_POW_EFFORT ?? '') || CONFIG.EFFORT + CONFIG.PORT = process.send ? 0 : +(process.env.NANO_POW_PORT ?? '') || CONFIG.PORT } await loadConfig() +process.on('SIGHUP', async (): Promise => { + log('Reloading configuration') + await loadConfig() +}) async function respond (res: http.ServerResponse, data: Buffer[]): Promise { let statusCode: number = 500 @@ -74,8 +93,8 @@ async function respond (res: http.ServerResponse, data: Buffer[]): Promise } response = `${action} failed` const options: NanoPowOptions = { - debug: DEBUG, - effort: EFFORT, + debug: CONFIG.DEBUG, + effort: CONFIG.EFFORT, difficulty } const args = [] @@ -153,7 +172,8 @@ function shutdown (): void { log('Server unresponsive, forcefully stopped') process.exit(1) }, 10000) - server.close((): Promise => { + server.close(async (): Promise => { + await browser.close() clearTimeout(kill) log('Server stopped') process.exit(0) @@ -165,7 +185,10 @@ process.on('SIGTERM', shutdown) // Initialize puppeteer log('Starting NanoPow work server') const NanoPow = await readFile(new URL('../main.min.js', import.meta.url), 'utf-8') -browser = await launch({ +const browser = await launch({ + handleSIGHUP: false, + handleSIGINT: false, + handleSIGTERM: false, headless: true, args: [ '--headless=new', @@ -175,7 +198,7 @@ browser = await launch({ '--enable-unsafe-webgpu' ] }) -page = await browser.newPage() +const page = await browser.newPage() page.on('console', msg => log(msg.text())) const path: string = new URL(import.meta.url).pathname @@ -201,7 +224,8 @@ await unlink(filename) log('Puppeteer initialized') // Listen on configured port -server.listen(PORT, async (): Promise => { - const ip = await lookup(hostname(), { family: 4 }) - log(`Server process ${process.pid} running at ${ip.address}:${PORT}/`) +server.listen(CONFIG.PORT, async (): Promise => { + const { port } = server.address() as AddressInfo + log(`Server process ${process.pid} listening on port ${port}`) + process.send?.({ type: 'listening', port }) }) -- 2.34.1