@clawlabz/clawnetwork 0.1.19 → 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 +108 -65
- 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
|
|
@@ -124,7 +124,10 @@ const fs = require('fs')
|
|
|
124
124
|
const { execFileSync, spawn: nodeSpawn, fork } = require('child_process')
|
|
125
125
|
|
|
126
126
|
function getBaseDir(): string {
|
|
127
|
-
//
|
|
127
|
+
// Gateway sets OPENCLAW_STATE_DIR for named profiles (e.g. ~/.openclaw-ludis)
|
|
128
|
+
// OPENCLAW_DIR is the user-facing alias (used by install.sh)
|
|
129
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR
|
|
130
|
+
if (stateDir) return stateDir
|
|
128
131
|
const envDir = process.env.OPENCLAW_DIR
|
|
129
132
|
if (envDir) return envDir
|
|
130
133
|
return path.join(os.homedir(), '.openclaw')
|
|
@@ -481,11 +484,13 @@ interface NodeStatus {
|
|
|
481
484
|
let nodeStartedAt: number | null = null
|
|
482
485
|
let lastHealth: { blockHeight: number | null; peerCount: number | null; syncing: boolean } = { blockHeight: null, peerCount: null, syncing: false }
|
|
483
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
|
|
484
489
|
|
|
485
|
-
function isNodeRunning(
|
|
486
|
-
//
|
|
490
|
+
function isNodeRunning(): { running: boolean; pid: number | null } {
|
|
491
|
+
// 1. In-memory process reference
|
|
487
492
|
if (nodeProcess && !nodeProcess.killed) return { running: true, pid: nodeProcess.pid }
|
|
488
|
-
//
|
|
493
|
+
// 2. PID file — the ONLY authority for "is MY node running"
|
|
489
494
|
const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
|
|
490
495
|
try {
|
|
491
496
|
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10)
|
|
@@ -496,27 +501,48 @@ function isNodeRunning(rpcPort: number = DEFAULT_RPC_PORT): { running: boolean;
|
|
|
496
501
|
}
|
|
497
502
|
}
|
|
498
503
|
} catch { /* no file */ }
|
|
499
|
-
//
|
|
500
|
-
try {
|
|
501
|
-
execFileSync('curl', ['-sf', '--max-time', '1', `http://localhost:${rpcPort}/health`], { timeout: 3000, encoding: 'utf8' })
|
|
502
|
-
// Port is responding — find PID by port
|
|
503
|
-
try {
|
|
504
|
-
const lsof = execFileSync('lsof', ['-ti', `:${rpcPort}`], { timeout: 3000, encoding: 'utf8' }).trim()
|
|
505
|
-
const pid = parseInt(lsof.split('\n')[0], 10)
|
|
506
|
-
if (pid > 0) {
|
|
507
|
-
// Write recovered PID to file for future management
|
|
508
|
-
try { fs.writeFileSync(pidFile, String(pid)) } catch { /* ok */ }
|
|
509
|
-
return { running: true, pid }
|
|
510
|
-
}
|
|
511
|
-
} catch { /* ok */ }
|
|
512
|
-
return { running: true, pid: null }
|
|
513
|
-
} catch { /* not responding */ }
|
|
504
|
+
// No port probing — if we don't have a PID, we don't own any running node
|
|
514
505
|
return { running: false, pid: null }
|
|
515
506
|
}
|
|
516
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
|
+
|
|
517
542
|
function buildStatus(cfg: PluginConfig): NodeStatus {
|
|
518
543
|
const wallet = loadWallet()
|
|
519
|
-
const nodeState = isNodeRunning(
|
|
544
|
+
const nodeState = isNodeRunning()
|
|
545
|
+
const rpcPort = activeRpcPort ?? cfg.rpcPort
|
|
520
546
|
const uptime = nodeStartedAt ? Math.floor((Date.now() - nodeStartedAt) / 1000) : null
|
|
521
547
|
return {
|
|
522
548
|
running: nodeState.running,
|
|
@@ -525,7 +551,7 @@ function buildStatus(cfg: PluginConfig): NodeStatus {
|
|
|
525
551
|
peerCount: lastHealth.peerCount,
|
|
526
552
|
network: cfg.network,
|
|
527
553
|
syncMode: cfg.syncMode,
|
|
528
|
-
rpcUrl: `http://localhost:${
|
|
554
|
+
rpcUrl: `http://localhost:${rpcPort}`,
|
|
529
555
|
walletAddress: wallet?.address ?? '',
|
|
530
556
|
binaryVersion: cachedBinaryVersion,
|
|
531
557
|
pluginVersion: VERSION,
|
|
@@ -566,12 +592,12 @@ async function rpcCall(rpcPort: number, method: string, params: unknown[] = []):
|
|
|
566
592
|
}
|
|
567
593
|
|
|
568
594
|
function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawApi): void {
|
|
569
|
-
// Guard: check in-memory reference
|
|
595
|
+
// Guard: check in-memory reference and PID file only (not port)
|
|
570
596
|
if (nodeProcess && !nodeProcess.killed) {
|
|
571
597
|
api.logger?.warn?.('[clawnetwork] node already running (in-memory)')
|
|
572
598
|
return
|
|
573
599
|
}
|
|
574
|
-
const existingState = isNodeRunning(
|
|
600
|
+
const existingState = isNodeRunning()
|
|
575
601
|
if (existingState.running) {
|
|
576
602
|
api.logger?.info?.(`[clawnetwork] node already running (pid=${existingState.pid}), skipping start`)
|
|
577
603
|
return
|
|
@@ -580,7 +606,12 @@ function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawAp
|
|
|
580
606
|
if (!isValidNetwork(cfg.network)) { api.logger?.error?.(`[clawnetwork] invalid network: ${cfg.network}`); return }
|
|
581
607
|
if (!isValidSyncMode(cfg.syncMode)) { api.logger?.error?.(`[clawnetwork] invalid sync mode: ${cfg.syncMode}`); return }
|
|
582
608
|
|
|
583
|
-
|
|
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']
|
|
584
615
|
|
|
585
616
|
// Add bootstrap peers: built-in for the network + user-configured extra peers
|
|
586
617
|
const peers = [...(BOOTSTRAP_PEERS[cfg.network] ?? []), ...cfg.extraBootstrapPeers]
|
|
@@ -623,6 +654,10 @@ function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawAp
|
|
|
623
654
|
const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
|
|
624
655
|
fs.writeFileSync(pidFile, String(nodeProcess.pid))
|
|
625
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
|
+
|
|
626
661
|
nodeProcess.on('exit', (code: number | null) => {
|
|
627
662
|
api.logger?.warn?.(`[clawnetwork] node exited with code ${code}`)
|
|
628
663
|
fs.closeSync(logFd)
|
|
@@ -651,7 +686,8 @@ function startHealthCheck(cfg: PluginConfig, api: OpenClawApi): void {
|
|
|
651
686
|
if (healthTimer) clearTimeout(healthTimer)
|
|
652
687
|
|
|
653
688
|
const check = async () => {
|
|
654
|
-
|
|
689
|
+
const rpcPort = activeRpcPort ?? cfg.rpcPort
|
|
690
|
+
lastHealth = await checkHealth(rpcPort)
|
|
655
691
|
if (lastHealth.blockHeight !== null) {
|
|
656
692
|
api.logger?.info?.(`[clawnetwork] height=${lastHealth.blockHeight} peers=${lastHealth.peerCount} syncing=${lastHealth.syncing}`)
|
|
657
693
|
}
|
|
@@ -670,7 +706,7 @@ function stopNode(api: OpenClawApi): void {
|
|
|
670
706
|
healthTimer = null
|
|
671
707
|
}
|
|
672
708
|
|
|
673
|
-
// Find PID: in-memory process or PID file
|
|
709
|
+
// Find PID: in-memory process or PID file — the ONLY ways to identify our node
|
|
674
710
|
let pid: number | null = nodeProcess?.pid ?? null
|
|
675
711
|
const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
|
|
676
712
|
if (!pid) {
|
|
@@ -682,23 +718,26 @@ function stopNode(api: OpenClawApi): void {
|
|
|
682
718
|
|
|
683
719
|
if (pid) {
|
|
684
720
|
api.logger?.info?.(`[clawnetwork] stopping node pid=${pid} (SIGTERM)...`)
|
|
685
|
-
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
|
+
}
|
|
686
724
|
setTimeout(() => {
|
|
687
725
|
try { process.kill(pid as number, 'SIGKILL') } catch { /* ok */ }
|
|
688
726
|
}, 10_000)
|
|
727
|
+
} else {
|
|
728
|
+
api.logger?.warn?.('[clawnetwork] no PID found — cannot stop node (may not be running)')
|
|
689
729
|
}
|
|
690
730
|
|
|
691
731
|
// Write stop signal file (tells restart loop in other CLI processes to stop)
|
|
692
732
|
const stopFile = path.join(WORKSPACE_DIR, 'stop.signal')
|
|
693
733
|
try { fs.writeFileSync(stopFile, String(Date.now())) } catch { /* ok */ }
|
|
694
734
|
|
|
695
|
-
//
|
|
696
|
-
if (!pid) {
|
|
697
|
-
try { execFileSync('pkill', ['-f', 'claw-node start'], { timeout: 3000 }) } catch { /* ok */ }
|
|
698
|
-
}
|
|
735
|
+
// NO pkill — we only kill our own process identified by PID file
|
|
699
736
|
|
|
700
737
|
nodeProcess = null
|
|
701
738
|
nodeStartedAt = null
|
|
739
|
+
activeRpcPort = null
|
|
740
|
+
activeP2pPort = null
|
|
702
741
|
lastHealth = { blockHeight: null, peerCount: null, syncing: false }
|
|
703
742
|
try { fs.unlinkSync(pidFile) } catch { /* ok */ }
|
|
704
743
|
}
|
|
@@ -1713,24 +1752,16 @@ async function handle(req, res) {
|
|
|
1713
1752
|
}
|
|
1714
1753
|
if (a === 'start') {
|
|
1715
1754
|
try {
|
|
1716
|
-
// Check if already running —
|
|
1755
|
+
// Check if already running — PID file is the only authority
|
|
1717
1756
|
const pidFile = OC_PID_FILE;
|
|
1718
|
-
try {
|
|
1719
|
-
const h = await fetchJson('http://localhost:' + RPC_PORT + '/health');
|
|
1720
|
-
if (h && (h.status === 'ok' || h.status === 'degraded')) {
|
|
1721
|
-
try {
|
|
1722
|
-
const { execSync } = require('child_process');
|
|
1723
|
-
const pgrep = execSync("pgrep -f 'claw-node start'", { encoding: 'utf8', timeout: 3000 }).trim();
|
|
1724
|
-
const livePid = parseInt(pgrep.split('\\n')[0], 10);
|
|
1725
|
-
if (livePid > 0) fs.writeFileSync(pidFile, String(livePid));
|
|
1726
|
-
} catch {}
|
|
1727
|
-
json(200, { message: 'Node already running' }); return;
|
|
1728
|
-
}
|
|
1729
|
-
} catch {}
|
|
1730
|
-
// Fallback: check PID file
|
|
1731
1757
|
try {
|
|
1732
1758
|
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
1733
|
-
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
|
+
}
|
|
1734
1765
|
} catch {}
|
|
1735
1766
|
// Find binary
|
|
1736
1767
|
const binDir = OC_BIN_DIR;
|
|
@@ -1783,14 +1814,14 @@ async function handle(req, res) {
|
|
|
1783
1814
|
// Write stop signal for restart loop
|
|
1784
1815
|
const stopFile = OC_STOP_SIGNAL;
|
|
1785
1816
|
try { fs.writeFileSync(stopFile, String(Date.now())); } catch {}
|
|
1786
|
-
//
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
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
|
+
}
|
|
1793
1823
|
}
|
|
1824
|
+
try { fs.unlinkSync(pidFile); } catch {}
|
|
1794
1825
|
json(200, { message: 'Node stopped' });
|
|
1795
1826
|
} catch (e) { json(500, { error: e.message }); }
|
|
1796
1827
|
return;
|
|
@@ -1805,13 +1836,17 @@ async function handle(req, res) {
|
|
|
1805
1836
|
} catch {}
|
|
1806
1837
|
const stopFile = OC_STOP_SIGNAL;
|
|
1807
1838
|
try { fs.writeFileSync(stopFile, String(Date.now())); } catch {}
|
|
1808
|
-
|
|
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 {}
|
|
1809
1849
|
try { fs.unlinkSync(pidFile); } catch {}
|
|
1810
|
-
// Wait for exit
|
|
1811
|
-
for (let w = 0; w < 10; w++) {
|
|
1812
|
-
try { require('child_process').execSync("pgrep -f 'claw-node start'", { timeout: 1000 }); } catch { break; }
|
|
1813
|
-
require('child_process').execSync('sleep 0.5', { timeout: 2000 });
|
|
1814
|
-
}
|
|
1815
1850
|
try { fs.unlinkSync(stopFile); } catch {}
|
|
1816
1851
|
// Now start (reuse start logic inline)
|
|
1817
1852
|
const binDir = OC_BIN_DIR;
|
|
@@ -1855,7 +1890,7 @@ async function handle(req, res) {
|
|
|
1855
1890
|
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
1856
1891
|
if (pid > 0) try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
1857
1892
|
} catch {}
|
|
1858
|
-
|
|
1893
|
+
// No pkill — only target our own PID
|
|
1859
1894
|
|
|
1860
1895
|
// 2. Download latest binary
|
|
1861
1896
|
const binDir = OC_BIN_DIR;
|
|
@@ -2155,7 +2190,7 @@ export default function register(api: OpenClawApi) {
|
|
|
2155
2190
|
|
|
2156
2191
|
const handleStart = async () => {
|
|
2157
2192
|
// Check if already running (in-memory or detached via PID file)
|
|
2158
|
-
const state = isNodeRunning(
|
|
2193
|
+
const state = isNodeRunning()
|
|
2159
2194
|
if (state.running) {
|
|
2160
2195
|
out({ message: 'Node already running', pid: state.pid })
|
|
2161
2196
|
return
|
|
@@ -2418,7 +2453,7 @@ export default function register(api: OpenClawApi) {
|
|
|
2418
2453
|
;(async () => {
|
|
2419
2454
|
try {
|
|
2420
2455
|
// Check if already running (e.g. from a previous detached start)
|
|
2421
|
-
const state = isNodeRunning(
|
|
2456
|
+
const state = isNodeRunning()
|
|
2422
2457
|
if (state.running) {
|
|
2423
2458
|
api.logger?.info?.(`[clawnetwork] node already running (pid=${state.pid})`)
|
|
2424
2459
|
|
|
@@ -2428,10 +2463,18 @@ export default function register(api: OpenClawApi) {
|
|
|
2428
2463
|
const binary = findBinary()
|
|
2429
2464
|
const localBinaryVersion = binary ? getBinaryVersion(binary) : null
|
|
2430
2465
|
|
|
2431
|
-
//
|
|
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
|
|
2432
2475
|
let runningProcessVersion: string | null = null
|
|
2433
2476
|
try {
|
|
2434
|
-
const health = await fetch(`http://localhost:${
|
|
2477
|
+
const health = await fetch(`http://localhost:${runtimeRpcPort}/health`)
|
|
2435
2478
|
if (health.ok) {
|
|
2436
2479
|
const hd = await health.json() as Record<string, unknown>
|
|
2437
2480
|
if (typeof hd.version === 'string') runningProcessVersion = hd.version.replace(/^v/, '')
|
package/openclaw.plugin.json
CHANGED