@clawlabz/clawnetwork 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +104 -64
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ declare function setInterval(fn: () => void, ms: number): unknown
|
|
|
7
7
|
declare function clearInterval(id: unknown): void
|
|
8
8
|
declare function fetch(url: string, init?: Record<string, unknown>): Promise<{ status: number; ok: boolean; text: () => Promise<string>; json: () => Promise<unknown> }>
|
|
9
9
|
|
|
10
|
-
const VERSION = '0.1.
|
|
10
|
+
const VERSION = '0.1.21'
|
|
11
11
|
const PLUGIN_ID = 'clawnetwork'
|
|
12
12
|
const GITHUB_REPO = 'clawlabz/claw-network'
|
|
13
13
|
const DEFAULT_RPC_PORT = 9710
|
|
@@ -484,11 +484,13 @@ interface NodeStatus {
|
|
|
484
484
|
let nodeStartedAt: number | null = null
|
|
485
485
|
let lastHealth: { blockHeight: number | null; peerCount: number | null; syncing: boolean } = { blockHeight: null, peerCount: null, syncing: false }
|
|
486
486
|
let cachedBinaryVersion: string | null = null
|
|
487
|
+
let activeRpcPort: number | null = null // actual port the plugin node is running on (may differ from config)
|
|
488
|
+
let activeP2pPort: number | null = null
|
|
487
489
|
|
|
488
|
-
function isNodeRunning(
|
|
489
|
-
//
|
|
490
|
+
function isNodeRunning(): { running: boolean; pid: number | null } {
|
|
491
|
+
// 1. In-memory process reference
|
|
490
492
|
if (nodeProcess && !nodeProcess.killed) return { running: true, pid: nodeProcess.pid }
|
|
491
|
-
//
|
|
493
|
+
// 2. PID file — the ONLY authority for "is MY node running"
|
|
492
494
|
const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
|
|
493
495
|
try {
|
|
494
496
|
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10)
|
|
@@ -499,27 +501,48 @@ function isNodeRunning(rpcPort: number = DEFAULT_RPC_PORT): { running: boolean;
|
|
|
499
501
|
}
|
|
500
502
|
}
|
|
501
503
|
} catch { /* no file */ }
|
|
502
|
-
//
|
|
503
|
-
try {
|
|
504
|
-
execFileSync('curl', ['-sf', '--max-time', '1', `http://localhost:${rpcPort}/health`], { timeout: 3000, encoding: 'utf8' })
|
|
505
|
-
// Port is responding — find PID by port
|
|
506
|
-
try {
|
|
507
|
-
const lsof = execFileSync('lsof', ['-ti', `:${rpcPort}`], { timeout: 3000, encoding: 'utf8' }).trim()
|
|
508
|
-
const pid = parseInt(lsof.split('\n')[0], 10)
|
|
509
|
-
if (pid > 0) {
|
|
510
|
-
// Write recovered PID to file for future management
|
|
511
|
-
try { fs.writeFileSync(pidFile, String(pid)) } catch { /* ok */ }
|
|
512
|
-
return { running: true, pid }
|
|
513
|
-
}
|
|
514
|
-
} catch { /* ok */ }
|
|
515
|
-
return { running: true, pid: null }
|
|
516
|
-
} catch { /* not responding */ }
|
|
504
|
+
// No port probing — if we don't have a PID, we don't own any running node
|
|
517
505
|
return { running: false, pid: null }
|
|
518
506
|
}
|
|
519
507
|
|
|
508
|
+
/** Check if a TCP port is in use by any process */
|
|
509
|
+
function isPortInUse(port: number): boolean {
|
|
510
|
+
try {
|
|
511
|
+
execFileSync('lsof', ['-ti', `:${port}`], { timeout: 3000, encoding: 'utf8' })
|
|
512
|
+
return true
|
|
513
|
+
} catch { return false }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Find available ports starting from the configured ones, skipping occupied ports */
|
|
517
|
+
function findAvailablePorts(rpcPort: number, p2pPort: number, api: OpenClawApi): { rpcPort: number; p2pPort: number } {
|
|
518
|
+
const MAX_TRIES = 20
|
|
519
|
+
let rpc = rpcPort
|
|
520
|
+
let p2p = p2pPort
|
|
521
|
+
|
|
522
|
+
// Find available RPC port
|
|
523
|
+
for (let i = 0; i < MAX_TRIES; i++) {
|
|
524
|
+
if (!isPortInUse(rpc)) break
|
|
525
|
+
api.logger?.info?.(`[clawnetwork] RPC port ${rpc} in use, trying ${rpc + 1}...`)
|
|
526
|
+
rpc++
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Find available P2P port
|
|
530
|
+
for (let i = 0; i < MAX_TRIES; i++) {
|
|
531
|
+
if (!isPortInUse(p2p)) break
|
|
532
|
+
api.logger?.info?.(`[clawnetwork] P2P port ${p2p} in use, trying ${p2p + 1}...`)
|
|
533
|
+
p2p++
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (rpc !== rpcPort || p2p !== p2pPort) {
|
|
537
|
+
api.logger?.info?.(`[clawnetwork] resolved ports: RPC=${rpc} (config=${rpcPort}), P2P=${p2p} (config=${p2pPort})`)
|
|
538
|
+
}
|
|
539
|
+
return { rpcPort: rpc, p2pPort: p2p }
|
|
540
|
+
}
|
|
541
|
+
|
|
520
542
|
function buildStatus(cfg: PluginConfig): NodeStatus {
|
|
521
543
|
const wallet = loadWallet()
|
|
522
|
-
const nodeState = isNodeRunning(
|
|
544
|
+
const nodeState = isNodeRunning()
|
|
545
|
+
const rpcPort = activeRpcPort ?? cfg.rpcPort
|
|
523
546
|
const uptime = nodeStartedAt ? Math.floor((Date.now() - nodeStartedAt) / 1000) : null
|
|
524
547
|
return {
|
|
525
548
|
running: nodeState.running,
|
|
@@ -528,7 +551,7 @@ function buildStatus(cfg: PluginConfig): NodeStatus {
|
|
|
528
551
|
peerCount: lastHealth.peerCount,
|
|
529
552
|
network: cfg.network,
|
|
530
553
|
syncMode: cfg.syncMode,
|
|
531
|
-
rpcUrl: `http://localhost:${
|
|
554
|
+
rpcUrl: `http://localhost:${rpcPort}`,
|
|
532
555
|
walletAddress: wallet?.address ?? '',
|
|
533
556
|
binaryVersion: cachedBinaryVersion,
|
|
534
557
|
pluginVersion: VERSION,
|
|
@@ -569,12 +592,12 @@ async function rpcCall(rpcPort: number, method: string, params: unknown[] = []):
|
|
|
569
592
|
}
|
|
570
593
|
|
|
571
594
|
function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawApi): void {
|
|
572
|
-
// Guard: check in-memory reference
|
|
595
|
+
// Guard: check in-memory reference and PID file only (not port)
|
|
573
596
|
if (nodeProcess && !nodeProcess.killed) {
|
|
574
597
|
api.logger?.warn?.('[clawnetwork] node already running (in-memory)')
|
|
575
598
|
return
|
|
576
599
|
}
|
|
577
|
-
const existingState = isNodeRunning(
|
|
600
|
+
const existingState = isNodeRunning()
|
|
578
601
|
if (existingState.running) {
|
|
579
602
|
api.logger?.info?.(`[clawnetwork] node already running (pid=${existingState.pid}), skipping start`)
|
|
580
603
|
return
|
|
@@ -583,7 +606,12 @@ function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawAp
|
|
|
583
606
|
if (!isValidNetwork(cfg.network)) { api.logger?.error?.(`[clawnetwork] invalid network: ${cfg.network}`); return }
|
|
584
607
|
if (!isValidSyncMode(cfg.syncMode)) { api.logger?.error?.(`[clawnetwork] invalid sync mode: ${cfg.syncMode}`); return }
|
|
585
608
|
|
|
586
|
-
|
|
609
|
+
// Find available ports (auto-increment if configured ports are occupied by other processes)
|
|
610
|
+
const ports = findAvailablePorts(cfg.rpcPort, cfg.p2pPort, api)
|
|
611
|
+
activeRpcPort = ports.rpcPort
|
|
612
|
+
activeP2pPort = ports.p2pPort
|
|
613
|
+
|
|
614
|
+
const args = ['start', '--network', cfg.network, '--rpc-port', String(ports.rpcPort), '--p2p-port', String(ports.p2pPort), '--sync-mode', cfg.syncMode, '--allow-genesis']
|
|
587
615
|
|
|
588
616
|
// Add bootstrap peers: built-in for the network + user-configured extra peers
|
|
589
617
|
const peers = [...(BOOTSTRAP_PEERS[cfg.network] ?? []), ...cfg.extraBootstrapPeers]
|
|
@@ -626,6 +654,10 @@ function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawAp
|
|
|
626
654
|
const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
|
|
627
655
|
fs.writeFileSync(pidFile, String(nodeProcess.pid))
|
|
628
656
|
|
|
657
|
+
// Save actual runtime ports to workspace (UI server and health checks read from here)
|
|
658
|
+
const runtimeCfg = path.join(WORKSPACE_DIR, 'runtime.json')
|
|
659
|
+
fs.writeFileSync(runtimeCfg, JSON.stringify({ rpcPort: ports.rpcPort, p2pPort: ports.p2pPort, pid: nodeProcess.pid, startedAt: Date.now() }))
|
|
660
|
+
|
|
629
661
|
nodeProcess.on('exit', (code: number | null) => {
|
|
630
662
|
api.logger?.warn?.(`[clawnetwork] node exited with code ${code}`)
|
|
631
663
|
fs.closeSync(logFd)
|
|
@@ -654,7 +686,8 @@ function startHealthCheck(cfg: PluginConfig, api: OpenClawApi): void {
|
|
|
654
686
|
if (healthTimer) clearTimeout(healthTimer)
|
|
655
687
|
|
|
656
688
|
const check = async () => {
|
|
657
|
-
|
|
689
|
+
const rpcPort = activeRpcPort ?? cfg.rpcPort
|
|
690
|
+
lastHealth = await checkHealth(rpcPort)
|
|
658
691
|
if (lastHealth.blockHeight !== null) {
|
|
659
692
|
api.logger?.info?.(`[clawnetwork] height=${lastHealth.blockHeight} peers=${lastHealth.peerCount} syncing=${lastHealth.syncing}`)
|
|
660
693
|
}
|
|
@@ -673,7 +706,7 @@ function stopNode(api: OpenClawApi): void {
|
|
|
673
706
|
healthTimer = null
|
|
674
707
|
}
|
|
675
708
|
|
|
676
|
-
// Find PID: in-memory process or PID file
|
|
709
|
+
// Find PID: in-memory process or PID file — the ONLY ways to identify our node
|
|
677
710
|
let pid: number | null = nodeProcess?.pid ?? null
|
|
678
711
|
const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
|
|
679
712
|
if (!pid) {
|
|
@@ -685,23 +718,26 @@ function stopNode(api: OpenClawApi): void {
|
|
|
685
718
|
|
|
686
719
|
if (pid) {
|
|
687
720
|
api.logger?.info?.(`[clawnetwork] stopping node pid=${pid} (SIGTERM)...`)
|
|
688
|
-
try { process.kill(pid, 'SIGTERM') } catch
|
|
721
|
+
try { process.kill(pid, 'SIGTERM') } catch (e: unknown) {
|
|
722
|
+
api.logger?.warn?.(`[clawnetwork] failed to kill pid=${pid}: ${(e as Error).message}`)
|
|
723
|
+
}
|
|
689
724
|
setTimeout(() => {
|
|
690
725
|
try { process.kill(pid as number, 'SIGKILL') } catch { /* ok */ }
|
|
691
726
|
}, 10_000)
|
|
727
|
+
} else {
|
|
728
|
+
api.logger?.warn?.('[clawnetwork] no PID found — cannot stop node (may not be running)')
|
|
692
729
|
}
|
|
693
730
|
|
|
694
731
|
// Write stop signal file (tells restart loop in other CLI processes to stop)
|
|
695
732
|
const stopFile = path.join(WORKSPACE_DIR, 'stop.signal')
|
|
696
733
|
try { fs.writeFileSync(stopFile, String(Date.now())) } catch { /* ok */ }
|
|
697
734
|
|
|
698
|
-
//
|
|
699
|
-
if (!pid) {
|
|
700
|
-
try { execFileSync('pkill', ['-f', 'claw-node start'], { timeout: 3000 }) } catch { /* ok */ }
|
|
701
|
-
}
|
|
735
|
+
// NO pkill — we only kill our own process identified by PID file
|
|
702
736
|
|
|
703
737
|
nodeProcess = null
|
|
704
738
|
nodeStartedAt = null
|
|
739
|
+
activeRpcPort = null
|
|
740
|
+
activeP2pPort = null
|
|
705
741
|
lastHealth = { blockHeight: null, peerCount: null, syncing: false }
|
|
706
742
|
try { fs.unlinkSync(pidFile) } catch { /* ok */ }
|
|
707
743
|
}
|
|
@@ -1716,24 +1752,16 @@ async function handle(req, res) {
|
|
|
1716
1752
|
}
|
|
1717
1753
|
if (a === 'start') {
|
|
1718
1754
|
try {
|
|
1719
|
-
// Check if already running —
|
|
1755
|
+
// Check if already running — PID file is the only authority
|
|
1720
1756
|
const pidFile = OC_PID_FILE;
|
|
1721
|
-
try {
|
|
1722
|
-
const h = await fetchJson('http://localhost:' + RPC_PORT + '/health');
|
|
1723
|
-
if (h && (h.status === 'ok' || h.status === 'degraded')) {
|
|
1724
|
-
try {
|
|
1725
|
-
const { execSync } = require('child_process');
|
|
1726
|
-
const pgrep = execSync("pgrep -f 'claw-node start'", { encoding: 'utf8', timeout: 3000 }).trim();
|
|
1727
|
-
const livePid = parseInt(pgrep.split('\\n')[0], 10);
|
|
1728
|
-
if (livePid > 0) fs.writeFileSync(pidFile, String(livePid));
|
|
1729
|
-
} catch {}
|
|
1730
|
-
json(200, { message: 'Node already running' }); return;
|
|
1731
|
-
}
|
|
1732
|
-
} catch {}
|
|
1733
|
-
// Fallback: check PID file
|
|
1734
1757
|
try {
|
|
1735
1758
|
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
1736
|
-
if (pid > 0) {
|
|
1759
|
+
if (pid > 0) {
|
|
1760
|
+
try { process.kill(pid, 0); json(200, { message: 'Node already running', pid }); return; } catch {
|
|
1761
|
+
// PID stale, clean up
|
|
1762
|
+
try { fs.unlinkSync(pidFile); } catch {}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1737
1765
|
} catch {}
|
|
1738
1766
|
// Find binary
|
|
1739
1767
|
const binDir = OC_BIN_DIR;
|
|
@@ -1786,14 +1814,14 @@ async function handle(req, res) {
|
|
|
1786
1814
|
// Write stop signal for restart loop
|
|
1787
1815
|
const stopFile = OC_STOP_SIGNAL;
|
|
1788
1816
|
try { fs.writeFileSync(stopFile, String(Date.now())); } catch {}
|
|
1789
|
-
//
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
require('child_process').execSync('sleep 0.5', { timeout: 2000 });
|
|
1817
|
+
// Wait for our process to exit (max 5s), using PID-specific check
|
|
1818
|
+
if (pid && pid > 0) {
|
|
1819
|
+
for (let w = 0; w < 10; w++) {
|
|
1820
|
+
try { process.kill(pid, 0); } catch { break; }
|
|
1821
|
+
require('child_process').execSync('sleep 0.5', { timeout: 2000 });
|
|
1822
|
+
}
|
|
1796
1823
|
}
|
|
1824
|
+
try { fs.unlinkSync(pidFile); } catch {}
|
|
1797
1825
|
json(200, { message: 'Node stopped' });
|
|
1798
1826
|
} catch (e) { json(500, { error: e.message }); }
|
|
1799
1827
|
return;
|
|
@@ -1808,13 +1836,17 @@ async function handle(req, res) {
|
|
|
1808
1836
|
} catch {}
|
|
1809
1837
|
const stopFile = OC_STOP_SIGNAL;
|
|
1810
1838
|
try { fs.writeFileSync(stopFile, String(Date.now())); } catch {}
|
|
1811
|
-
|
|
1839
|
+
// Wait for our process to exit (PID-specific)
|
|
1840
|
+
try {
|
|
1841
|
+
const stoppedPid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
1842
|
+
if (stoppedPid > 0) {
|
|
1843
|
+
for (let w = 0; w < 10; w++) {
|
|
1844
|
+
try { process.kill(stoppedPid, 0); } catch { break; }
|
|
1845
|
+
require('child_process').execSync('sleep 0.5', { timeout: 2000 });
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
} catch {}
|
|
1812
1849
|
try { fs.unlinkSync(pidFile); } catch {}
|
|
1813
|
-
// Wait for exit
|
|
1814
|
-
for (let w = 0; w < 10; w++) {
|
|
1815
|
-
try { require('child_process').execSync("pgrep -f 'claw-node start'", { timeout: 1000 }); } catch { break; }
|
|
1816
|
-
require('child_process').execSync('sleep 0.5', { timeout: 2000 });
|
|
1817
|
-
}
|
|
1818
1850
|
try { fs.unlinkSync(stopFile); } catch {}
|
|
1819
1851
|
// Now start (reuse start logic inline)
|
|
1820
1852
|
const binDir = OC_BIN_DIR;
|
|
@@ -1858,7 +1890,7 @@ async function handle(req, res) {
|
|
|
1858
1890
|
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
1859
1891
|
if (pid > 0) try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
1860
1892
|
} catch {}
|
|
1861
|
-
|
|
1893
|
+
// No pkill — only target our own PID
|
|
1862
1894
|
|
|
1863
1895
|
// 2. Download latest binary
|
|
1864
1896
|
const binDir = OC_BIN_DIR;
|
|
@@ -2158,7 +2190,7 @@ export default function register(api: OpenClawApi) {
|
|
|
2158
2190
|
|
|
2159
2191
|
const handleStart = async () => {
|
|
2160
2192
|
// Check if already running (in-memory or detached via PID file)
|
|
2161
|
-
const state = isNodeRunning(
|
|
2193
|
+
const state = isNodeRunning()
|
|
2162
2194
|
if (state.running) {
|
|
2163
2195
|
out({ message: 'Node already running', pid: state.pid })
|
|
2164
2196
|
return
|
|
@@ -2421,7 +2453,7 @@ export default function register(api: OpenClawApi) {
|
|
|
2421
2453
|
;(async () => {
|
|
2422
2454
|
try {
|
|
2423
2455
|
// Check if already running (e.g. from a previous detached start)
|
|
2424
|
-
const state = isNodeRunning(
|
|
2456
|
+
const state = isNodeRunning()
|
|
2425
2457
|
if (state.running) {
|
|
2426
2458
|
api.logger?.info?.(`[clawnetwork] node already running (pid=${state.pid})`)
|
|
2427
2459
|
|
|
@@ -2431,10 +2463,18 @@ export default function register(api: OpenClawApi) {
|
|
|
2431
2463
|
const binary = findBinary()
|
|
2432
2464
|
const localBinaryVersion = binary ? getBinaryVersion(binary) : null
|
|
2433
2465
|
|
|
2434
|
-
//
|
|
2466
|
+
// Read actual runtime port from last run (may differ from config if port was auto-shifted)
|
|
2467
|
+
let runtimeRpcPort = cfg.rpcPort
|
|
2468
|
+
try {
|
|
2469
|
+
const rt = JSON.parse(fs.readFileSync(path.join(WORKSPACE_DIR, 'runtime.json'), 'utf8'))
|
|
2470
|
+
if (rt.rpcPort) { runtimeRpcPort = rt.rpcPort; activeRpcPort = rt.rpcPort }
|
|
2471
|
+
if (rt.p2pPort) { activeP2pPort = rt.p2pPort }
|
|
2472
|
+
} catch { /* no runtime file, use config */ }
|
|
2473
|
+
|
|
2474
|
+
// Get the RUNNING process version from health endpoint
|
|
2435
2475
|
let runningProcessVersion: string | null = null
|
|
2436
2476
|
try {
|
|
2437
|
-
const health = await fetch(`http://localhost:${
|
|
2477
|
+
const health = await fetch(`http://localhost:${runtimeRpcPort}/health`)
|
|
2438
2478
|
if (health.ok) {
|
|
2439
2479
|
const hd = await health.json() as Record<string, unknown>
|
|
2440
2480
|
if (typeof hd.version === 'string') runningProcessVersion = hd.version.replace(/^v/, '')
|
package/openclaw.plugin.json
CHANGED