- Hook handler now loads .env.daemon for proper config (plugin URL/secret, bot user ID) - Hook logs to /tmp/status-watcher.log instead of /dev/null - Added .env.daemon config file (.gitignored - contains tokens) - Added start-daemon.sh convenience script - Plugin mode: mobile fallback updates post message field with formatted markdown - Fixed unbounded lines array in status-watcher (capped at 50) - Added session marker to formatter output for restart recovery - Go plugin: added updatePostMessageForMobile() for dual-render strategy (webapp gets custom React component, mobile gets markdown in message field) Fixes: daemon silently dying, no plugin connection, mobile showing blank posts
157 lines
4.6 KiB
JavaScript
157 lines
4.6 KiB
JavaScript
'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;
|