feat(process-manager): add ProcessManager with unified lifecycle, events, and service integrations - #13856
feat(process-manager): add ProcessManager with unified lifecycle, events, and service integrations#13856DeJeune wants to merge 33 commits into
Conversation
Define ProcessState enum, ProcessHandle, UtilityProcessHandle, ProcessLogLine, ProcessManagerEvents, ChildProcessDefinition, UtilityProcessDefinition, and ProcessDefinition union type as the foundation for the ProcessManager system. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…d logging Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…o loggerService - In the error event handler, call onExited(null, null) after setting state to Crashed so ProcessManagerService receives process:exited for spawn failures (e.g. ENOENT). - Forward stdout data to logger.debug and stderr data to logger.warn before invoking the onLog callback, so output appears in Winston logs. - Add test: error event triggers onExited with (null, null). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…and events Adds ProcessManagerService as a lifecycle service extending BaseService. Provides a central registry for managed child processes with a unified event hub (process:started, process:exited, process:log) forwarded from individual ChildProcessHandle callbacks. Graceful shutdown stops all running processes in parallel, catching individual errors so one failure does not block others. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…stry Add barrel export for process module and register ProcessManagerService in the centralized service registry for lifecycle-managed access. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…t IPC Add UtilityProcessHandle class wrapping Electron's utilityProcess.fork() for structured-clone MessagePort communication. Update ProcessManagerService to handle type: 'utility' registration and update barrel exports. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
Electron's utilityProcess.fork() takes (modulePath, args?, options?), not (modulePath, options). Fixed the call to pass args as the second parameter and options as the third. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
Add TaskExecutor, a composite layer over ProcessManagerService that manages a pool of utility process workers for parallel task dispatch. Supports worker reuse, auto-scaling up to a max cap, task queuing, idle timeouts, and clean shutdown with rejection of pending tasks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…ager Shorter name, consistent with the pattern used by other services. Updated class name, @Injectable identifier, file names, imports, exports, and documentation references. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…ldProcessDefinition - Add `detached`, `stdio`, and `skipOnStop` optional fields to `ChildProcessDefinition` in types.ts - Add `skipOnStop: boolean` readonly property to `ProcessHandle` interface - Import `StdioOptions` from `child_process` in types.ts - Pass `detached` and `stdio` through to `crossPlatformSpawn` in `ChildProcessHandle.start()` - Call `child.unref()` when `detached` is true - Add `skipOnStop` getter to `ChildProcessHandle` (reads from def, defaults false) - Add `skipOnStop` getter to `UtilityProcessHandle` (always false) - Filter out `skipOnStop` handles in `ProcessManager.onStop()` - Add `unref: vi.fn()` to `createMockChildProcess` in both test files - Add tests for detached, stdio, skipOnStop options in ChildProcessHandle.test.ts - Add skipOnStop test in ProcessManager.test.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
Replace fire-and-forget exec() in runOvms() with ProcessManager registration so the ovms-server handle is tracked and stopped automatically during application.shutdown(). stopOvms() now delegates to the PM handle when available, falling back to the PowerShell kill for externally-started processes. getOvmsStatus() checks the PM handle before querying PowerShell. Remove the explicit ovmsManager.stopOvms() call from the will-quit handler in index.ts — ProcessManager.onStop() handles cleanup automatically. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
Signed-off-by: suyao <sy20010504@gmail.com> # Conflicts: # src/main/core/application/serviceRegistry.ts # src/main/index.ts
… tests - Wrap crossPlatformSpawn() and utilityProcess.fork() in try/catch so state transitions to Crashed and onExited fires on synchronous errors - Tighten weak toContain assertions in TaskExecutor tests to exact match - Add edge-case tests: spawn throw, fork throw, pid undefined, skipOnStop - Remove trivial types.test.ts (enum string-value checks) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…rface - Delete duplicate `interface UtilityProcessHandle` from types.ts that shadowed the class of the same name and was never referenced - Add explicit `implements ProcessHandle` to the UtilityProcessHandle class for compile-time contract enforcement Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…ype discriminant
- Extract common fields (id, args, env, killTimeoutMs) into ProcessOptions base
- Rename ChildProcessDefinition → ChildProcessOptions
- Rename UtilityProcessDefinition → UtilityProcessOptions
- Remove type discriminant field; use structural narrowing ('modulePath' in options)
- Simplify ProcessManager.register() callback wiring (deduplicate)
- Update all callers and tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
DeJeune
left a comment
There was a problem hiding this comment.
Well-structured process management addition with clean architecture (primitive handles → orchestrator → composite executor). Found 4 issues:
- [A1]
ChildProcessHandle.stop()can hang if process exits during race window (missingresolve()in kill timeout) - [A7] Double
onExitedcallback when botherrorandcloseevents fire - [A7] OpenClawService overrides callbacks after
start(), creating a race for early exit diagnostics - [C1] String literal
'running'instead ofProcessState.Runningenum in OpenClawService
…ack race - ChildProcessHandle.stop(): add resolve() in kill timeout callback to prevent promise hanging when process exits during race window - ChildProcessHandle: add _exited guard to prevent double onExited callback when both error and close events fire - OpenClawService: move onLog/onExited callback overrides before handle.start() to capture early process events - OpenClawService: use ProcessState.Running enum instead of string literal for consistency with OvmsManager Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…k and add _exited guard to UtilityProcessHandle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…down filter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…ate leaks on assertion failure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e enabled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…pectedly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…t listener. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add taskTimeoutMs option to TaskExecutor for per-task timeout support - Add handleTaskTimeout to reject tasks and free workers when timeout fires - Clear timeout timers on normal response, shutdown, and worker crash - Add public getters (workerCount, pendingCount, queueLength) to TaskExecutor - Fix scheduleIdleTimeout to use finally for unregister (handles stop() failure) - Refactor tests: use direct state assertions, remove redundant/trivial tests - Add task timeout tests (TDD red→green) - Fix unhandled promise rejections across all test cases Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
- Remove pass-through tests (fork args, spawn env, detached, stdio options) - Remove hardcoded constant tests (skipOnStop) - Remove pure wiring tests (onStarted, onExited callbacks) - Merge duplicate stdout/stderr log tests into single test - Replace dynamic imports with static imports, remove loadModules() - Use vi.hoisted() for UtilityProcessHandle electron mock Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
- Remove trivial initial state tests (already covered by handle tests) - Remove UtilityProcessHandle type test that only checked id/state - Remove Map.get() undefined semantics test - Remove empty onInit test - Remove unregister no-op for unknown id test - Merge duplicate stdout/stderr log event tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
Conflicts: - src/main/index.ts: removed unused `runAsyncFunction` import (v2 deleted it) - src/main/services/OvmsManager.ts: combined lifecycle decorator imports from v2 with application/ProcessState imports from our branch; dropped unused isWin and getCpuName imports Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…on, reduce duplication
- Extract DEFAULT_KILL_TIMEOUT_MS to types.ts (was duplicated in both handles)
- Replace Date.now()+Math.random() task ID with crypto.randomUUID()
- Use spawnWorker() return value directly instead of re-scanning workers map
- Remove redundant vi.mock('@logger') from 4 test files (global setup handles it)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
| return this.def.skipOnStop ?? false | ||
| } | ||
|
|
||
| async start(): Promise<void> { |
There was a problem hiding this comment.
必须修复:An in-flight start remains visible as Idle, so concurrent starts or unregister/replacement can create an untracked child process.
inv_d75b471e44106063#c0
| }) | ||
| } | ||
|
|
||
| await handle.start() |
There was a problem hiding this comment.
必须修复:OpenClaw startup and restart can leave a managed gateway handle registered after failure, causing later registration to fail with a duplicate-id error.
inv_d75b471e44106063#c1
| this.dispatch() | ||
| } | ||
|
|
||
| private handleTaskTimeout(taskId: string, entry: WorkerEntry): void { |
There was a problem hiding this comment.
建议修复:Timeout cleanup marks workers idle or terminal before the underlying process has exited, allowing reuse of a worker that may still be executing and suppressing exit notifications.
inv_d75b471e44106063#c2
|
|
||
| this.workers.set(workerId, entry) | ||
|
|
||
| await handle.start() |
There was a problem hiding this comment.
建议修复:A worker start failure can strand queued exec() promises indefinitely.
inv_d75b471e44106063#c3
| export { ChildProcessHandle } from './ChildProcessHandle' | ||
| export { ProcessManager } from './ProcessManager' | ||
| export type { TaskExecutorOptions } from './TaskExecutor' | ||
| export { TaskExecutor } from './TaskExecutor' |
There was a problem hiding this comment.
必须修复:TaskExecutor is exposed as a new public process abstraction without a legitimate production consumer or concrete invariant requiring it.
inv_d75b471e44106063#c4
| /** | ||
| * Process states - shared across all process types | ||
| */ | ||
| enum ProcessState { |
There was a problem hiding this comment.
建议修复:The English and Chinese process-manager references document public types and examples that do not match the shipped implementation.
inv_d75b471e44106063#c8
|
|
||
| ### Graceful Shutdown Sequence | ||
|
|
||
| When `ProcessManager.onStop()` is called (app shutdown): |
There was a problem hiding this comment.
建议修复:The bilingual shutdown and migration documentation contradicts current ownership and lifecycle behavior.
inv_d75b471e44106063#c9
|
|
||
| let proc: Electron.UtilityProcess | ||
| try { | ||
| proc = utilityProcess.fork(this.def.modulePath, this.def.args, { |
There was a problem hiding this comment.
建议修复:Utility workers launched by TaskExecutor can receive an empty environment instead of inheriting the main-process environment.
inv_d75b471e44106063#c10
| this.onStarted?.(child.pid) | ||
| } | ||
|
|
||
| child.stdout?.on('data', (data: Buffer) => { |
There was a problem hiding this comment.
待作者确认:Shell output is decoded as UTF-8 rather than with the repository's Windows OEM/GBK decoder, so Chinese Windows output can be garbled.
inv_d75b471e44106063#c11
| const mockCp1 = createMockChildProcess(1111) | ||
| const mockCp2 = createMockChildProcess(2222) | ||
|
|
||
| mockCp1.kill = vi.fn().mockImplementation(() => { |
There was a problem hiding this comment.
建议修复:The ProcessManager shutdown test leaves a delayed SIGKILL timer whose callback throws outside the test promise.
inv_d75b471e44106063#c12
|
Thank you very much for the extensive work on unified process management. The main-process lifecycle, service ownership, and process integrations have continued to evolve since this PR was opened. This implementation also has unresolved review blockers and no longer applies cleanly to current We are closing the PR as part of the v2 cleanup. The underlying requirement may still be valuable; if you would like to continue, you are very welcome to reopen this PR and redesign/update it against the latest Thank you again for your contribution and understanding. |
Resolve ProcessManager integration against the current lifecycle, process runner, and documentation architecture. Signed-off-by: suyao <sy20010504@gmail.com>
Signed-off-by: suyao <sy20010504@gmail.com>
What this PR does
Before this PR:
exec()with no stored PID, PowerShell kill-by-name,pkill -9, etc.After this PR:
ProcessManagerlifecycle service that provides unified process registration, lifecycle management (start/stop/restart), state tracking, event hub, and graceful shutdown.ChildProcessHandlewrapscrossPlatformSpawnwith SIGTERM→SIGKILL graceful shutdown, log forwarding, and state machine (Idle→Running→Stopping→Stopped/Crashed).UtilityProcessHandlewraps Electron'sutilityProcess.fork()with MessagePort IPC.TaskExecutorprovides a composite layer for parallel task dispatch via auto-scaling worker pools.Why we need it and why it was done in this way
Centralized process management provides:
process:started,process:exited,process:logeventsonStop()The following tradeoffs were made:
skipOnStop: truebecause it's intentionally detached to survive app exit — ProcessManager tracks it but doesn't kill it on shutdown.StdioClientTransportinternally, and wrapping it would be fragile.addModel()/stopAddModel()stay as-is (one-shotexecAsyncwith stdout capture) — only the long-running server process uses ProcessManager.The following alternatives were considered:
StdioClientTransport— rejected due to tight SDK couplingBreaking changes
None. This is additive — existing behavior is preserved.
Special notes for your reviewer
ChildProcessHandleandUtilityProcessHandleare the primitive layer;ProcessManageris the orchestration layer;TaskExecutoris the composite layer.stopOvms()fromwill-quithandler —ProcessManager.onStop()handles it duringapplication.shutdown().killAllOpenClawProcesses()is kept for stopping since the gateway may be externally started.Checklist
Release note