'use strict'; /** * status-watcher-hook/handler.js * * Spawns the Live Status v4 watcher-manager daemon on gateway startup. * * Events: ["gateway:startup"] * * Behavior: * 1. Check PID file — if watcher is already running, do nothing. * 2. If not running, spawn watcher-manager.js as a detached background process. * 3. The hook handler returns immediately; the daemon runs independently. */ /* eslint-disable no-console */ const fs = require('fs'); const path = require('path'); const { spawn } = require('child_process'); // PID file location (must match watcher-manager.js default) const PID_FILE = process.env.PID_FILE || '/tmp/status-watcher.pid'; // Log file location const LOG_FILE = process.env.LIVESTATUS_LOG_FILE || '/tmp/status-watcher.log'; // Path to watcher-manager.js (relative to this hook file's location) // Hook is in: workspace/hooks/status-watcher-hook/handler.js // Watcher is in: workspace/projects/openclaw-live-status/src/watcher-manager.js const WATCHER_PATH = path.resolve( __dirname, '../../projects/openclaw-live-status/src/watcher-manager.js', ); // Path to .env.daemon config file const ENV_DAEMON_PATH = path.resolve( __dirname, '../../projects/openclaw-live-status/.env.daemon', ); /** * Check if a process is alive given its PID. * Returns true if process exists and is running. */ function isProcessRunning(pid) { try { process.kill(pid, 0); return true; } catch (_err) { return false; } } /** * Check if the watcher daemon is already running via PID file. * Returns true if running, false if not (or PID file stale/missing). */ function isWatcherRunning() { try { // eslint-disable-next-line security/detect-non-literal-fs-filename const pidStr = fs.readFileSync(PID_FILE, 'utf8').trim(); const pid = parseInt(pidStr, 10); if (isNaN(pid) || pid <= 0) return false; return isProcessRunning(pid); } catch (_err) { // PID file missing or unreadable — watcher is not running return false; } } /** * Load .env.daemon file into an env object (key=value lines, ignoring comments). * Returns merged env: process.env + .env.daemon overrides. */ function loadDaemonEnv() { const env = Object.assign({}, process.env); try { // eslint-disable-next-line security/detect-non-literal-fs-filename const content = fs.readFileSync(ENV_DAEMON_PATH, 'utf8'); for (const line of content.split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eqIdx = trimmed.indexOf('='); if (eqIdx <= 0) continue; const key = trimmed.slice(0, eqIdx).trim(); const val = trimmed.slice(eqIdx + 1).trim(); env[key] = val; // eslint-disable-line security/detect-object-injection } console.log('[status-watcher-hook] Loaded daemon config from', ENV_DAEMON_PATH); } catch (_err) { console.warn('[status-watcher-hook] No .env.daemon found at', ENV_DAEMON_PATH, '— using process.env only'); } return env; } /** * Spawn the watcher daemon as a detached background process. * The parent (this hook handler) does not wait for it. */ function spawnWatcher() { if (!fs.existsSync(WATCHER_PATH)) { console.error('[status-watcher-hook] watcher-manager.js not found at:', WATCHER_PATH); console.error('[status-watcher-hook] Deploy the live-status project first: see install.sh'); return; } console.log('[status-watcher-hook] Starting Live Status v4 watcher daemon...'); // Load .env.daemon for proper config (plugin URL/secret, bot user ID, etc.) const daemonEnv = loadDaemonEnv(); // Open log file for append — never /dev/null let logFd; try { logFd = fs.openSync(LOG_FILE, 'a'); console.log('[status-watcher-hook] Logging to', LOG_FILE); } catch (err) { console.error('[status-watcher-hook] Cannot open log file', LOG_FILE, ':', err.message); logFd = 'ignore'; } const child = spawn(process.execPath, [WATCHER_PATH, 'start'], { detached: true, stdio: ['ignore', logFd, logFd], env: daemonEnv, }); child.unref(); // Close the fd in the parent process (child inherits its own copy) if (typeof logFd === 'number') { try { fs.closeSync(logFd); } catch (_e) { /* ignore */ } } console.log( '[status-watcher-hook] Watcher daemon spawned (PID will be written to', PID_FILE + ')', ); } /** * Hook entry point — called by OpenClaw on gateway:startup event. */ async function handle(_event) { if (isWatcherRunning()) { console.log('[status-watcher-hook] Watcher already running, skipping spawn.'); return; } spawnWatcher(); } // OpenClaw hook loader expects a default export module.exports = handle; module.exports.default = handle;