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:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -132,6 +132,23 @@ export class WorkerService {
|
|||||||
res.status(200).json({ status: 'ok' });
|
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.viewerRoutes.setupRoutes(this.app);
|
||||||
this.sessionRoutes.setupRoutes(this.app);
|
this.sessionRoutes.setupRoutes(this.app);
|
||||||
this.dataRoutes.setupRoutes(this.app);
|
this.dataRoutes.setupRoutes(this.app);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function handleWorkerError(error: any): never {
|
|||||||
error.name === 'TimeoutError' ||
|
error.name === 'TimeoutError' ||
|
||||||
error.message?.includes('fetch failed')) {
|
error.message?.includes('fetch failed')) {
|
||||||
throw new Error(
|
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;
|
throw error;
|
||||||
|
|||||||
+25
-100
@@ -1,29 +1,25 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { existsSync } from "fs";
|
|
||||||
import { homedir } from "os";
|
import { homedir } from "os";
|
||||||
import { spawnSync } from "child_process";
|
import { spawnSync } from "child_process";
|
||||||
import { SettingsDefaultsManager } from "./SettingsDefaultsManager.js";
|
|
||||||
import { logger } from "../utils/logger.js";
|
import { logger } from "../utils/logger.js";
|
||||||
import { HOOK_TIMEOUTS, getTimeout } from "./hook-constants.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');
|
const MARKETPLACE_ROOT = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
|
||||||
|
|
||||||
// Named constants for health checks
|
// Named constants for health checks
|
||||||
// Windows needs longer timeouts due to startup overhead
|
|
||||||
const HEALTH_CHECK_TIMEOUT_MS = getTimeout(HOOK_TIMEOUTS.HEALTH_CHECK);
|
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
|
* 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 {
|
export function getWorkerPort(): number {
|
||||||
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
|
return MIGRATION_PORT;
|
||||||
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
|
|
||||||
return parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,101 +42,32 @@ async function isWorkerHealthy(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the worker service
|
* Start the worker service using ProcessManager
|
||||||
* On Windows: Uses PowerShell Start-Process with hidden window to avoid console flash
|
* Handles both Unix (Bun) and Windows (compiled exe) platforms
|
||||||
* On Unix: Uses PM2 for process management
|
|
||||||
*/
|
*/
|
||||||
async function startWorker(): Promise<boolean> {
|
async function startWorker(): Promise<boolean> {
|
||||||
try {
|
// Clean up legacy PM2 (one-time migration)
|
||||||
const workerScript = path.join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'worker-service.cjs');
|
if (process.platform !== 'win32') {
|
||||||
|
try {
|
||||||
if (!existsSync(workerScript)) {
|
spawnSync('pm2', ['delete', 'claude-mem-worker'], { stdio: 'ignore' });
|
||||||
throw new Error(`Worker script not found at ${workerScript}`);
|
} catch {
|
||||||
|
// PM2 not installed or process doesn't exist - ignore
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
const port = getWorkerPort();
|
||||||
// On Windows, use PowerShell Start-Process with -WindowStyle Hidden
|
const result = await ProcessManager.start(port);
|
||||||
// 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 result = spawnSync('powershell.exe', [
|
if (!result.success) {
|
||||||
'-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) {
|
|
||||||
logger.error('SYSTEM', 'Failed to start worker', {
|
logger.error('SYSTEM', 'Failed to start worker', {
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
workerScript: path.join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'worker-service.cjs'),
|
port,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: result.error,
|
||||||
marketplaceRoot: MARKETPLACE_ROOT
|
marketplaceRoot: MARKETPLACE_ROOT
|
||||||
});
|
});
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result.success;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,10 +93,8 @@ export async function ensureWorkerRunning(): Promise<void> {
|
|||||||
const port = getWorkerPort();
|
const port = getWorkerPort();
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Worker service failed to start on port ${port}.\n\n` +
|
`Worker service failed to start on port ${port}.\n\n` +
|
||||||
`To start manually, run:\n` +
|
`To start manually, run: npm run worker:start\n` +
|
||||||
` cd ${MARKETPLACE_ROOT}\n` +
|
`If already running, try: npm run worker:restart`
|
||||||
` npx pm2 start ecosystem.config.cjs\n\n` +
|
|
||||||
`If already running, try: npx pm2 restart claude-mem-worker`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user