]> zoso.dev Git - nano-pow.git/commitdiff
Use SIGHUP to reload config except PORT which requires relaunching the server. Refact...
authorChris Duncan <chris@zoso.dev>
Mon, 14 Apr 2025 15:27:45 +0000 (08:27 -0700)
committerChris Duncan <chris@zoso.dev>
Mon, 14 Apr 2025 15:27:45 +0000 (08:27 -0700)
docs/nano-pow.1
src/bin/cli.ts
src/bin/server.ts

index 998dfef9af17a96671d2d523f991520545850571..524e5de32b051fa374ed0ad9ec261cc6a3e77c16 100644 (file)
@@ -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.
index 6cb38db1451b15487fd6f5efb2725fd5aa64b4a4..bf8720bb1466ce93c85244755bf3b6a27fdcb208 100755 (executable)
@@ -2,6 +2,7 @@
 //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
 //! 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 <value>     generate work for specified number of random hashes
-
   -b, --batch                 process all data before returning final results as array
   -d, --difficulty <value>    override the minimum difficulty value
   -e, --effort <value>        increase demand on GPU processing
   -v, --validate <value>      check an existing work value instead of searching for one
 
+  -h, --help                  show this dialog
+      --debug                 enable additional logging output
+      --benchmark <value>     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()
index dacacdc6a871b88da111532010baa444a4dd90d8..0a98ed7ee75b6c2e1ef92391b0657c142ef7d829 100755 (executable)
@@ -2,53 +2,72 @@
 //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
 //! 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<void> {
+       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<void> => {
+       log('Reloading configuration')
+       await loadConfig()
+})
 
 async function respond (res: http.ServerResponse, data: Buffer[]): Promise<void> {
        let statusCode: number = 500
@@ -74,8 +93,8 @@ async function respond (res: http.ServerResponse, data: Buffer[]): Promise<void>
                }
                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<never> => {
+       server.close(async (): Promise<never> => {
+               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<void> => {
-       const ip = await lookup(hostname(), { family: 4 })
-       log(`Server process ${process.pid} running at ${ip.address}:${PORT}/`)
+server.listen(CONFIG.PORT, async (): Promise<void> => {
+       const { port } = server.address() as AddressInfo
+       log(`Server process ${process.pid} listening on port ${port}`)
+       process.send?.({ type: 'listening', port })
 })