mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
refactor(daemon): use mtime-based version detection instead of string comparison
Replace CLI version string comparison with file modification time (mtime) based detection. This provides a more reliable way to detect when the CLI binary has been updated, especially for bun-compiled executables where package.json may not be accessible. Changes: - Add getInstalledCliMtimeMs() utility to check CLI binary or package.json mtime - Store startedWithCliMtimeMs in daemon state on startup - Use mtime comparison in version check loop for daemon auto-restart - Move version flag handling to early CLI startup before daemon initialization - Fix machine active state merging to prefer newer activeAt timestamp This improves daemon restart behavior when CLI is updated via package managers.
This commit is contained in:
@@ -6,10 +6,31 @@
|
||||
import { logger } from '@/ui/logger';
|
||||
import { clearDaemonState, readDaemonState } from '@/persistence';
|
||||
import { Metadata } from '@/api/types';
|
||||
import { projectPath } from '@/projectPath';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { configuration } from '@/configuration';
|
||||
import packageJson from '../../package.json';
|
||||
import { existsSync, statSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { isBunCompiled, projectPath } from '@/projectPath';
|
||||
|
||||
export function getInstalledCliMtimeMs(): number | undefined {
|
||||
if (isBunCompiled()) {
|
||||
try {
|
||||
return statSync(process.execPath).mtimeMs;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const packageJsonPath = join(projectPath(), 'package.json');
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return statSync(packageJsonPath).mtimeMs;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function daemonPost(path: string, body?: any): Promise<{ error?: string } | any> {
|
||||
const state = await readDaemonState();
|
||||
@@ -154,11 +175,13 @@ export async function isDaemonRunningCurrentlyInstalledHappyVersion(): Promise<b
|
||||
}
|
||||
|
||||
try {
|
||||
// Read package.json on demand from disk - so we are guaranteed to get the latest version
|
||||
const packageJsonPath = join(projectPath(), 'package.json');
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
||||
const currentCliMtimeMs = getInstalledCliMtimeMs();
|
||||
if (typeof currentCliMtimeMs === 'number' && typeof state.startedWithCliMtimeMs === 'number') {
|
||||
logger.debug(`[DAEMON CONTROL] Current CLI mtime: ${currentCliMtimeMs}, Daemon started with mtime: ${state.startedWithCliMtimeMs}`);
|
||||
return currentCliMtimeMs === state.startedWithCliMtimeMs;
|
||||
}
|
||||
|
||||
const currentCliVersion = packageJson.version;
|
||||
|
||||
logger.debug(`[DAEMON CONTROL] Current CLI version: ${currentCliVersion}, Daemon started with version: ${state.startedWithCliVersion}`);
|
||||
return currentCliVersion === state.startedWithCliVersion;
|
||||
|
||||
|
||||
+10
-7
@@ -13,11 +13,10 @@ import { getEnvironmentInfo } from '@/ui/doctor';
|
||||
import { spawnHappyCLI } from '@/utils/spawnHappyCLI';
|
||||
import { writeDaemonState, DaemonLocallyPersistedState, readDaemonState, acquireDaemonLock, releaseDaemonLock } from '@/persistence';
|
||||
|
||||
import { cleanupDaemonState, isDaemonRunningCurrentlyInstalledHappyVersion, stopDaemon } from './controlClient';
|
||||
import { cleanupDaemonState, getInstalledCliMtimeMs, isDaemonRunningCurrentlyInstalledHappyVersion, stopDaemon } from './controlClient';
|
||||
import { startDaemonControlServer } from './controlServer';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { projectPath, runtimePath } from '@/projectPath';
|
||||
import { runtimePath } from '@/projectPath';
|
||||
|
||||
// Prepare initial metadata
|
||||
export const initialMachineMetadata: MachineMetadata = {
|
||||
@@ -403,12 +402,15 @@ export async function startDaemon(): Promise<void> {
|
||||
onHappySessionWebhook
|
||||
});
|
||||
|
||||
const startedWithCliMtimeMs = getInstalledCliMtimeMs();
|
||||
|
||||
// Write initial daemon state (no lock needed for state file)
|
||||
const fileState: DaemonLocallyPersistedState = {
|
||||
pid: process.pid,
|
||||
httpPort: controlPort,
|
||||
startTime: new Date().toLocaleString(),
|
||||
startedWithCliVersion: packageJson.version,
|
||||
startedWithCliMtimeMs,
|
||||
daemonLogPath: logger.logFilePath
|
||||
};
|
||||
writeDaemonState(fileState);
|
||||
@@ -476,10 +478,10 @@ export async function startDaemon(): Promise<void> {
|
||||
}
|
||||
|
||||
// Check if daemon needs update
|
||||
// If version on disk is different from the one in package.json - we need to restart
|
||||
// BIG if - does this get updated from underneath us on npm upgrade?
|
||||
const projectVersion = JSON.parse(readFileSync(join(projectPath(), 'package.json'), 'utf-8')).version;
|
||||
if (projectVersion !== configuration.currentCliVersion) {
|
||||
const installedCliMtimeMs = getInstalledCliMtimeMs();
|
||||
if (typeof installedCliMtimeMs === 'number' &&
|
||||
typeof startedWithCliMtimeMs === 'number' &&
|
||||
installedCliMtimeMs !== startedWithCliMtimeMs) {
|
||||
logger.debug('[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval');
|
||||
|
||||
clearInterval(restartOnStaleVersionAndHeartbeat);
|
||||
@@ -521,6 +523,7 @@ export async function startDaemon(): Promise<void> {
|
||||
httpPort: controlPort,
|
||||
startTime: fileState.startTime,
|
||||
startedWithCliVersion: packageJson.version,
|
||||
startedWithCliMtimeMs,
|
||||
lastHeartbeat: new Date().toLocaleString(),
|
||||
daemonLogPath: fileState.daemonLogPath
|
||||
};
|
||||
|
||||
+6
-19
@@ -52,6 +52,11 @@ import { withBunRuntimeEnv } from './utils/bunRuntime'
|
||||
return process.argv.slice(1)
|
||||
})()
|
||||
|
||||
if (args.includes('-v') || args.includes('--version')) {
|
||||
console.log(`hapi version: ${packageJson.version}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Check if first argument is a subcommand
|
||||
const subcommand = args[0]
|
||||
|
||||
@@ -79,10 +84,7 @@ import { withBunRuntimeEnv } from './utils/bunRuntime'
|
||||
|
||||
await ensureRuntimeAssets()
|
||||
|
||||
// If --version is passed - do not log, its likely daemon inquiring about our version
|
||||
if (!args.includes('--version')) {
|
||||
logger.debug('Starting hapi CLI with args: ', process.argv)
|
||||
}
|
||||
logger.debug('Starting hapi CLI with args: ', process.argv)
|
||||
|
||||
if (subcommand === 'doctor') {
|
||||
// Check for clean subcommand
|
||||
@@ -318,7 +320,6 @@ ${chalk.bold('To clean up runaway processes:')} Use ${chalk.cyan('hapi doctor cl
|
||||
// Parse command line arguments for main command
|
||||
const options: StartOptions = {}
|
||||
let showHelp = false
|
||||
let showVersion = false
|
||||
const unknownArgs: string[] = [] // Collect unknown args to pass through to claude
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
@@ -328,10 +329,6 @@ ${chalk.bold('To clean up runaway processes:')} Use ${chalk.cyan('hapi doctor cl
|
||||
showHelp = true
|
||||
// Also pass through to claude
|
||||
unknownArgs.push(arg)
|
||||
} else if (arg === '-v' || arg === '--version') {
|
||||
showVersion = true
|
||||
// Also pass through to claude (will show after our version)
|
||||
unknownArgs.push(arg)
|
||||
} else if (arg === '--hapi-starting-mode') {
|
||||
options.startingMode = z.enum(['local', 'remote']).parse(args[++i])
|
||||
} else if (arg === '--yolo') {
|
||||
@@ -404,16 +401,6 @@ ${chalk.bold.cyan('Claude Code Options (from `claude --help`):')}
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Show version
|
||||
if (showVersion) {
|
||||
console.log(`hapi version: ${packageJson.version}`)
|
||||
const versionOnly = args.every((value) => value === '-v' || value === '--version')
|
||||
if (versionOnly) {
|
||||
process.exit(0)
|
||||
}
|
||||
// Continue to pass --version to Claude Code when other args are present.
|
||||
}
|
||||
|
||||
// Normal flow - auth and machine setup
|
||||
await initializeToken();
|
||||
await authAndSetupMachineIfNeeded();
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface DaemonLocallyPersistedState {
|
||||
httpPort: number;
|
||||
startTime: string;
|
||||
startedWithCliVersion: string;
|
||||
startedWithCliMtimeMs?: number;
|
||||
lastHeartbeat?: string;
|
||||
daemonLogPath?: string;
|
||||
}
|
||||
|
||||
@@ -476,13 +476,17 @@ export class SyncEngine {
|
||||
return { host, platform, happyCliVersion, displayName, ...data }
|
||||
})()
|
||||
|
||||
const storedActiveAt = stored.activeAt ?? stored.createdAt
|
||||
const existingActiveAt = existing?.activeAt ?? 0
|
||||
const useStoredActivity = storedActiveAt > existingActiveAt
|
||||
|
||||
const machine: Machine = {
|
||||
id: stored.id,
|
||||
seq: stored.seq,
|
||||
createdAt: stored.createdAt,
|
||||
updatedAt: stored.updatedAt,
|
||||
active: existing?.active ?? stored.active,
|
||||
activeAt: existing?.activeAt ?? (stored.activeAt ?? stored.createdAt),
|
||||
active: useStoredActivity ? stored.active : (existing?.active ?? stored.active),
|
||||
activeAt: useStoredActivity ? storedActiveAt : (existingActiveAt || storedActiveAt),
|
||||
metadata,
|
||||
metadataVersion: stored.metadataVersion,
|
||||
daemonState: stored.daemonState,
|
||||
|
||||
Reference in New Issue
Block a user