Files
sol 09441b34c1 fix: persistent daemon startup, plugin integration, mobile fallback
- 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
2026-03-08 07:42:27 +00:00

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;