auto-heartbeat-starter.cjsโข7.23 kB
#!/usr/bin/env node
/**
* Auto Heartbeat Starter - Singleton Pattern Implementation
*
* This module provides automatic heartbeat management for database tools
* when no active heartbeat is detected. It ensures only one test runs
* at a time without rate limiting for easier debugging.
*/
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const LOCK_FILE = path.join(__dirname, 'global-test.lock');
const STATUS_FILE = path.join(__dirname, 'global-test-status.json');
class AutoHeartbeatStarter {
/**
* Check if global test is already running
*/
static isGlobalTestRunning() {
try {
if (!fs.existsSync(LOCK_FILE)) {
return { isRunning: false, reason: 'No lock file' };
}
if (!fs.existsSync(STATUS_FILE)) {
return { isRunning: false, reason: 'No status file' };
}
const status = JSON.parse(fs.readFileSync(STATUS_FILE, 'utf8'));
if (!status.isRunning) {
return { isRunning: false, reason: 'Status shows not running' };
}
// Check if process is still alive (basic check)
const ageMinutes = (Date.now() - new Date(status.lastUpdate)) / (1000 * 60);
if (ageMinutes > 15) { // If status is older than 15 minutes, assume dead
return { isRunning: false, reason: 'Status too old' };
}
// More robust check - if status was updated very recently, it's active
const ageSeconds = (Date.now() - new Date(status.lastUpdate)) / 1000;
if (ageSeconds < 30) { // If updated in last 30 seconds, definitely running
return {
isRunning: true,
pid: status.pid,
testType: status.testType,
benefits: status.benefits || []
};
}
// Otherwise, check more conservatively
return {
isRunning: true,
pid: status.pid,
testType: status.testType,
benefits: status.benefits || []
};
} catch (error) {
console.error('โ Failed to check global test status:', error.message);
return { isRunning: false, reason: 'Error checking status' };
}
}
/**
* Start the global test singleton
*/
static async startGlobalTest(toolName) {
try {
console.log(`๐ Starting global heartbeat test for tool: ${toolName}`);
// Start global test using direct spawn to avoid import issues
const testProcess = spawn('node', [
'../../../test-complete-system.cjs'
], {
cwd: __dirname,
detached: true,
stdio: 'ignore'
});
// Don't wait for the process, let it run in background
testProcess.unref();
// Give it a moment to start
await new Promise(resolve => setTimeout(resolve, 2000));
console.log(`โ
Global heartbeat started for "${toolName}" (PID: ${testProcess.pid})`);
return {
success: true,
message: `Global heartbeat started for tool "${toolName}"`,
pid: testProcess.pid
};
} catch (error) {
console.error('โ Auto heartbeat starter error:', error.message);
return {
success: false,
message: `Auto heartbeat starter failed: ${error.message}`
};
}
}
/**
* Enhanced validation method that can auto-start heartbeat
*/
static async validateWithAutoStart(toolName = 'unknown-tool') {
console.log(`๐ Tool "${toolName}" needs heartbeat - attempting auto-start...`);
// Check if global test is already running
const globalCheck = this.isGlobalTestRunning();
if (globalCheck.isRunning) {
console.log(`โ
Global test already running (PID: ${globalCheck.pid})`);
return {
success: true,
message: `Heartbeat already active for "${toolName}"`,
alreadyRunning: true,
pid: globalCheck.pid
};
}
// Try to start the global test
const startResult = await this.startGlobalTest(toolName);
if (startResult.success) {
console.log(`โ
Global heartbeat started for "${toolName}" (PID: ${startResult.pid})`);
return {
success: true,
message: `Heartbeat auto-started successfully for ${toolName}`,
autoStarted: true,
pid: startResult.pid
};
}
// If auto-start failed, return helpful error message
return {
success: false,
message: startResult.message || 'Auto-start failed',
suggestion: 'Manually start the heartbeat test using: node ../../../test-complete-system.cjs'
};
}
/**
* Update heartbeat time (forward to database-utils)
*/
static updateHeartbeatTime() {
try {
const { SimpleEGWDatabase } = require('./database-utils.js');
SimpleEGWDatabase.updateHeartbeatTime();
console.log('๐ Heartbeat time updated');
} catch (error) {
console.error('โ Failed to update heartbeat time:', error.message);
}
}
/**
* Get current heartbeat status
*/
static getCurrentHeartbeatStatus() {
const globalCheck = this.isGlobalTestRunning();
return {
isActive: globalCheck.isRunning,
lastHeartbeat: globalCheck.isRunning ? new Date() : null,
globalTestRunning: globalCheck.isRunning,
pid: globalCheck.isRunning ? globalCheck.pid : null
};
}
/**
* Clean up old files
*/
static cleanup() {
try {
const filesToCheck = [LOCK_FILE, STATUS_FILE];
filesToCheck.forEach(file => {
if (fs.existsSync(file)) {
const stats = fs.statSync(file);
const ageHours = (Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60);
// Clean up files older than 1 hour
if (ageHours > 1) {
fs.unlinkSync(file);
console.log(`๐งน Cleaned up old file: ${path.basename(file)}`);
}
}
});
} catch (error) {
console.error('โ Cleanup failed:', error.message);
}
}
}
// Export for use by other modules
module.exports = { AutoHeartbeatStarter };
// CLI interface for testing
if (require.main === module) {
const args = process.argv.slice(2);
const command = args[0];
const toolName = args[1] || 'test-tool';
switch (command) {
case 'test':
AutoHeartbeatStarter.validateWithAutoStart(toolName).then(result => {
console.log('๐งช Auto-heartbeat test result:');
console.log(JSON.stringify(result, null, 2));
});
break;
case 'status':
const status = AutoHeartbeatStarter.getCurrentHeartbeatStatus();
console.log('๐ Heartbeat status:', status);
break;
case 'cleanup':
AutoHeartbeatStarter.cleanup();
console.log('๐งน Cleanup completed');
break;
default:
console.log(`
Auto Heartbeat Starter - Tool for automatic heartbeat management
Usage: node auto-heartbeat-starter.cjs <command> [toolName]
Commands:
test [toolName] Test auto-start for a tool
status Show current heartbeat status
cleanup Clean up old files
Examples:
node auto-heartbeat-starter.cjs test chat-cli
node auto-heartbeat-starter.cjs status
`);
}
}