From a696cafdd4195a4051fec789e59d92d1bb4bda8d Mon Sep 17 00:00:00 2001 From: weishu Date: Wed, 17 Dec 2025 15:59:50 +0800 Subject: [PATCH] refactor: remove macOS caffeinate sleep prevention functionality --- cli/README.md | 1 - cli/src/claude/runClaude.ts | 14 ---- cli/src/codex/runCodex.ts | 4 -- cli/src/configuration.ts | 2 - cli/src/daemon/CLAUDE.md | 3 - cli/src/daemon/run.ts | 8 --- cli/src/utils/caffeinate.ts | 140 ------------------------------------ 7 files changed, 172 deletions(-) delete mode 100644 cli/src/utils/caffeinate.ts diff --git a/cli/README.md b/cli/README.md index 34411fcd..867887bd 100644 --- a/cli/README.md +++ b/cli/README.md @@ -44,7 +44,6 @@ This will: - `HAPPY_BOT_URL` - Bot URL (default: http://localhost:3006) - `CLI_API_TOKEN` - Shared secret for bot authentication (required) - `HAPPY_HOME_DIR` - Custom home directory for hapi data (default: ~/.happy) -- `HAPPY_DISABLE_CAFFEINATE` - Disable macOS sleep prevention (set to `true`, `1`, or `yes`) - `HAPPY_EXPERIMENTAL` - Enable experimental features (set to `true`, `1`, or `yes`) ## Requirements diff --git a/cli/src/claude/runClaude.ts b/cli/src/claude/runClaude.ts index 713b38b1..85408ebf 100644 --- a/cli/src/claude/runClaude.ts +++ b/cli/src/claude/runClaude.ts @@ -10,7 +10,6 @@ import { readSettings } from '@/persistence'; import { EnhancedMode, PermissionMode } from './loop'; import { MessageQueue2 } from '@/utils/MessageQueue2'; import { hashObject } from '@/utils/deterministicJson'; -import { startCaffeinate, stopCaffeinate } from '@/utils/caffeinate'; import { extractSDKMetadataAsync } from '@/claude/sdk/metadataExtractor'; import { parseSpecialCommand } from '@/parsers/specialCommands'; import { getEnvironmentInfo } from '@/ui/doctor'; @@ -137,12 +136,6 @@ export async function runClaude(options: StartOptions = {}): Promise { controlledByUser: options.startingMode !== 'remote' })); - // Start caffeinate to prevent sleep on macOS - const caffeinateStarted = startCaffeinate(); - if (caffeinateStarted) { - logger.infoDeveloper('Sleep prevention enabled (macOS)'); - } - // Import MessageQueue2 and create message queue const messageQueue = new MessageQueue2(mode => hashObject({ isPlan: mode.permissionMode === 'plan', @@ -310,9 +303,6 @@ export async function runClaude(options: StartOptions = {}): Promise { await session.close(); } - // Stop caffeinate - stopCaffeinate(); - // Stop Happy MCP server happyServer.stop(); @@ -382,10 +372,6 @@ export async function runClaude(options: StartOptions = {}): Promise { logger.debug('Closing session...'); await session.close(); - // Stop caffeinate before exiting - stopCaffeinate(); - logger.debug('Stopped sleep prevention'); - // Stop Happy MCP server happyServer.stop(); logger.debug('Stopped Happy MCP server'); diff --git a/cli/src/codex/runCodex.ts b/cli/src/codex/runCodex.ts index 6e97fd5a..84436391 100644 --- a/cli/src/codex/runCodex.ts +++ b/cli/src/codex/runCodex.ts @@ -26,7 +26,6 @@ import type { CodexSessionConfig } from './types'; import { notifyDaemonSessionStarted } from "@/daemon/controlClient"; import { registerKillSessionHandler } from "@/claude/registerKillSessionHandler"; import { delay } from "@/utils/time"; -import { stopCaffeinate } from "@/utils/caffeinate"; type ReadyEventOptions = { pending: unknown; @@ -271,9 +270,6 @@ export async function runCodex(opts: { await session.close(); } - // Stop caffeinate - stopCaffeinate(); - // Stop Happy MCP server happyServer.stop(); diff --git a/cli/src/configuration.ts b/cli/src/configuration.ts index 4da11b07..63d04a6f 100644 --- a/cli/src/configuration.ts +++ b/cli/src/configuration.ts @@ -25,7 +25,6 @@ class Configuration { public readonly currentCliVersion: string public readonly isExperimentalEnabled: boolean - public readonly disableCaffeinate: boolean constructor() { // Bot server configuration @@ -52,7 +51,6 @@ class Configuration { this.daemonLockFile = join(this.happyHomeDir, 'daemon.state.json.lock') this.isExperimentalEnabled = ['true', '1', 'yes'].includes(process.env.HAPPY_EXPERIMENTAL?.toLowerCase() || '') - this.disableCaffeinate = ['true', '1', 'yes'].includes(process.env.HAPPY_DISABLE_CAFFEINATE?.toLowerCase() || '') this.currentCliVersion = packageJson.version diff --git a/cli/src/daemon/CLAUDE.md b/cli/src/daemon/CLAUDE.md index f72b268f..59f1e5a6 100644 --- a/cli/src/daemon/CLAUDE.md +++ b/cli/src/daemon/CLAUDE.md @@ -173,9 +173,6 @@ I do not like how - we loose track of children processes when daemon exits / restarts - we should write them to the same state file? At least the pids should be there for doctor & cleanup -- caffeinate process is not tracked in state at all & might become runaway -- caffeinate is also started by individual sesions - we should not do that for simpler cleanup - - the daemon control server binds to `127.0.0.1` on a random port; if we ever expose it beyond localhost, require an explicit auth token/header diff --git a/cli/src/daemon/run.ts b/cli/src/daemon/run.ts index 1013d7fd..d874dac1 100644 --- a/cli/src/daemon/run.ts +++ b/cli/src/daemon/run.ts @@ -9,7 +9,6 @@ import { SpawnSessionOptions, SpawnSessionResult } from '@/modules/common/regist import { logger } from '@/ui/logger'; import { authAndSetupMachineIfNeeded } from '@/ui/auth'; import { configuration } from '@/configuration'; -import { startCaffeinate, stopCaffeinate } from '@/utils/caffeinate'; import packageJson from '../../package.json'; import { getEnvironmentInfo } from '@/ui/doctor'; import { spawnHappyCLI } from '@/utils/spawnHappyCLI'; @@ -121,12 +120,6 @@ export async function startDaemon(): Promise { // 2. Should not have another daemon process running try { - // Start caffeinate - const caffeinateStarted = startCaffeinate(); - if (caffeinateStarted) { - logger.debug('[DAEMON RUN] Sleep prevention enabled'); - } - // Ensure auth and machine registration BEFORE anything else const { machineId } = await authAndSetupMachineIfNeeded(); logger.debug('[DAEMON RUN] Auth and machine setup complete'); @@ -562,7 +555,6 @@ export async function startDaemon(): Promise { apiMachine.shutdown(); await stopControlServer(); await cleanupDaemonState(); - await stopCaffeinate(); await releaseDaemonLock(daemonLockHandle); logger.debug('[DAEMON RUN] Cleanup completed, exiting process'); diff --git a/cli/src/utils/caffeinate.ts b/cli/src/utils/caffeinate.ts deleted file mode 100644 index 284158ed..00000000 --- a/cli/src/utils/caffeinate.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Caffeinate utility for preventing macOS from sleeping - * Uses the built-in macOS caffeinate command to keep the system awake - */ - -import { spawn, ChildProcess } from 'child_process' -import { logger } from '@/ui/logger' -import { configuration } from '@/configuration' - -let caffeinateProcess: ChildProcess | null = null - -/** - * Start caffeinate to prevent system sleep - * Only works on macOS, silently does nothing on other platforms - * - * @returns true if caffeinate was started, false otherwise - */ -export function startCaffeinate(): boolean { - // Check if caffeinate is disabled via configuration - if (configuration.disableCaffeinate) { - logger.debug('[caffeinate] Caffeinate disabled via HAPPY_DISABLE_CAFFEINATE environment variable') - return false - } - - // Only run on macOS - if (process.platform !== 'darwin') { - logger.debug('[caffeinate] Not on macOS, skipping caffeinate') - return false - } - - // Don't start if already running - if (caffeinateProcess && !caffeinateProcess.killed) { - logger.debug('[caffeinate] Caffeinate already running') - return true - } - - try { - // Spawn caffeinate with flags: - // -i: Prevent system from idle sleeping - // -m: Prevent disk from sleeping - caffeinateProcess = spawn('caffeinate', ['-im'], { - stdio: 'ignore', - detached: false - }) - - caffeinateProcess.on('error', (error) => { - logger.debug('[caffeinate] Error starting caffeinate:', error) - caffeinateProcess = null - }) - - caffeinateProcess.on('exit', (code, signal) => { - logger.debug(`[caffeinate] Process exited with code ${code}, signal ${signal}`) - caffeinateProcess = null - }) - - logger.debug(`[caffeinate] Started with PID ${caffeinateProcess.pid}`) - - // Set up cleanup handlers - setupCleanupHandlers() - - return true - } catch (error) { - logger.debug('[caffeinate] Failed to start caffeinate:', error) - return false - } -} - -let isStopping = false - -/** - * Stop the caffeinate process - */ -export async function stopCaffeinate(): Promise { - // Prevent re-entrant calls during cleanup - if (isStopping) { - logger.debug('[caffeinate] Already stopping, skipping') - return - } - - if (caffeinateProcess && !caffeinateProcess.killed) { - isStopping = true - logger.debug(`[caffeinate] Stopping caffeinate process PID ${caffeinateProcess.pid}`) - - try { - caffeinateProcess.kill('SIGTERM') - - // Give it a moment to terminate gracefully - await new Promise(resolve => setTimeout(resolve, 1000)) - - if (caffeinateProcess && !caffeinateProcess.killed) { - logger.debug('[caffeinate] Force killing caffeinate process') - caffeinateProcess.kill('SIGKILL') - } - caffeinateProcess = null - isStopping = false - } catch (error) { - logger.debug('[caffeinate] Error stopping caffeinate:', error) - isStopping = false - } - } -} - -/** - * Check if caffeinate is currently running - */ -export function isCaffeinateRunning(): boolean { - return caffeinateProcess !== null && !caffeinateProcess.killed -} - -/** - * Set up cleanup handlers to ensure caffeinate is stopped on exit - */ -let cleanupHandlersSet = false - -function setupCleanupHandlers(): void { - if (cleanupHandlersSet) { - return - } - - cleanupHandlersSet = true - - // Clean up on various exit conditions - const cleanup = () => { - stopCaffeinate() - } - - process.on('exit', cleanup) - process.on('SIGINT', cleanup) - process.on('SIGTERM', cleanup) - process.on('SIGUSR1', cleanup) - process.on('SIGUSR2', cleanup) - process.on('uncaughtException', (error) => { - logger.debug('[caffeinate] Uncaught exception, cleaning up:', error) - cleanup() - }) - process.on('unhandledRejection', (reason, promise) => { - logger.debug('[caffeinate] Unhandled rejection, cleaning up:', reason) - cleanup() - }) -} \ No newline at end of file