@clawlabz/clawnetwork 0.1.20 → 0.1.22
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 +106 -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.22'
|
|
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,50 @@ 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 via nc -z (works across users, no bind needed) */
|
|
509
|
+
function isPortInUse(port: number): boolean {
|
|
510
|
+
try {
|
|
511
|
+
execFileSync('nc', ['-z', '127.0.0.1', String(port)], { timeout: 2000, stdio: 'ignore' })
|
|
512
|
+
return true // connection succeeded → something is listening
|
|
513
|
+
} catch {
|
|
514
|
+
return false // connection refused → port is free
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Find available ports starting from the configured ones, skipping occupied ports */
|
|
519
|
+
function findAvailablePorts(rpcPort: number, p2pPort: number, api: OpenClawApi): { rpcPort: number; p2pPort: number } {
|
|
520
|
+
const MAX_TRIES = 20
|
|
521
|
+
let rpc = rpcPort
|
|
522
|
+
let p2p = p2pPort
|
|
523
|
+
|
|
524
|
+
// Find available RPC port
|
|
525
|
+
for (let i = 0; i < MAX_TRIES; i++) {
|
|
526
|
+
if (!isPortInUse(rpc)) break
|
|
527
|
+
api.logger?.info?.(`[clawnetwork] RPC port ${rpc} in use, trying ${rpc + 1}...`)
|
|
528
|
+
rpc++
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Find available P2P port
|
|
532
|
+
for (let i = 0; i < MAX_TRIES; i++) {
|
|
533
|
+
if (!isPortInUse(p2p)) break
|
|
534
|
+
api.logger?.info?.(`[clawnetwork] P2P port ${p2p} in use, trying ${p2p + 1}...`)
|
|
535
|
+
p2p++
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (rpc !== rpcPort || p2p !== p2pPort) {
|
|
539
|
+
api.logger?.info?.(`[clawnetwork] resolved ports: RPC=${rpc} (config=${rpcPort}), P2P=${p2p} (config=${p2pPort})`)
|
|
540
|
+
}
|
|
541
|
+
return { rpcPort: rpc, p2pPort: p2p }
|
|
542
|
+
}
|
|
543
|
+
|
|
520
544
|
function buildStatus(cfg: PluginConfig): NodeStatus {
|
|
521
545
|
const wallet = loadWallet()
|
|
522
|
-
const nodeState = isNodeRunning(
|
|
546
|
+
const nodeState = isNodeRunning()
|
|
547
|
+
const rpcPort = activeRpcPort ?? cfg.rpcPort
|
|
523
548
|
const uptime = nodeStartedAt ? Math.floor((Date.now() - nodeStartedAt) / 1000) : null
|
|
524
549
|
return {
|
|
525
550
|
running: nodeState.running,
|
|
@@ -528,7 +553,7 @@ function buildStatus(cfg: PluginConfig): NodeStatus {
|
|
|
528
553
|
peerCount: lastHealth.peerCount,
|
|
529
554
|
network: cfg.network,
|
|
530
555
|
syncMode: cfg.syncMode,
|
|
531
|
-
rpcUrl: `http://localhost:${
|
|
556
|
+
rpcUrl: `http://localhost:${rpcPort}`,
|
|
532
557
|
walletAddress: wallet?.address ?? '',
|
|
533
558
|
binaryVersion: cachedBinaryVersion,
|
|
534
559
|
pluginVersion: VERSION,
|
|
@@ -569,12 +594,12 @@ async function rpcCall(rpcPort: number, method: string, params: unknown[] = []):
|
|
|
569
594
|
}
|
|
570
595
|
|
|
571
596
|
function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawApi): void {
|
|
572
|
-
// Guard: check in-memory reference
|
|
597
|
+
// Guard: check in-memory reference and PID file only (not port)
|
|
573
598
|
if (nodeProcess && !nodeProcess.killed) {
|
|
574
599
|
api.logger?.warn?.('[clawnetwork] node already running (in-memory)')
|
|
575
600
|
return
|
|
576
601
|
}
|
|
577
|
-
const existingState = isNodeRunning(
|
|
602
|
+
const existingState = isNodeRunning()
|
|
578
603
|
if (existingState.running) {
|
|
579
604
|
api.logger?.info?.(`[clawnetwork] node already running (pid=${existingState.pid}), skipping start`)
|
|
580
605
|
return
|
|
@@ -583,7 +608,12 @@ function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawAp
|
|
|
583
608
|
if (!isValidNetwork(cfg.network)) { api.logger?.error?.(`[clawnetwork] invalid network: ${cfg.network}`); return }
|
|
584
609
|
if (!isValidSyncMode(cfg.syncMode)) { api.logger?.error?.(`[clawnetwork] invalid sync mode: ${cfg.syncMode}`); return }
|
|
585
610
|
|
|
586
|
-
|
|
611
|
+
// Find available ports (auto-increment if configured ports are occupied by other processes)
|
|
612
|
+
const ports = findAvailablePorts(cfg.rpcPort, cfg.p2pPort, api)
|
|
613
|
+
activeRpcPort = ports.rpcPort
|
|
614
|
+
activeP2pPort = ports.p2pPort
|
|
615
|
+
|
|
616
|
+
const args = ['start', '--network', cfg.network, '--rpc-port', String(ports.rpcPort), '--p2p-port', String(ports.p2pPort), '--sync-mode', cfg.syncMode, '--allow-genesis']
|
|
587
617
|
|
|
588
618
|
// Add bootstrap peers: built-in for the network + user-configured extra peers
|
|
589
619
|
const peers = [...(BOOTSTRAP_PEERS[cfg.network] ?? []), ...cfg.extraBootstrapPeers]
|
|
@@ -626,6 +656,10 @@ function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawAp
|
|
|
626
656
|
const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
|
|
627
657
|
fs.writeFileSync(pidFile, String(nodeProcess.pid))
|
|
628
658
|
|
|
659
|
+
// Save actual runtime ports to workspace (UI server and health checks read from here)
|
|
660
|
+
const runtimeCfg = path.join(WORKSPACE_DIR, 'runtime.json')
|
|
661
|
+
fs.writeFileSync(runtimeCfg, JSON.stringify({ rpcPort: ports.rpcPort, p2pPort: ports.p2pPort, pid: nodeProcess.pid, startedAt: Date.now() }))
|
|
662
|
+
|
|
629
663
|
nodeProcess.on('exit', (code: number | null) => {
|
|
630
664
|
api.logger?.warn?.(`[clawnetwork] node exited with code ${code}`)
|
|
631
665
|
fs.closeSync(logFd)
|
|
@@ -654,7 +688,8 @@ function startHealthCheck(cfg: PluginConfig, api: OpenClawApi): void {
|
|
|
654
688
|
if (healthTimer) clearTimeout(healthTimer)
|
|
655
689
|
|
|
656
690
|
const check = async () => {
|
|
657
|
-
|
|
691
|
+
const rpcPort = activeRpcPort ?? cfg.rpcPort
|
|
692
|
+
lastHealth = await checkHealth(rpcPort)
|
|
658
693
|
if (lastHealth.blockHeight !== null) {
|
|
659
694
|
api.logger?.info?.(`[clawnetwork] height=${lastHealth.blockHeight} peers=${lastHealth.peerCount} syncing=${lastHealth.syncing}`)
|
|
660
695
|
}
|
|
@@ -673,7 +708,7 @@ function stopNode(api: OpenClawApi): void {
|
|
|
673
708
|
healthTimer = null
|
|
674
709
|
}
|
|
675
710
|
|
|
676
|
-
// Find PID: in-memory process or PID file
|
|
711
|
+
// Find PID: in-memory process or PID file — the ONLY ways to identify our node
|
|
677
712
|
let pid: number | null = nodeProcess?.pid ?? null
|
|
678
713
|
const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
|
|
679
714
|
if (!pid) {
|
|
@@ -685,23 +720,26 @@ function stopNode(api: OpenClawApi): void {
|
|
|
685
720
|
|
|
686
721
|
if (pid) {
|
|
687
722
|
api.logger?.info?.(`[clawnetwork] stopping node pid=${pid} (SIGTERM)...`)
|
|
688
|
-
try { process.kill(pid, 'SIGTERM') } catch
|
|
723
|
+
try { process.kill(pid, 'SIGTERM') } catch (e: unknown) {
|
|
724
|
+
api.logger?.warn?.(`[clawnetwork] failed to kill pid=${pid}: ${(e as Error).message}`)
|
|
725
|
+
}
|
|
689
726
|
setTimeout(() => {
|
|
690
727
|
try { process.kill(pid as number, 'SIGKILL') } catch { /* ok */ }
|
|
691
728
|
}, 10_000)
|
|
729
|
+
} else {
|
|
730
|
+
api.logger?.warn?.('[clawnetwork] no PID found — cannot stop node (may not be running)')
|
|
692
731
|
}
|
|
693
732
|
|
|
694
733
|
// Write stop signal file (tells restart loop in other CLI processes to stop)
|
|
695
734
|
const stopFile = path.join(WORKSPACE_DIR, 'stop.signal')
|
|
696
735
|
try { fs.writeFileSync(stopFile, String(Date.now())) } catch { /* ok */ }
|
|
697
736
|
|
|
698
|
-
//
|
|
699
|
-
if (!pid) {
|
|
700
|
-
try { execFileSync('pkill', ['-f', 'claw-node start'], { timeout: 3000 }) } catch { /* ok */ }
|
|
701
|
-
}
|
|
737
|
+
// NO pkill — we only kill our own process identified by PID file
|
|
702
738
|
|
|
703
739
|
nodeProcess = null
|
|
704
740
|
nodeStartedAt = null
|
|
741
|
+
activeRpcPort = null
|
|
742
|
+
activeP2pPort = null
|
|
705
743
|
lastHealth = { blockHeight: null, peerCount: null, syncing: false }
|
|
706
744
|
try { fs.unlinkSync(pidFile) } catch { /* ok */ }
|
|
707
745
|
}
|
|
@@ -1716,24 +1754,16 @@ async function handle(req, res) {
|
|
|
1716
1754
|
}
|
|
1717
1755
|
if (a === 'start') {
|
|
1718
1756
|
try {
|
|
1719
|
-
// Check if already running —
|
|
1757
|
+
// Check if already running — PID file is the only authority
|
|
1720
1758
|
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
1759
|
try {
|
|
1735
1760
|
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
1736
|
-
if (pid > 0) {
|
|
1761
|
+
if (pid > 0) {
|
|
1762
|
+
try { process.kill(pid, 0); json(200, { message: 'Node already running', pid }); return; } catch {
|
|
1763
|
+
// PID stale, clean up
|
|
1764
|
+
try { fs.unlinkSync(pidFile); } catch {}
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1737
1767
|
} catch {}
|
|
1738
1768
|
// Find binary
|
|
1739
1769
|
const binDir = OC_BIN_DIR;
|
|
@@ -1786,14 +1816,14 @@ async function handle(req, res) {
|
|
|
1786
1816
|
// Write stop signal for restart loop
|
|
1787
1817
|
const stopFile = OC_STOP_SIGNAL;
|
|
1788
1818
|
try { fs.writeFileSync(stopFile, String(Date.now())); } catch {}
|
|
1789
|
-
//
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
require('child_process').execSync('sleep 0.5', { timeout: 2000 });
|
|
1819
|
+
// Wait for our process to exit (max 5s), using PID-specific check
|
|
1820
|
+
if (pid && pid > 0) {
|
|
1821
|
+
for (let w = 0; w < 10; w++) {
|
|
1822
|
+
try { process.kill(pid, 0); } catch { break; }
|
|
1823
|
+
require('child_process').execSync('sleep 0.5', { timeout: 2000 });
|
|
1824
|
+
}
|
|
1796
1825
|
}
|
|
1826
|
+
try { fs.unlinkSync(pidFile); } catch {}
|
|
1797
1827
|
json(200, { message: 'Node stopped' });
|
|
1798
1828
|
} catch (e) { json(500, { error: e.message }); }
|
|
1799
1829
|
return;
|
|
@@ -1808,13 +1838,17 @@ async function handle(req, res) {
|
|
|
1808
1838
|
} catch {}
|
|
1809
1839
|
const stopFile = OC_STOP_SIGNAL;
|
|
1810
1840
|
try { fs.writeFileSync(stopFile, String(Date.now())); } catch {}
|
|
1811
|
-
|
|
1841
|
+
// Wait for our process to exit (PID-specific)
|
|
1842
|
+
try {
|
|
1843
|
+
const stoppedPid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
1844
|
+
if (stoppedPid > 0) {
|
|
1845
|
+
for (let w = 0; w < 10; w++) {
|
|
1846
|
+
try { process.kill(stoppedPid, 0); } catch { break; }
|
|
1847
|
+
require('child_process').execSync('sleep 0.5', { timeout: 2000 });
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
} catch {}
|
|
1812
1851
|
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
1852
|
try { fs.unlinkSync(stopFile); } catch {}
|
|
1819
1853
|
// Now start (reuse start logic inline)
|
|
1820
1854
|
const binDir = OC_BIN_DIR;
|
|
@@ -1858,7 +1892,7 @@ async function handle(req, res) {
|
|
|
1858
1892
|
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
1859
1893
|
if (pid > 0) try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
1860
1894
|
} catch {}
|
|
1861
|
-
|
|
1895
|
+
// No pkill — only target our own PID
|
|
1862
1896
|
|
|
1863
1897
|
// 2. Download latest binary
|
|
1864
1898
|
const binDir = OC_BIN_DIR;
|
|
@@ -2158,7 +2192,7 @@ export default function register(api: OpenClawApi) {
|
|
|
2158
2192
|
|
|
2159
2193
|
const handleStart = async () => {
|
|
2160
2194
|
// Check if already running (in-memory or detached via PID file)
|
|
2161
|
-
const state = isNodeRunning(
|
|
2195
|
+
const state = isNodeRunning()
|
|
2162
2196
|
if (state.running) {
|
|
2163
2197
|
out({ message: 'Node already running', pid: state.pid })
|
|
2164
2198
|
return
|
|
@@ -2421,7 +2455,7 @@ export default function register(api: OpenClawApi) {
|
|
|
2421
2455
|
;(async () => {
|
|
2422
2456
|
try {
|
|
2423
2457
|
// Check if already running (e.g. from a previous detached start)
|
|
2424
|
-
const state = isNodeRunning(
|
|
2458
|
+
const state = isNodeRunning()
|
|
2425
2459
|
if (state.running) {
|
|
2426
2460
|
api.logger?.info?.(`[clawnetwork] node already running (pid=${state.pid})`)
|
|
2427
2461
|
|
|
@@ -2431,10 +2465,18 @@ export default function register(api: OpenClawApi) {
|
|
|
2431
2465
|
const binary = findBinary()
|
|
2432
2466
|
const localBinaryVersion = binary ? getBinaryVersion(binary) : null
|
|
2433
2467
|
|
|
2434
|
-
//
|
|
2468
|
+
// Read actual runtime port from last run (may differ from config if port was auto-shifted)
|
|
2469
|
+
let runtimeRpcPort = cfg.rpcPort
|
|
2470
|
+
try {
|
|
2471
|
+
const rt = JSON.parse(fs.readFileSync(path.join(WORKSPACE_DIR, 'runtime.json'), 'utf8'))
|
|
2472
|
+
if (rt.rpcPort) { runtimeRpcPort = rt.rpcPort; activeRpcPort = rt.rpcPort }
|
|
2473
|
+
if (rt.p2pPort) { activeP2pPort = rt.p2pPort }
|
|
2474
|
+
} catch { /* no runtime file, use config */ }
|
|
2475
|
+
|
|
2476
|
+
// Get the RUNNING process version from health endpoint
|
|
2435
2477
|
let runningProcessVersion: string | null = null
|
|
2436
2478
|
try {
|
|
2437
|
-
const health = await fetch(`http://localhost:${
|
|
2479
|
+
const health = await fetch(`http://localhost:${runtimeRpcPort}/health`)
|
|
2438
2480
|
if (health.ok) {
|
|
2439
2481
|
const hd = await health.json() as Record<string, unknown>
|
|
2440
2482
|
if (typeof hd.version === 'string') runningProcessVersion = hd.version.replace(/^v/, '')
|
package/openclaw.plugin.json
CHANGED