claude-code-best/src/daemon/state.ts
claude-code-best c8d08d235b
Feat/integrate lint preview (#285)
* feat: 适配 zed acp 协议

* docs: 完善 acp 文档

* feat: integrate feature branches + daemon/job 命令层级化 + 跨平台后台引擎

Cherry-picked from origin/lint/preview (637c908), excluding lint-only changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: correct detectMimeFromBase64 to decode raw bytes from base64

Cherry-picked from origin/lint/preview (ee36954).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: daemon 子进程 spawn 跨平台修复 + CliLaunchSpec 集中化重构

Cherry-picked from origin/lint/preview (c5f52cd), excluding lint-only formatting changes.

- 新建 src/utils/cliLaunch.ts: 集中化 CLI 子进程启动层
- 修复 --daemon-worker=kind 等号格式解析
- 修复 daemon/bg fast path 缺少 setShellIfWindows()
- 修复 checkPathExists 用 existsSync 替代 execSync('dir')
- 7 个 spawn 站点迁移到 CliLaunchSpec

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: merge tsconfig.base.json into tsconfig.json with full compiler options

The cherry-pick from 637c908 dropped jsx/strict/etc settings when removing
tsconfig.base.json. This commit restores them in a single tsconfig.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: merge tsconfig.base.json into tsconfig.json with full compiler options

The cherry-pick from 637c908 dropped jsx/strict/etc settings when removing
tsconfig.base.json. This commit restores them in a single tsconfig.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 20:59:29 +08:00

158 lines
3.6 KiB
TypeScript

import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs'
import { join, dirname } from 'path'
import { getClaudeConfigHomeDir } from '../utils/envUtils.js'
/**
* Daemon state persisted to disk so that `status` / `stop` can work
* from a different CLI process than the one that started the daemon.
*/
export interface DaemonStateData {
pid: number
cwd: string
startedAt: string
workerKinds: string[]
lastStatus: 'running' | 'stopped' | 'error'
}
export type DaemonStatus = 'running' | 'stopped' | 'stale'
/**
* Returns the path to the daemon state file for a given daemon name.
*/
export function getDaemonStateFilePath(name = 'remote-control'): string {
return join(getClaudeConfigHomeDir(), 'daemon', `${name}.json`)
}
/**
* Write daemon state to disk. Called by the supervisor on startup.
*/
export function writeDaemonState(
state: DaemonStateData,
name = 'remote-control',
): void {
const filePath = getDaemonStateFilePath(name)
mkdirSync(dirname(filePath), { recursive: true })
writeFileSync(filePath, JSON.stringify(state, null, 2), 'utf-8')
}
/**
* Read daemon state from disk. Returns null if no state file exists.
*/
export function readDaemonState(
name = 'remote-control',
): DaemonStateData | null {
const filePath = getDaemonStateFilePath(name)
try {
const raw = readFileSync(filePath, 'utf-8')
return JSON.parse(raw) as DaemonStateData
} catch {
return null
}
}
/**
* Remove the daemon state file.
*/
export function removeDaemonState(name = 'remote-control'): void {
const filePath = getDaemonStateFilePath(name)
try {
unlinkSync(filePath)
} catch {
// File may not exist — that's fine
}
}
/**
* Check if a process with the given PID is alive.
*/
function isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0)
return true
} catch {
return false
}
}
/**
* Query the daemon status by reading the state file and probing the PID.
*
* Returns:
* - { status: 'running', state } — PID is alive
* - { status: 'stopped' } — no state file
* - { status: 'stale' } — state file exists but PID is dead (auto-cleaned)
*/
export function queryDaemonStatus(name = 'remote-control'): {
status: DaemonStatus
state?: DaemonStateData
} {
const state = readDaemonState(name)
if (!state) {
return { status: 'stopped' }
}
if (isProcessAlive(state.pid)) {
return { status: 'running', state }
}
// Stale — process is dead but state file remains
removeDaemonState(name)
return { status: 'stale' }
}
/**
* Stop a running daemon by sending SIGTERM, waiting, then SIGKILL if needed.
* Cleans up the state file afterward.
*
* @returns true if the daemon was stopped, false if it wasn't running
*/
export async function stopDaemonByPid(
name = 'remote-control',
timeoutMs = 10_000,
): Promise<boolean> {
const state = readDaemonState(name)
if (!state) {
return false
}
const { pid } = state
if (!isProcessAlive(pid)) {
removeDaemonState(name)
return false
}
// Send SIGTERM
try {
process.kill(pid, 'SIGTERM')
} catch {
removeDaemonState(name)
return false
}
// Wait for exit with timeout
const deadline = Date.now() + timeoutMs
const pollInterval = 200
while (Date.now() < deadline) {
if (!isProcessAlive(pid)) {
removeDaemonState(name)
return true
}
await new Promise(resolve => setTimeout(resolve, pollInterval))
}
// Force kill
try {
process.kill(pid, 'SIGKILL')
} catch {
// Already dead
}
// Brief wait for SIGKILL to take effect
await new Promise(resolve => setTimeout(resolve, 500))
removeDaemonState(name)
return true
}