@doriandev/devcc 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +659 -0
- package/bin/devcc.mjs +66 -0
- package/dist/devcc/app/App.d.ts +178 -0
- package/dist/devcc/app/App.d.ts.map +1 -0
- package/dist/devcc/app/App.js +1255 -0
- package/dist/devcc/app/App.js.map +1 -0
- package/dist/devcc/app/builtinCommands.d.ts +17 -0
- package/dist/devcc/app/builtinCommands.d.ts.map +1 -0
- package/dist/devcc/app/builtinCommands.js +389 -0
- package/dist/devcc/app/builtinCommands.js.map +1 -0
- package/dist/devcc/app/keymap.d.ts +21 -0
- package/dist/devcc/app/keymap.d.ts.map +1 -0
- package/dist/devcc/app/keymap.js +162 -0
- package/dist/devcc/app/keymap.js.map +1 -0
- package/dist/devcc/app/theme.d.ts +49 -0
- package/dist/devcc/app/theme.d.ts.map +1 -0
- package/dist/devcc/app/theme.js +68 -0
- package/dist/devcc/app/theme.js.map +1 -0
- package/dist/devcc/cli.d.ts +3 -0
- package/dist/devcc/cli.d.ts.map +1 -0
- package/dist/devcc/cli.js +113 -0
- package/dist/devcc/cli.js.map +1 -0
- package/dist/devcc/components/CommandPalette.d.ts +26 -0
- package/dist/devcc/components/CommandPalette.d.ts.map +1 -0
- package/dist/devcc/components/CommandPalette.js +113 -0
- package/dist/devcc/components/CommandPalette.js.map +1 -0
- package/dist/devcc/components/ConfirmDialog.d.ts +31 -0
- package/dist/devcc/components/ConfirmDialog.d.ts.map +1 -0
- package/dist/devcc/components/ConfirmDialog.js +147 -0
- package/dist/devcc/components/ConfirmDialog.js.map +1 -0
- package/dist/devcc/components/Footer.d.ts +22 -0
- package/dist/devcc/components/Footer.d.ts.map +1 -0
- package/dist/devcc/components/Footer.js +72 -0
- package/dist/devcc/components/Footer.js.map +1 -0
- package/dist/devcc/components/Header.d.ts +12 -0
- package/dist/devcc/components/Header.d.ts.map +1 -0
- package/dist/devcc/components/Header.js +84 -0
- package/dist/devcc/components/Header.js.map +1 -0
- package/dist/devcc/components/HelpOverlay.d.ts +18 -0
- package/dist/devcc/components/HelpOverlay.d.ts.map +1 -0
- package/dist/devcc/components/HelpOverlay.js +48 -0
- package/dist/devcc/components/HelpOverlay.js.map +1 -0
- package/dist/devcc/components/Overlay.d.ts +31 -0
- package/dist/devcc/components/Overlay.d.ts.map +1 -0
- package/dist/devcc/components/Overlay.js +60 -0
- package/dist/devcc/components/Overlay.js.map +1 -0
- package/dist/devcc/components/ProjectPicker.d.ts +26 -0
- package/dist/devcc/components/ProjectPicker.d.ts.map +1 -0
- package/dist/devcc/components/ProjectPicker.js +114 -0
- package/dist/devcc/components/ProjectPicker.js.map +1 -0
- package/dist/devcc/components/Sidebar.d.ts +42 -0
- package/dist/devcc/components/Sidebar.d.ts.map +1 -0
- package/dist/devcc/components/Sidebar.js +127 -0
- package/dist/devcc/components/Sidebar.js.map +1 -0
- package/dist/devcc/components/StatusBadge.d.ts +16 -0
- package/dist/devcc/components/StatusBadge.d.ts.map +1 -0
- package/dist/devcc/components/StatusBadge.js +30 -0
- package/dist/devcc/components/StatusBadge.js.map +1 -0
- package/dist/devcc/components/primitives.d.ts +62 -0
- package/dist/devcc/components/primitives.d.ts.map +1 -0
- package/dist/devcc/components/primitives.js +169 -0
- package/dist/devcc/components/primitives.js.map +1 -0
- package/dist/devcc/components/styled.d.ts +16 -0
- package/dist/devcc/components/styled.d.ts.map +1 -0
- package/dist/devcc/components/styled.js +35 -0
- package/dist/devcc/components/styled.js.map +1 -0
- package/dist/devcc/core/commands.d.ts +89 -0
- package/dist/devcc/core/commands.d.ts.map +1 -0
- package/dist/devcc/core/commands.js +68 -0
- package/dist/devcc/core/commands.js.map +1 -0
- package/dist/devcc/core/config.d.ts +81 -0
- package/dist/devcc/core/config.d.ts.map +1 -0
- package/dist/devcc/core/config.js +313 -0
- package/dist/devcc/core/config.js.map +1 -0
- package/dist/devcc/core/events.d.ts +21 -0
- package/dist/devcc/core/events.d.ts.map +1 -0
- package/dist/devcc/core/events.js +27 -0
- package/dist/devcc/core/events.js.map +1 -0
- package/dist/devcc/core/state.d.ts +104 -0
- package/dist/devcc/core/state.d.ts.map +1 -0
- package/dist/devcc/core/state.js +139 -0
- package/dist/devcc/core/state.js.map +1 -0
- package/dist/devcc/deps/DependencyService.d.ts +50 -0
- package/dist/devcc/deps/DependencyService.d.ts.map +1 -0
- package/dist/devcc/deps/DependencyService.js +109 -0
- package/dist/devcc/deps/DependencyService.js.map +1 -0
- package/dist/devcc/deps/parse.d.ts +49 -0
- package/dist/devcc/deps/parse.d.ts.map +1 -0
- package/dist/devcc/deps/parse.js +158 -0
- package/dist/devcc/deps/parse.js.map +1 -0
- package/dist/devcc/git/GitService.d.ts +60 -0
- package/dist/devcc/git/GitService.d.ts.map +1 -0
- package/dist/devcc/git/GitService.js +160 -0
- package/dist/devcc/git/GitService.js.map +1 -0
- package/dist/devcc/git/parse.d.ts +57 -0
- package/dist/devcc/git/parse.d.ts.map +1 -0
- package/dist/devcc/git/parse.js +174 -0
- package/dist/devcc/git/parse.js.map +1 -0
- package/dist/devcc/index.d.ts +37 -0
- package/dist/devcc/index.d.ts.map +1 -0
- package/dist/devcc/index.js +36 -0
- package/dist/devcc/index.js.map +1 -0
- package/dist/devcc/logs/LogBuffer.d.ts +86 -0
- package/dist/devcc/logs/LogBuffer.d.ts.map +1 -0
- package/dist/devcc/logs/LogBuffer.js +200 -0
- package/dist/devcc/logs/LogBuffer.js.map +1 -0
- package/dist/devcc/logs/merge.d.ts +36 -0
- package/dist/devcc/logs/merge.d.ts.map +1 -0
- package/dist/devcc/logs/merge.js +71 -0
- package/dist/devcc/logs/merge.js.map +1 -0
- package/dist/devcc/processes/ManagedProcess.d.ts +129 -0
- package/dist/devcc/processes/ManagedProcess.d.ts.map +1 -0
- package/dist/devcc/processes/ManagedProcess.js +80 -0
- package/dist/devcc/processes/ManagedProcess.js.map +1 -0
- package/dist/devcc/processes/ProcessManager.d.ts +112 -0
- package/dist/devcc/processes/ProcessManager.d.ts.map +1 -0
- package/dist/devcc/processes/ProcessManager.js +525 -0
- package/dist/devcc/processes/ProcessManager.js.map +1 -0
- package/dist/devcc/processes/graph.d.ts +35 -0
- package/dist/devcc/processes/graph.d.ts.map +1 -0
- package/dist/devcc/processes/graph.js +68 -0
- package/dist/devcc/processes/graph.js.map +1 -0
- package/dist/devcc/projects/ProjectRegistry.d.ts +52 -0
- package/dist/devcc/projects/ProjectRegistry.d.ts.map +1 -0
- package/dist/devcc/projects/ProjectRegistry.js +177 -0
- package/dist/devcc/projects/ProjectRegistry.js.map +1 -0
- package/dist/devcc/services/ServiceManager.d.ts +48 -0
- package/dist/devcc/services/ServiceManager.d.ts.map +1 -0
- package/dist/devcc/services/ServiceManager.js +146 -0
- package/dist/devcc/services/ServiceManager.js.map +1 -0
- package/dist/devcc/services/docker.d.ts +37 -0
- package/dist/devcc/services/docker.d.ts.map +1 -0
- package/dist/devcc/services/docker.js +179 -0
- package/dist/devcc/services/docker.js.map +1 -0
- package/dist/devcc/services/node.d.ts +40 -0
- package/dist/devcc/services/node.d.ts.map +1 -0
- package/dist/devcc/services/node.js +107 -0
- package/dist/devcc/services/node.js.map +1 -0
- package/dist/devcc/services/ollama.d.ts +27 -0
- package/dist/devcc/services/ollama.d.ts.map +1 -0
- package/dist/devcc/services/ollama.js +66 -0
- package/dist/devcc/services/ollama.js.map +1 -0
- package/dist/devcc/services/postgres.d.ts +30 -0
- package/dist/devcc/services/postgres.d.ts.map +1 -0
- package/dist/devcc/services/postgres.js +47 -0
- package/dist/devcc/services/postgres.js.map +1 -0
- package/dist/devcc/services/redis.d.ts +20 -0
- package/dist/devcc/services/redis.d.ts.map +1 -0
- package/dist/devcc/services/redis.js +39 -0
- package/dist/devcc/services/redis.js.map +1 -0
- package/dist/devcc/services/types.d.ts +51 -0
- package/dist/devcc/services/types.d.ts.map +1 -0
- package/dist/devcc/services/types.js +10 -0
- package/dist/devcc/services/types.js.map +1 -0
- package/dist/devcc/system/SystemMonitor.d.ts +87 -0
- package/dist/devcc/system/SystemMonitor.d.ts.map +1 -0
- package/dist/devcc/system/SystemMonitor.js +142 -0
- package/dist/devcc/system/SystemMonitor.js.map +1 -0
- package/dist/devcc/utils/errors.d.ts +31 -0
- package/dist/devcc/utils/errors.d.ts.map +1 -0
- package/dist/devcc/utils/errors.js +52 -0
- package/dist/devcc/utils/errors.js.map +1 -0
- package/dist/devcc/utils/exec.d.ts +41 -0
- package/dist/devcc/utils/exec.d.ts.map +1 -0
- package/dist/devcc/utils/exec.js +112 -0
- package/dist/devcc/utils/exec.js.map +1 -0
- package/dist/devcc/utils/format.d.ts +18 -0
- package/dist/devcc/utils/format.d.ts.map +1 -0
- package/dist/devcc/utils/format.js +85 -0
- package/dist/devcc/utils/format.js.map +1 -0
- package/dist/devcc/utils/fuzzy.d.ts +12 -0
- package/dist/devcc/utils/fuzzy.d.ts.map +1 -0
- package/dist/devcc/utils/fuzzy.js +20 -0
- package/dist/devcc/utils/fuzzy.js.map +1 -0
- package/dist/devcc/utils/logger.d.ts +41 -0
- package/dist/devcc/utils/logger.d.ts.map +1 -0
- package/dist/devcc/utils/logger.js +117 -0
- package/dist/devcc/utils/logger.js.map +1 -0
- package/dist/devcc/utils/packageManager.d.ts +39 -0
- package/dist/devcc/utils/packageManager.d.ts.map +1 -0
- package/dist/devcc/utils/packageManager.js +85 -0
- package/dist/devcc/utils/packageManager.js.map +1 -0
- package/dist/devcc/utils/paths.d.ts +12 -0
- package/dist/devcc/utils/paths.d.ts.map +1 -0
- package/dist/devcc/utils/paths.js +39 -0
- package/dist/devcc/utils/paths.js.map +1 -0
- package/dist/devcc/views/DependenciesView.d.ts +34 -0
- package/dist/devcc/views/DependenciesView.d.ts.map +1 -0
- package/dist/devcc/views/DependenciesView.js +170 -0
- package/dist/devcc/views/DependenciesView.js.map +1 -0
- package/dist/devcc/views/GitView.d.ts +29 -0
- package/dist/devcc/views/GitView.d.ts.map +1 -0
- package/dist/devcc/views/GitView.js +226 -0
- package/dist/devcc/views/GitView.js.map +1 -0
- package/dist/devcc/views/LogsView.d.ts +39 -0
- package/dist/devcc/views/LogsView.d.ts.map +1 -0
- package/dist/devcc/views/LogsView.js +249 -0
- package/dist/devcc/views/LogsView.js.map +1 -0
- package/dist/devcc/views/OverviewView.d.ts +16 -0
- package/dist/devcc/views/OverviewView.d.ts.map +1 -0
- package/dist/devcc/views/OverviewView.js +135 -0
- package/dist/devcc/views/OverviewView.js.map +1 -0
- package/dist/devcc/views/ProcessesView.d.ts +26 -0
- package/dist/devcc/views/ProcessesView.d.ts.map +1 -0
- package/dist/devcc/views/ProcessesView.js +161 -0
- package/dist/devcc/views/ProcessesView.js.map +1 -0
- package/dist/devcc/views/ScriptsView.d.ts +30 -0
- package/dist/devcc/views/ScriptsView.d.ts.map +1 -0
- package/dist/devcc/views/ScriptsView.js +168 -0
- package/dist/devcc/views/ScriptsView.js.map +1 -0
- package/dist/devcc/views/ServicesView.d.ts +28 -0
- package/dist/devcc/views/ServicesView.d.ts.map +1 -0
- package/dist/devcc/views/ServicesView.js +153 -0
- package/dist/devcc/views/ServicesView.js.map +1 -0
- package/dist/devcc/views/SettingsView.d.ts +20 -0
- package/dist/devcc/views/SettingsView.d.ts.map +1 -0
- package/dist/devcc/views/SettingsView.js +114 -0
- package/dist/devcc/views/SettingsView.js.map +1 -0
- package/dist/devcc/views/SystemView.d.ts +13 -0
- package/dist/devcc/views/SystemView.d.ts.map +1 -0
- package/dist/devcc/views/SystemView.js +82 -0
- package/dist/devcc/views/SystemView.js.map +1 -0
- package/dist/devcc/views/View.d.ts +35 -0
- package/dist/devcc/views/View.d.ts.map +1 -0
- package/dist/devcc/views/View.js +42 -0
- package/dist/devcc/views/View.js.map +1 -0
- package/package.json +79 -0
- package/src/devcc/app/App.ts +1463 -0
- package/src/devcc/app/builtinCommands.ts +429 -0
- package/src/devcc/app/keymap.ts +167 -0
- package/src/devcc/app/theme.ts +83 -0
- package/src/devcc/cli.ts +122 -0
- package/src/devcc/components/CommandPalette.ts +136 -0
- package/src/devcc/components/ConfirmDialog.ts +174 -0
- package/src/devcc/components/Footer.ts +85 -0
- package/src/devcc/components/Header.ts +93 -0
- package/src/devcc/components/HelpOverlay.ts +62 -0
- package/src/devcc/components/Overlay.ts +91 -0
- package/src/devcc/components/ProjectPicker.ts +138 -0
- package/src/devcc/components/Sidebar.ts +163 -0
- package/src/devcc/components/StatusBadge.ts +36 -0
- package/src/devcc/components/primitives.ts +213 -0
- package/src/devcc/components/styled.ts +44 -0
- package/src/devcc/core/commands.ts +140 -0
- package/src/devcc/core/config.ts +405 -0
- package/src/devcc/core/events.ts +41 -0
- package/src/devcc/core/state.ts +236 -0
- package/src/devcc/deps/DependencyService.ts +152 -0
- package/src/devcc/deps/parse.ts +210 -0
- package/src/devcc/git/GitService.ts +205 -0
- package/src/devcc/git/parse.ts +203 -0
- package/src/devcc/index.ts +115 -0
- package/src/devcc/logs/LogBuffer.ts +237 -0
- package/src/devcc/logs/merge.ts +87 -0
- package/src/devcc/processes/ManagedProcess.ts +195 -0
- package/src/devcc/processes/ProcessManager.ts +625 -0
- package/src/devcc/processes/graph.ts +98 -0
- package/src/devcc/projects/ProjectRegistry.ts +219 -0
- package/src/devcc/services/ServiceManager.ts +180 -0
- package/src/devcc/services/docker.ts +220 -0
- package/src/devcc/services/node.ts +137 -0
- package/src/devcc/services/ollama.ts +80 -0
- package/src/devcc/services/postgres.ts +57 -0
- package/src/devcc/services/redis.ts +47 -0
- package/src/devcc/services/types.ts +59 -0
- package/src/devcc/system/SystemMonitor.ts +207 -0
- package/src/devcc/utils/errors.ts +64 -0
- package/src/devcc/utils/exec.ts +165 -0
- package/src/devcc/utils/format.ts +83 -0
- package/src/devcc/utils/fuzzy.ts +44 -0
- package/src/devcc/utils/logger.ts +140 -0
- package/src/devcc/utils/packageManager.ts +112 -0
- package/src/devcc/utils/paths.ts +39 -0
- package/src/devcc/views/DependenciesView.ts +206 -0
- package/src/devcc/views/GitView.ts +259 -0
- package/src/devcc/views/LogsView.ts +300 -0
- package/src/devcc/views/OverviewView.ts +187 -0
- package/src/devcc/views/ProcessesView.ts +177 -0
- package/src/devcc/views/ScriptsView.ts +187 -0
- package/src/devcc/views/ServicesView.ts +188 -0
- package/src/devcc/views/SettingsView.ts +146 -0
- package/src/devcc/views/SystemView.ts +110 -0
- package/src/devcc/views/View.ts +63 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process"
|
|
2
|
+
import { isAbsolute, resolve } from "node:path"
|
|
3
|
+
import { TypedEmitter } from "../core/events.js"
|
|
4
|
+
import type { LogEntry, LogStore } from "../logs/LogBuffer.js"
|
|
5
|
+
import { DevccError, errorMessage } from "../utils/errors.js"
|
|
6
|
+
import { probeTcpPort } from "../utils/exec.js"
|
|
7
|
+
import type { ScopedLogger } from "../utils/logger.js"
|
|
8
|
+
import { planStartOrder, type StartPlan } from "./graph.js"
|
|
9
|
+
import {
|
|
10
|
+
canTransition,
|
|
11
|
+
crashBackoffDelay,
|
|
12
|
+
createManagedProcess,
|
|
13
|
+
DEFAULT_CRASH_POLICY,
|
|
14
|
+
DEFAULT_READINESS,
|
|
15
|
+
describeCommand,
|
|
16
|
+
isActive,
|
|
17
|
+
shouldAutoRestart,
|
|
18
|
+
type ManagedProcess,
|
|
19
|
+
type ProcessDefinition,
|
|
20
|
+
type ProcessStatus,
|
|
21
|
+
type ReadinessCheck,
|
|
22
|
+
} from "./ManagedProcess.js"
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_KILL_GRACE_MS = 5_000
|
|
25
|
+
|
|
26
|
+
export type ProcessManagerEvents = {
|
|
27
|
+
/** Any change to a process snapshot (status, pid, exit code, ...). */
|
|
28
|
+
"process:changed": [ManagedProcess]
|
|
29
|
+
/** The set of known processes changed (added or removed). */
|
|
30
|
+
"process:list": [ManagedProcess[]]
|
|
31
|
+
"process:started": [ManagedProcess]
|
|
32
|
+
"process:exit": [ManagedProcess]
|
|
33
|
+
/** An automatic restart was scheduled after a crash. */
|
|
34
|
+
"process:retry": [ManagedProcess, number]
|
|
35
|
+
/** The readiness check passed (or timed out and was reported anyway). */
|
|
36
|
+
"process:ready": [ManagedProcess]
|
|
37
|
+
"process:error": [DevccError]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Runtime {
|
|
41
|
+
child: ChildProcess
|
|
42
|
+
/** Process group id we may signal, when `detached` spawning succeeded. */
|
|
43
|
+
pgid: number | null
|
|
44
|
+
killTimer: NodeJS.Timeout | null
|
|
45
|
+
exitPromise: Promise<void>
|
|
46
|
+
resolveExit: () => void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ProcessManagerOptions {
|
|
50
|
+
logs: LogStore
|
|
51
|
+
logger: ScopedLogger
|
|
52
|
+
/** Default cwd for definitions that don't specify one. */
|
|
53
|
+
rootDir: string
|
|
54
|
+
env?: NodeJS.ProcessEnv
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Owns every child process devcc launches.
|
|
59
|
+
*
|
|
60
|
+
* Design notes:
|
|
61
|
+
* - Commands are always `spawn(executable, args[])`; nothing is ever passed to a
|
|
62
|
+
* shell, so arguments containing spaces or quotes are safe by construction.
|
|
63
|
+
* - Children are spawned `detached`, which makes each one a process-group leader.
|
|
64
|
+
* Stopping signals the whole group (`kill(-pgid)`) so dev servers that fork
|
|
65
|
+
* workers do not leave orphans behind.
|
|
66
|
+
* - Stopping escalates SIGTERM -> SIGKILL after a grace period.
|
|
67
|
+
*/
|
|
68
|
+
export class ProcessManager extends TypedEmitter<ProcessManagerEvents> {
|
|
69
|
+
private readonly definitions = new Map<string, ProcessDefinition>()
|
|
70
|
+
private readonly snapshots = new Map<string, ManagedProcess>()
|
|
71
|
+
private readonly runtimes = new Map<string, Runtime>()
|
|
72
|
+
/** Pending automatic-restart timers, keyed by process id. */
|
|
73
|
+
private readonly retryTimers = new Map<string, NodeJS.Timeout>()
|
|
74
|
+
/** In-flight readiness checks, so a stop can cancel the probe loop. */
|
|
75
|
+
private readonly readiness = new Map<string, { cancel: () => void; done: Promise<void> }>()
|
|
76
|
+
private readonly logs: LogStore
|
|
77
|
+
private readonly logger: ScopedLogger
|
|
78
|
+
private readonly rootDir: string
|
|
79
|
+
private readonly baseEnv: NodeJS.ProcessEnv
|
|
80
|
+
private disposed = false
|
|
81
|
+
|
|
82
|
+
constructor(options: ProcessManagerOptions) {
|
|
83
|
+
super()
|
|
84
|
+
this.logs = options.logs
|
|
85
|
+
this.logger = options.logger
|
|
86
|
+
this.rootDir = options.rootDir
|
|
87
|
+
this.baseEnv = options.env ?? process.env
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Register (or replace, while stopped) a process definition. */
|
|
91
|
+
register(definition: ProcessDefinition): ManagedProcess {
|
|
92
|
+
const existing = this.snapshots.get(definition.id)
|
|
93
|
+
if (existing && isActive(existing.status)) {
|
|
94
|
+
this.definitions.set(definition.id, definition)
|
|
95
|
+
return existing
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.definitions.set(definition.id, definition)
|
|
99
|
+
const snapshot = createManagedProcess(definition)
|
|
100
|
+
this.snapshots.set(definition.id, snapshot)
|
|
101
|
+
this.logs.register(definition.id, definition.name)
|
|
102
|
+
this.emit("process:list", this.list())
|
|
103
|
+
return snapshot
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
registerAll(definitions: readonly ProcessDefinition[]): void {
|
|
107
|
+
for (const definition of definitions) this.register(definition)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Remove a stopped process. Refuses while it is still running. */
|
|
111
|
+
unregister(id: string): void {
|
|
112
|
+
const snapshot = this.snapshots.get(id)
|
|
113
|
+
if (snapshot && isActive(snapshot.status)) {
|
|
114
|
+
throw new DevccError({ scope: "process", message: `Cannot remove "${id}" while it is running` })
|
|
115
|
+
}
|
|
116
|
+
this.definitions.delete(id)
|
|
117
|
+
this.snapshots.delete(id)
|
|
118
|
+
this.logs.unregister(id)
|
|
119
|
+
this.emit("process:list", this.list())
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
list(): ManagedProcess[] {
|
|
123
|
+
return [...this.snapshots.values()]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
get(id: string): ManagedProcess | undefined {
|
|
127
|
+
return this.snapshots.get(id)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getDefinition(id: string): ProcessDefinition | undefined {
|
|
131
|
+
return this.definitions.get(id)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
isRunning(id: string): boolean {
|
|
135
|
+
const snapshot = this.snapshots.get(id)
|
|
136
|
+
return snapshot ? isActive(snapshot.status) : false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Start a process.
|
|
141
|
+
*
|
|
142
|
+
* A start the user asked for clears the crash budget; the automatic restart
|
|
143
|
+
* path passes `auto` so a crash loop cannot refill its own budget.
|
|
144
|
+
*/
|
|
145
|
+
async start(id: string, options: { auto?: boolean } = {}): Promise<ManagedProcess> {
|
|
146
|
+
const definition = this.definitions.get(id)
|
|
147
|
+
const snapshot = this.snapshots.get(id)
|
|
148
|
+
if (!definition || !snapshot) {
|
|
149
|
+
throw new DevccError({ scope: "process", message: `Unknown process "${id}"` })
|
|
150
|
+
}
|
|
151
|
+
this.cancelRetry(id)
|
|
152
|
+
if (isActive(snapshot.status)) return snapshot
|
|
153
|
+
if (this.disposed) {
|
|
154
|
+
throw new DevccError({ scope: "process", message: "Shutting down; refusing to start new processes" })
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.update(id, {
|
|
158
|
+
status: "starting",
|
|
159
|
+
exitCode: undefined,
|
|
160
|
+
exitSignal: undefined,
|
|
161
|
+
lastError: undefined,
|
|
162
|
+
...(options.auto ? {} : { crashRestarts: 0, crashRestartsExhausted: false }),
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const cwd = definition.cwd ? resolveCwd(this.rootDir, definition.cwd) : this.rootDir
|
|
166
|
+
const buffer = this.logs.register(id, definition.name)
|
|
167
|
+
buffer.push(`$ ${describeCommand({ command: definition.command, args: definition.args })}`, "system")
|
|
168
|
+
|
|
169
|
+
let child: ChildProcess
|
|
170
|
+
try {
|
|
171
|
+
child = spawn(definition.command, definition.args ? [...definition.args] : [], {
|
|
172
|
+
cwd,
|
|
173
|
+
env: { ...this.baseEnv, ...definition.env, FORCE_COLOR: definition.env?.FORCE_COLOR ?? "0" },
|
|
174
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
175
|
+
// Own process group so we can signal the whole tree on stop.
|
|
176
|
+
detached: process.platform !== "win32",
|
|
177
|
+
windowsHide: true,
|
|
178
|
+
})
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const message = `Failed to start ${definition.name}: ${errorMessage(error)}`
|
|
181
|
+
this.update(id, { status: "failed", lastError: message })
|
|
182
|
+
const devccError = new DevccError({ scope: "process", message, cause: error })
|
|
183
|
+
this.emit("process:error", devccError)
|
|
184
|
+
throw devccError
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let resolveExit: () => void = () => {}
|
|
188
|
+
const exitPromise = new Promise<void>((resolve) => {
|
|
189
|
+
resolveExit = resolve
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const runtime: Runtime = {
|
|
193
|
+
child,
|
|
194
|
+
pgid: child.pid !== undefined && process.platform !== "win32" ? child.pid : null,
|
|
195
|
+
killTimer: null,
|
|
196
|
+
exitPromise,
|
|
197
|
+
resolveExit,
|
|
198
|
+
}
|
|
199
|
+
this.runtimes.set(id, runtime)
|
|
200
|
+
|
|
201
|
+
child.stdout?.setEncoding("utf8")
|
|
202
|
+
child.stdout?.on("data", (chunk: string) => this.logs.appendChunk(id, chunk, "stdout"))
|
|
203
|
+
child.stderr?.setEncoding("utf8")
|
|
204
|
+
child.stderr?.on("data", (chunk: string) => this.logs.appendChunk(id, chunk, "stderr"))
|
|
205
|
+
|
|
206
|
+
child.once("error", (error: Error) => {
|
|
207
|
+
const message = `${definition.name}: ${error.message}`
|
|
208
|
+
this.logs.append(id, message, "stderr")
|
|
209
|
+
this.update(id, { status: "failed", lastError: message, pid: undefined })
|
|
210
|
+
this.finishRuntime(id)
|
|
211
|
+
this.emit("process:error", new DevccError({ scope: "process", message, cause: error }))
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
child.once("exit", (code, signal) => {
|
|
215
|
+
this.handleExit(id, code, signal)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
this.update(id, {
|
|
219
|
+
pid: child.pid,
|
|
220
|
+
startedAt: new Date(),
|
|
221
|
+
stoppedAt: undefined,
|
|
222
|
+
ready: false,
|
|
223
|
+
readinessTimedOut: false,
|
|
224
|
+
})
|
|
225
|
+
this.logger.info("process started", { id, pid: child.pid, cwd })
|
|
226
|
+
|
|
227
|
+
if (definition.readiness) {
|
|
228
|
+
// Stay in `starting` until the check passes; `running` means usable.
|
|
229
|
+
this.beginReadinessCheck(id, definition.readiness)
|
|
230
|
+
} else {
|
|
231
|
+
this.markReady(id, false)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const started = this.snapshots.get(id)
|
|
235
|
+
if (started) this.emit("process:started", started)
|
|
236
|
+
return started ?? snapshot
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Resolve once the process is ready (readiness passed or timed out), or
|
|
241
|
+
* immediately if it is not starting. Used to sequence dependent starts.
|
|
242
|
+
*/
|
|
243
|
+
async waitForReady(id: string): Promise<ManagedProcess | undefined> {
|
|
244
|
+
const pending = this.readiness.get(id)
|
|
245
|
+
if (pending) await pending.done
|
|
246
|
+
return this.snapshots.get(id)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Start several processes honouring `dependsOn`: each wave waits for the
|
|
251
|
+
* previous one to become ready. Failures in one wave are reported and the
|
|
252
|
+
* next wave still starts, since an unrelated process should not be blocked.
|
|
253
|
+
*/
|
|
254
|
+
async startAll(ids?: readonly string[]): Promise<StartPlan> {
|
|
255
|
+
const plan = planStartOrder([...this.definitions.values()], ids)
|
|
256
|
+
|
|
257
|
+
for (const edge of plan.missing) {
|
|
258
|
+
this.logs.append(edge.id, `dependsOn "${edge.dependsOn}" is not a known process; ignoring`, "stderr")
|
|
259
|
+
}
|
|
260
|
+
if (plan.cycle.length > 0) {
|
|
261
|
+
this.emit(
|
|
262
|
+
"process:error",
|
|
263
|
+
new DevccError({
|
|
264
|
+
scope: "process",
|
|
265
|
+
message: `Dependency cycle between ${plan.cycle.join(", ")}; not starting them`,
|
|
266
|
+
}),
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const wave of plan.waves) {
|
|
271
|
+
const targets = wave.filter((id) => {
|
|
272
|
+
const snapshot = this.snapshots.get(id)
|
|
273
|
+
return snapshot && !isActive(snapshot.status)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
await Promise.all(
|
|
277
|
+
targets.map(async (id) => {
|
|
278
|
+
try {
|
|
279
|
+
await this.start(id)
|
|
280
|
+
} catch (error) {
|
|
281
|
+
this.logger.warn("start failed", { id, error: errorMessage(error) })
|
|
282
|
+
}
|
|
283
|
+
}),
|
|
284
|
+
)
|
|
285
|
+
// Wait for the whole wave (including ones already running) before moving on.
|
|
286
|
+
await Promise.all(wave.map((id) => this.waitForReady(id)))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return plan
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private beginReadinessCheck(id: string, check: ReadinessCheck): void {
|
|
293
|
+
this.cancelReadiness(id)
|
|
294
|
+
|
|
295
|
+
const timeoutMs = check.timeoutMs ?? DEFAULT_READINESS.timeoutMs
|
|
296
|
+
const intervalMs = check.intervalMs ?? DEFAULT_READINESS.intervalMs
|
|
297
|
+
let cancelled = false
|
|
298
|
+
let settle: () => void = () => {}
|
|
299
|
+
const done = new Promise<void>((resolve) => {
|
|
300
|
+
settle = resolve
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
const finish = (timedOut: boolean) => {
|
|
304
|
+
if (cancelled) return
|
|
305
|
+
cancelled = true
|
|
306
|
+
this.readiness.delete(id)
|
|
307
|
+
if (logListener) this.logs.off("log:append", logListener)
|
|
308
|
+
clearTimeout(timer)
|
|
309
|
+
if (this.isActiveRuntime(id)) this.markReady(id, timedOut)
|
|
310
|
+
settle()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// --- log pattern -------------------------------------------------------
|
|
314
|
+
let logListener: ((sourceId: string, entries: LogEntry[]) => void) | null = null
|
|
315
|
+
if (check.logPattern) {
|
|
316
|
+
let pattern: RegExp | null = null
|
|
317
|
+
try {
|
|
318
|
+
pattern = new RegExp(check.logPattern, "iu")
|
|
319
|
+
} catch {
|
|
320
|
+
this.logs.append(id, `readiness.logPattern is not a valid regex: ${check.logPattern}`, "stderr")
|
|
321
|
+
}
|
|
322
|
+
if (pattern) {
|
|
323
|
+
const matcher = pattern
|
|
324
|
+
logListener = (sourceId, entries) => {
|
|
325
|
+
if (sourceId !== id) return
|
|
326
|
+
if (entries.some((entry) => matcher.test(entry.text))) finish(false)
|
|
327
|
+
}
|
|
328
|
+
this.logs.on("log:append", logListener)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// --- port probe --------------------------------------------------------
|
|
333
|
+
const probe = async () => {
|
|
334
|
+
while (!cancelled) {
|
|
335
|
+
if (
|
|
336
|
+
check.port &&
|
|
337
|
+
(await probeTcpPort(check.port, check.host ?? "127.0.0.1", Math.min(intervalMs, 700)))
|
|
338
|
+
) {
|
|
339
|
+
finish(false)
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
if (!check.port && !logListener) {
|
|
343
|
+
// Nothing to wait for after all.
|
|
344
|
+
finish(false)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs))
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const timer = setTimeout(() => {
|
|
352
|
+
this.logs.append(id, `Readiness check timed out after ${timeoutMs}ms; reporting as running`, "stderr")
|
|
353
|
+
finish(true)
|
|
354
|
+
}, timeoutMs)
|
|
355
|
+
timer.unref?.()
|
|
356
|
+
|
|
357
|
+
this.readiness.set(id, { cancel: () => finish(false), done })
|
|
358
|
+
void probe()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private cancelReadiness(id: string): void {
|
|
362
|
+
const pending = this.readiness.get(id)
|
|
363
|
+
if (!pending) return
|
|
364
|
+
this.readiness.delete(id)
|
|
365
|
+
pending.cancel()
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private isActiveRuntime(id: string): boolean {
|
|
369
|
+
const snapshot = this.snapshots.get(id)
|
|
370
|
+
return this.runtimes.has(id) && snapshot?.status === "starting"
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private markReady(id: string, timedOut: boolean): void {
|
|
374
|
+
this.update(id, { status: "running", ready: true, readinessTimedOut: timedOut })
|
|
375
|
+
const snapshot = this.snapshots.get(id)
|
|
376
|
+
if (snapshot) {
|
|
377
|
+
if (!timedOut && this.definitions.get(id)?.readiness) this.logs.append(id, "Ready", "system")
|
|
378
|
+
this.emit("process:ready", snapshot)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Stop a managed process and everything it spawned.
|
|
384
|
+
* Resolves once the child has actually exited (or the grace period elapsed).
|
|
385
|
+
*/
|
|
386
|
+
async stop(id: string, graceMs?: number): Promise<void> {
|
|
387
|
+
const snapshot = this.snapshots.get(id)
|
|
388
|
+
const runtime = this.runtimes.get(id)
|
|
389
|
+
if (!snapshot) throw new DevccError({ scope: "process", message: `Unknown process "${id}"` })
|
|
390
|
+
if (snapshot.ownership !== "managed") {
|
|
391
|
+
throw new DevccError({
|
|
392
|
+
scope: "process",
|
|
393
|
+
message: `"${snapshot.name}" was detected, not launched by devcc - refusing to signal it`,
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
// A manual stop cancels any pending automatic restart and clears the
|
|
397
|
+
// crash budget: the user is now in charge of this process.
|
|
398
|
+
this.cancelRetry(id)
|
|
399
|
+
this.cancelReadiness(id)
|
|
400
|
+
this.update(id, { crashRestarts: 0, retryAt: undefined, crashRestartsExhausted: false })
|
|
401
|
+
|
|
402
|
+
if (!runtime || !isActive(snapshot.status)) return
|
|
403
|
+
|
|
404
|
+
this.update(id, { status: "stopping" })
|
|
405
|
+
this.logs.append(id, "Stopping…", "system")
|
|
406
|
+
|
|
407
|
+
const grace = graceMs ?? this.definitions.get(id)?.killGraceMs ?? DEFAULT_KILL_GRACE_MS
|
|
408
|
+
this.signal(runtime, "SIGTERM")
|
|
409
|
+
|
|
410
|
+
runtime.killTimer = setTimeout(() => {
|
|
411
|
+
if (this.runtimes.has(id)) {
|
|
412
|
+
this.logs.append(id, "Grace period elapsed, sending SIGKILL", "system")
|
|
413
|
+
this.signal(runtime, "SIGKILL")
|
|
414
|
+
}
|
|
415
|
+
}, grace)
|
|
416
|
+
runtime.killTimer.unref?.()
|
|
417
|
+
|
|
418
|
+
await runtime.exitPromise
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async restart(id: string): Promise<ManagedProcess> {
|
|
422
|
+
const snapshot = this.snapshots.get(id)
|
|
423
|
+
if (!snapshot) throw new DevccError({ scope: "process", message: `Unknown process "${id}"` })
|
|
424
|
+
this.cancelRetry(id)
|
|
425
|
+
if (isActive(snapshot.status)) await this.stop(id)
|
|
426
|
+
const restarted = await this.start(id)
|
|
427
|
+
this.update(id, { restarts: (this.snapshots.get(id)?.restarts ?? 0) + 1 })
|
|
428
|
+
return this.snapshots.get(id) ?? restarted
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Start every definition marked `autoStart`. Failures are reported, not thrown. */
|
|
432
|
+
async startAutoStart(): Promise<void> {
|
|
433
|
+
const targets = [...this.definitions.values()].filter((definition) => definition.autoStart)
|
|
434
|
+
if (targets.length === 0) return
|
|
435
|
+
await this.startAll(targets.map((definition) => definition.id))
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Stop everything devcc launched. Called on shutdown; never touches processes
|
|
440
|
+
* that were only detected.
|
|
441
|
+
*/
|
|
442
|
+
async stopAll(graceMs = DEFAULT_KILL_GRACE_MS): Promise<void> {
|
|
443
|
+
this.disposed = true
|
|
444
|
+
for (const id of [...this.retryTimers.keys()]) this.cancelRetry(id)
|
|
445
|
+
const running = this.list().filter((snapshot) => isActive(snapshot.status))
|
|
446
|
+
await Promise.all(
|
|
447
|
+
running.map(async (snapshot) => {
|
|
448
|
+
try {
|
|
449
|
+
await this.stop(snapshot.id, graceMs)
|
|
450
|
+
} catch (error) {
|
|
451
|
+
this.logger.warn("stop failed during shutdown", { id: snapshot.id, error: errorMessage(error) })
|
|
452
|
+
}
|
|
453
|
+
}),
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private handleExit(id: string, code: number | null, signal: NodeJS.Signals | null): void {
|
|
458
|
+
this.cancelReadiness(id)
|
|
459
|
+
this.logs.flush(id)
|
|
460
|
+
const snapshot = this.snapshots.get(id)
|
|
461
|
+
const wasStopping = snapshot?.status === "stopping"
|
|
462
|
+
const kind = snapshot?.kind ?? "service"
|
|
463
|
+
|
|
464
|
+
const failed = !wasStopping && code !== 0 && code !== null
|
|
465
|
+
const nextStatus: ProcessStatus = failed ? "failed" : "stopped"
|
|
466
|
+
|
|
467
|
+
this.logs.append(
|
|
468
|
+
id,
|
|
469
|
+
signal ? `Exited via ${signal}` : `Exited with code ${code ?? "unknown"}`,
|
|
470
|
+
failed ? "stderr" : "system",
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
// A run that lasted long enough counts as healthy and clears the budget.
|
|
474
|
+
const definition = this.definitions.get(id)
|
|
475
|
+
const ranFor = snapshot?.startedAt ? Date.now() - snapshot.startedAt.getTime() : 0
|
|
476
|
+
const healthyAfter = definition?.healthyAfterMs ?? DEFAULT_CRASH_POLICY.healthyAfterMs
|
|
477
|
+
const wasHealthy = ranFor >= healthyAfter
|
|
478
|
+
const crashRestarts = failed ? (wasHealthy ? 0 : (snapshot?.crashRestarts ?? 0)) : 0
|
|
479
|
+
|
|
480
|
+
this.update(id, {
|
|
481
|
+
status: nextStatus,
|
|
482
|
+
pid: undefined,
|
|
483
|
+
exitCode: code ?? undefined,
|
|
484
|
+
exitSignal: signal ?? undefined,
|
|
485
|
+
stoppedAt: new Date(),
|
|
486
|
+
crashRestarts,
|
|
487
|
+
retryAt: undefined,
|
|
488
|
+
ready: false,
|
|
489
|
+
lastError: failed ? `${snapshot?.name ?? id} exited with code ${code ?? "?"}` : undefined,
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
this.finishRuntime(id)
|
|
493
|
+
|
|
494
|
+
const updated = this.snapshots.get(id)
|
|
495
|
+
if (updated) {
|
|
496
|
+
this.logger.info("process exited", { id, code, signal, kind })
|
|
497
|
+
this.emit("process:exit", updated)
|
|
498
|
+
if (failed) this.considerAutoRestart(id)
|
|
499
|
+
if (failed) {
|
|
500
|
+
this.emit(
|
|
501
|
+
"process:error",
|
|
502
|
+
new DevccError({
|
|
503
|
+
scope: "process",
|
|
504
|
+
message: `${updated.name} exited with code ${code ?? "?"}`,
|
|
505
|
+
detail: `Command: ${describeCommand(updated)}`,
|
|
506
|
+
}),
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Schedule an automatic restart for a crashed process, if its definition opted
|
|
514
|
+
* in and it still has budget left. Backoff doubles per consecutive crash, so a
|
|
515
|
+
* process that is broken rather than flaky stops thrashing quickly.
|
|
516
|
+
*/
|
|
517
|
+
private considerAutoRestart(id: string): void {
|
|
518
|
+
if (this.disposed) return
|
|
519
|
+
const definition = this.definitions.get(id)
|
|
520
|
+
const snapshot = this.snapshots.get(id)
|
|
521
|
+
if (!definition || !snapshot) return
|
|
522
|
+
|
|
523
|
+
if (!definition.restartOnCrash) return
|
|
524
|
+
|
|
525
|
+
if (!shouldAutoRestart(definition, snapshot)) {
|
|
526
|
+
if (!snapshot.crashRestartsExhausted) {
|
|
527
|
+
this.update(id, { crashRestartsExhausted: true })
|
|
528
|
+
this.logs.append(id, `Giving up after ${snapshot.crashRestarts} automatic restarts`, "system")
|
|
529
|
+
this.emit(
|
|
530
|
+
"process:error",
|
|
531
|
+
new DevccError({
|
|
532
|
+
scope: "process",
|
|
533
|
+
message: `${definition.name} keeps crashing; automatic restarts exhausted`,
|
|
534
|
+
detail: `Last exit code ${snapshot.exitCode ?? "?"}. Start it manually to try again.`,
|
|
535
|
+
}),
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const attempt = snapshot.crashRestarts + 1
|
|
542
|
+
const delay = crashBackoffDelay(attempt, definition.crashBackoffMs, definition.maxCrashBackoffMs)
|
|
543
|
+
const retryAt = new Date(Date.now() + delay)
|
|
544
|
+
|
|
545
|
+
this.update(id, { crashRestarts: attempt, retryAt, crashRestartsExhausted: false })
|
|
546
|
+
this.logs.append(
|
|
547
|
+
id,
|
|
548
|
+
`Restarting automatically in ${Math.round(delay / 100) / 10}s (attempt ${attempt})`,
|
|
549
|
+
"system",
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
const updated = this.snapshots.get(id)
|
|
553
|
+
if (updated) this.emit("process:retry", updated, delay)
|
|
554
|
+
|
|
555
|
+
const timer = setTimeout(() => {
|
|
556
|
+
this.retryTimers.delete(id)
|
|
557
|
+
if (this.disposed) return
|
|
558
|
+
// The user may have started or removed it while we waited.
|
|
559
|
+
const current = this.snapshots.get(id)
|
|
560
|
+
if (!current || isActive(current.status)) return
|
|
561
|
+
this.update(id, { retryAt: undefined })
|
|
562
|
+
void this.start(id, { auto: true }).catch((error: unknown) => {
|
|
563
|
+
this.logger.warn("automatic restart failed", { id, error: errorMessage(error) })
|
|
564
|
+
})
|
|
565
|
+
}, delay)
|
|
566
|
+
timer.unref?.()
|
|
567
|
+
this.retryTimers.set(id, timer)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
private cancelRetry(id: string): void {
|
|
571
|
+
const timer = this.retryTimers.get(id)
|
|
572
|
+
if (!timer) return
|
|
573
|
+
clearTimeout(timer)
|
|
574
|
+
this.retryTimers.delete(id)
|
|
575
|
+
this.update(id, { retryAt: undefined })
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private finishRuntime(id: string): void {
|
|
579
|
+
const runtime = this.runtimes.get(id)
|
|
580
|
+
if (!runtime) return
|
|
581
|
+
if (runtime.killTimer) clearTimeout(runtime.killTimer)
|
|
582
|
+
this.runtimes.delete(id)
|
|
583
|
+
runtime.resolveExit()
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** Signal the child's process group when we have one, else the child alone. */
|
|
587
|
+
private signal(runtime: Runtime, signal: NodeJS.Signals): void {
|
|
588
|
+
const { child, pgid } = runtime
|
|
589
|
+
try {
|
|
590
|
+
if (pgid !== null) {
|
|
591
|
+
process.kill(-pgid, signal)
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
} catch (error) {
|
|
595
|
+
const code = (error as NodeJS.ErrnoException).code
|
|
596
|
+
// ESRCH: the group is already gone. Anything else: fall back to the child.
|
|
597
|
+
if (code === "ESRCH") return
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
child.kill(signal)
|
|
601
|
+
} catch {
|
|
602
|
+
// The process is already gone.
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private update(id: string, patch: Partial<ManagedProcess>): void {
|
|
607
|
+
const current = this.snapshots.get(id)
|
|
608
|
+
if (!current) return
|
|
609
|
+
|
|
610
|
+
if (patch.status && patch.status !== current.status && !canTransition(current.status, patch.status)) {
|
|
611
|
+
this.logger.debug("ignored illegal status transition", { id, from: current.status, to: patch.status })
|
|
612
|
+
const { status: _ignored, ...rest } = patch
|
|
613
|
+
if (Object.keys(rest).length === 0) return
|
|
614
|
+
patch = rest
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const next: ManagedProcess = { ...current, ...patch }
|
|
618
|
+
this.snapshots.set(id, next)
|
|
619
|
+
this.emit("process:changed", next)
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function resolveCwd(rootDir: string, cwd: string): string {
|
|
624
|
+
return isAbsolute(cwd) ? cwd : resolve(rootDir, cwd)
|
|
625
|
+
}
|