feat: add admin endpoints for process management and improve error handling
- Introduced `/api/admin/restart` and `/api/admin/shutdown` endpoints in WorkerService for restarting and shutting down the service. - Updated error message in hook-error-handler to provide clearer instructions for restarting the worker. - Refactored worker-utils to remove PM2 dependency and implement ProcessManager for starting the worker service. - Cleaned up legacy PM2 references and provided new manual start instructions.
This commit is contained in:
@@ -132,6 +132,23 @@ export class WorkerService {
|
||||
res.status(200).json({ status: 'ok' });
|
||||
});
|
||||
|
||||
// Admin endpoints for process management
|
||||
this.app.post('/api/admin/restart', async (_req, res) => {
|
||||
res.json({ status: 'restarting' });
|
||||
setTimeout(async () => {
|
||||
await this.shutdown();
|
||||
process.exit(0);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
this.app.post('/api/admin/shutdown', async (_req, res) => {
|
||||
res.json({ status: 'shutting_down' });
|
||||
setTimeout(async () => {
|
||||
await this.shutdown();
|
||||
process.exit(0);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
this.viewerRoutes.setupRoutes(this.app);
|
||||
this.sessionRoutes.setupRoutes(this.app);
|
||||
this.dataRoutes.setupRoutes(this.app);
|
||||
|
||||
@@ -7,7 +7,7 @@ export function handleWorkerError(error: any): never {
|
||||
error.name === 'TimeoutError' ||
|
||||
error.message?.includes('fetch failed')) {
|
||||
throw new Error(
|
||||
"There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"
|
||||
"There's a problem with the worker. Try: npm run worker:restart"
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
|
||||
+25
-100
@@ -1,29 +1,25 @@
|
||||
import path from "path";
|
||||
import { existsSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { spawnSync } from "child_process";
|
||||
import { SettingsDefaultsManager } from "./SettingsDefaultsManager.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { HOOK_TIMEOUTS, getTimeout } from "./hook-constants.js";
|
||||
import { ProcessManager } from "../services/process/ProcessManager.js";
|
||||
|
||||
// CRITICAL: Always use marketplace directory for PM2/ecosystem
|
||||
// This ensures cross-platform compatibility and avoids cache directory confusion
|
||||
const MARKETPLACE_ROOT = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
|
||||
|
||||
// Named constants for health checks
|
||||
// Windows needs longer timeouts due to startup overhead
|
||||
const HEALTH_CHECK_TIMEOUT_MS = getTimeout(HOOK_TIMEOUTS.HEALTH_CHECK);
|
||||
const WORKER_STARTUP_WAIT_MS = HOOK_TIMEOUTS.WORKER_STARTUP_WAIT;
|
||||
const WORKER_STARTUP_RETRIES = HOOK_TIMEOUTS.WORKER_STARTUP_RETRIES;
|
||||
|
||||
// MIGRATION: Hardcoded port to avoid conflicts with PM2 worker on 37777
|
||||
// TODO: Switch to settings-based port after PM2 is fully removed
|
||||
const MIGRATION_PORT = 38888;
|
||||
|
||||
/**
|
||||
* Get the worker port number
|
||||
* Priority: ~/.claude-mem/settings.json > env var > default
|
||||
* Currently hardcoded for migration; will use settings after PM2 removal
|
||||
*/
|
||||
export function getWorkerPort(): number {
|
||||
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
|
||||
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
|
||||
return parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10);
|
||||
return MIGRATION_PORT;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,101 +42,32 @@ async function isWorkerHealthy(): Promise<boolean> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the worker service
|
||||
* On Windows: Uses PowerShell Start-Process with hidden window to avoid console flash
|
||||
* On Unix: Uses PM2 for process management
|
||||
* Start the worker service using ProcessManager
|
||||
* Handles both Unix (Bun) and Windows (compiled exe) platforms
|
||||
*/
|
||||
async function startWorker(): Promise<boolean> {
|
||||
try {
|
||||
const workerScript = path.join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'worker-service.cjs');
|
||||
|
||||
if (!existsSync(workerScript)) {
|
||||
throw new Error(`Worker script not found at ${workerScript}`);
|
||||
// Clean up legacy PM2 (one-time migration)
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
spawnSync('pm2', ['delete', 'claude-mem-worker'], { stdio: 'ignore' });
|
||||
} catch {
|
||||
// PM2 not installed or process doesn't exist - ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// On Windows, use PowerShell Start-Process with -WindowStyle Hidden
|
||||
// This avoids visible console windows that PM2 creates on Windows
|
||||
// Escape single quotes for PowerShell by doubling them
|
||||
const escapedScript = workerScript.replace(/'/g, "''");
|
||||
const escapedWorkingDir = MARKETPLACE_ROOT.replace(/'/g, "''");
|
||||
const port = getWorkerPort();
|
||||
const result = await ProcessManager.start(port);
|
||||
|
||||
const result = spawnSync('powershell.exe', [
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
`Start-Process -FilePath 'node' -ArgumentList '${escapedScript}' -WorkingDirectory '${escapedWorkingDir}' -WindowStyle Hidden`
|
||||
], {
|
||||
cwd: MARKETPLACE_ROOT,
|
||||
stdio: 'pipe',
|
||||
encoding: 'utf-8',
|
||||
windowsHide: true
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(result.stderr || 'PowerShell Start-Process failed');
|
||||
}
|
||||
} else {
|
||||
// On Unix, use PM2 for process management
|
||||
const ecosystemPath = path.join(MARKETPLACE_ROOT, 'ecosystem.config.cjs');
|
||||
|
||||
if (!existsSync(ecosystemPath)) {
|
||||
throw new Error(`Ecosystem config not found at ${ecosystemPath}`);
|
||||
}
|
||||
|
||||
const localPm2Base = path.join(MARKETPLACE_ROOT, 'node_modules', '.bin', 'pm2');
|
||||
let pm2Command: string;
|
||||
|
||||
if (existsSync(localPm2Base)) {
|
||||
pm2Command = localPm2Base;
|
||||
} else {
|
||||
// Check if global pm2 exists
|
||||
const globalPm2Check = spawnSync('which', ['pm2'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
if (globalPm2Check.status !== 0) {
|
||||
throw new Error(
|
||||
'PM2 not found. Install it locally with:\n' +
|
||||
` cd ${MARKETPLACE_ROOT}\n` +
|
||||
' npm install\n\n' +
|
||||
'Or install globally with: npm install -g pm2'
|
||||
);
|
||||
}
|
||||
|
||||
pm2Command = 'pm2';
|
||||
}
|
||||
|
||||
const result = spawnSync(pm2Command, ['start', ecosystemPath], {
|
||||
cwd: MARKETPLACE_ROOT,
|
||||
stdio: 'pipe',
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(result.stderr || 'PM2 start failed');
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for worker to become healthy
|
||||
for (let i = 0; i < WORKER_STARTUP_RETRIES; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, WORKER_STARTUP_WAIT_MS));
|
||||
if (await isWorkerHealthy()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
if (!result.success) {
|
||||
logger.error('SYSTEM', 'Failed to start worker', {
|
||||
platform: process.platform,
|
||||
workerScript: path.join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'worker-service.cjs'),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
port,
|
||||
error: result.error,
|
||||
marketplaceRoot: MARKETPLACE_ROOT
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return result.success;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,10 +93,8 @@ export async function ensureWorkerRunning(): Promise<void> {
|
||||
const port = getWorkerPort();
|
||||
throw new Error(
|
||||
`Worker service failed to start on port ${port}.\n\n` +
|
||||
`To start manually, run:\n` +
|
||||
` cd ${MARKETPLACE_ROOT}\n` +
|
||||
` npx pm2 start ecosystem.config.cjs\n\n` +
|
||||
`If already running, try: npx pm2 restart claude-mem-worker`
|
||||
`To start manually, run: npm run worker:start\n` +
|
||||
`If already running, try: npm run worker:restart`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user