gadget/gadget-drone/docs/subprocess.md
Rob Colbert e155a7ffcf Phase 2: SubProcess observability in DroneManager and DroneInspector
Adds real-time subprocess monitoring to the IDE via the callback-chain and
subscribe/broadcast socket patterns:

Shared types (packages/api):
- New subprocess.ts message types: SubProcessStat, RequestProcessStatsMessage,
  SubscribeDroneMessage, UnsubscribeDroneMessage
- New socket events in socket.ts: requestProcessStats, subscribeDrone,
  unsubscribeDrone (ClientToServer); drone:log, drone:status (ServerToClient)

Drone (gadget-drone):
- onRequestProcessStats handler calls SubProcessService.ps() + summarize(),
  returns typed SubProcessStat[] with status detection (exitCode/killed)

Backend routing (gadget-code):
- SocketService: getDroneSessionByRegistrationId(), droneMonitorIndex map,
  addDroneMonitor()/removeDroneMonitor()/broadcastToMonitors()
- CodeSession: requestProcessStats proxy, subscribeDrone/unsubscribeDrone
- DroneSession: broadcast drone:log + drone:status to monitor subscribers
  in addition to existing chat-session routing

Frontend socket layer:
- SocketClient: requestProcessStats(), subscribeDrone(), unsubscribeDrone()
- drone:log + drone:status events forwarded to event bus

Frontend components:
- LogRenderer — reusable log rendering core extracted from LogPanel
- SubProcessTable — btop-style process table with status dots
- DroneMonitorGauge — canvas oscilloscope waveform (studio-equipment aesthetic)
- DroneMonitor — 3-gauge container (CPU/red, NETWORK/cyan, FILE I/O/green)

Frontend pages:
- DroneManager: live log streaming, SubProcessTable, DroneMonitor gauges,
  1-second polling, auto-scroll log with pause-on-scroll-up
- Home/DroneInspector: process count summary, mini LogRenderer, navigate-to-manager link

Documentation:
- Updated gadget-drone/docs/subprocess.md Phase 2 section
- Updated gadget-code/docs/ui-design-guide.md Phase 2 section
- Updated docs/socket-protocol.md with new events, sequences, and patterns
2026-05-14 13:52:59 -04:00

232 lines
9.4 KiB
Markdown

# Gadget Drone Sub-Processes
The Gadget Code agent manages child processes through gadget-drone's SubProcess system. In the Gadget ecosystem, we refer to a managed child process spawned by gadget-drone as a _SubProcess_.
## Phase Status
- **Phase 1 (Complete):** SubProcessService + SubprocessTool internal to drone.
- **Phase 2 (Complete):** SubProcess observability in DroneManager and DroneInspector. Live log streaming via subscribe/broadcast pattern. Canvas-based resource gauges (CPU, Network, File I/O — mocked). See [Phase 2 Details](#phase-2-subprocess-observability) below.
## SubProcessService
[Source](../src/services/subprocess.ts)
`SubProcessService` is a singleton service that manages child processes on behalf of Gadget Drone. It is started and stopped as part of the standard service lifecycle in `gadget-drone.ts`. On stop, all managed subprocesses are killed (SIGINT with graceful timeout, falling back to SIGKILL) and their log files are removed.
### API
| Method | Signature | Description |
|---|---|---|
| `create` | `(project: IProject, cmd: string, args?: string[]) => Promise<SubProcess>` | Spawns a child process, starts writing stdout/stderr to log files under `<gadgetDir>/subprocess-logs/`. |
| `ps` | `() => SubProcess[]` | Returns an array of all managed subprocesses. |
| `summarize` | `(sp: SubProcess) => SubProcessSummary` | Returns a serializable summary (without ChildProcess/WriteStream refs). |
| `killPid` | `(pid: number) => Promise<boolean>` | Kills a specific subprocess by PID and removes its log files. |
| `killProject` | `(projectId: GadgetId) => Promise<void>` | Kills all subprocesses for a given project. |
| `killSubProcess` | `(sp: SubProcess) => Promise<boolean>` | Kills a subprocess and removes its log files. |
| `killAll` | `() => Promise<boolean>` | Kills all managed subprocesses and removes all log files. |
| `haltPid` | `(pid: number) => Promise<boolean>` | Stops a subprocess by PID but **retains its log files** for inspection. |
| `haltSubProcess` | `(sp: SubProcess) => Promise<boolean>` | Stops a subprocess but retains its log files. |
| `getLog` | `(pid: number, stream: "stdout" \| "stderr") => Promise<string>` | Reads the full content of a subprocess's stdout or stderr log file. |
### Lifecycle
The service is registered in `GadgetDrone.startServices()` / `stopServices()`. During `stop()`, `killAll()` is called, ensuring no orphaned processes remain when the drone shuts down.
### Process Termination
Termination follows a graceful escalation:
1. Send `SIGINT` to the process.
2. Wait up to 5 seconds for the process to exit.
3. If still alive, send `SIGKILL`.
4. Wait up to another 5 seconds.
5. Remove stdout/stderr listeners.
6. Close log file streams.
For `kill`, log files are deleted after termination. For `halt`, log files are preserved.
## SubprocessTool
[Source](../src/tools/system/subprocess.ts)
The `subprocess` tool is registered in the agent's toolbox for modes: `Build`, `Test`, `Ship`, `Develop`. It provides a single function with a `cmd` parameter that dispatches to the appropriate operation.
### Tool Definition
**Name:** `subprocess`
**Category:** `system`
**Parameters:**
| Parameter | Type | Required | Description |
|---|---|---|---|
| `cmd` | string (enum) | yes | One of: `create`, `list`, `kill`, `killAll`, `halt`, `log` |
| `command` | string | for `create` | The executable to spawn (e.g., `node`, `npm`, `python`) |
| `args` | string[] | for `create` | Command-line arguments for the spawned process |
| `pid` | number | for `kill`, `halt`, `log` | The managed process ID |
| `which` | string (enum) | for `log` | `"stdout"` or `"stderr"` |
### Commands
#### `create`
Spawns a new child process in the project directory. Logs stdout and stderr to `<gadgetDir>/subprocess-logs/`.
Response:
```
SUBPROCESS CREATED
pid: 12345
stdout: /path/to/.gadget/subprocess-logs/subproc-12345.stdout.log
stderr: /path/to/.gadget/subprocess-logs/subproc-12345.stderr.log
```
#### `list`
Returns all currently managed subprocesses with their PIDs, project slugs, and timestamps.
Response:
```
SUBPROCESS LIST
pid: 12345 | project: my-project | created: ... | updated: ...
```
#### `kill`
Terminates a subprocess and removes its log files. Uses SIGINT with graceful escalation.
Response:
```
SUBPROCESS KILLED
pid: 12345
```
#### `killAll`
Terminates all managed subprocesses and removes all log files.
Response:
```
SUBPROCESS KILLALL
All managed subprocesses have been terminated and their logs removed.
```
#### `halt`
Terminates a subprocess but retains its log files. Useful for inspecting logs after process exit.
Response:
```
SUBPROCESS HALTED
pid: 12345
The process has been stopped. Log files are retained for inspection.
```
#### `log`
Reads stdout or stderr output from a subprocess's log file.
Response:
```
SUBPROCESS LOG (stdout)
pid: 12345
---
<log content>
```
## Integration Points
### System Prompts
All agent modes include a **Process Management** block in their system prompt. The block is loaded from [process-management-block.md](../../gadget-code/data/prompts/common/process-management-block.md) and rendered via `{{process_management_block}}` in the mode templates. This tells the agent about the `subprocess` tool and how to use it.
### Shutdown
`gadget-drone.ts` calls `SubProcessService.stop()` during shutdown, which triggers `killAll()` to clean up any remaining child processes.
## Tests
| File | What it covers |
|---|---|
| `src/services/subprocess.test.ts` | SubProcessService: create, ps, killPid, killAll, haltPid, getLog, error cases |
| `src/tools/system/subprocess.test.ts` | SubprocessTool: all 6 commands, parameter validation, service error handling |
---
## Phase 2: SubProcess Observability
### Architecture
Phase 2 exposes the drone's managed subprocesses to the frontend via a **callback-chain pattern** over Socket.IO, identical to the existing `fileTreeRequest`/`fileReadRequest` flow:
```
IDE (DroneManager.tsx)
──emit("requestProcessStats", registrationId, cb)──▶
CodeSession.onRequestProcessStats()
──droneSession.socket.emit("requestProcessStats", cb)──▶
GadgetDrone.onRequestProcessStats()
│ SubProcessService.ps()
│ .summarize() each
│ → SubProcessStat[]
◀──cb(true, { processes })──
◀──ack callback fires──
◀──cb(success, data)──
```
### Socket Protocol
| Direction | Event | Signature |
|-----------|-------|-----------|
| IDE → Backend | `requestProcessStats` | `(registrationId: string, cb: RequestProcessStatsCallback) => void` |
| Backend → Drone | `requestProcessStats` | `(cb: RequestProcessStatsCallback) => void` (no registrationId — drone identifies itself) |
**Callback type:**
```typescript
type RequestProcessStatsCallback = (
success: boolean,
data?: { processes?: SubProcessStat[]; message?: string }
) => void;
```
### `SubProcessStat` Type
Defined in `packages/api/src/messages/subprocess.ts`:
```typescript
interface SubProcessStat {
pid: number;
command: string;
args: string[];
projectId: GadgetId;
projectSlug: string;
projectName: string;
status: "running" | "stopped" | "error";
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
stdoutFileName: string;
stderrFileName: string;
}
```
### Handler Behavior
The `onRequestProcessStats` handler in `gadget-drone.ts`:
1. Calls `SubProcessService.ps()` to get all tracked subprocesses
2. For each, calls `summarize()` for serializable fields
3. Determines status: checks `sp.process.exitCode !== null || sp.process.killed`
4. Extracts `command` and `args` from `sp.process.spawnargs`
5. Returns `cb(true, { processes })` on success, `cb(false, { message })` on error
### Live Log Streaming (Subscribe/Broadcast)
In addition to request/response stats, the drone continuously emits `log` events through its `GadgetLogTransportSocket`. Phase 2 adds a **monitor broadcast** path:
1. **IDE sends** `subscribeDrone(registrationId)` → Backend adds IDE's socket to a monitor set
2. **IDE sends** `unsubscribeDrone(registrationId)` → Backend removes from monitor set
3. **Drone emits** `log``DroneSession.onLog()` forwards to chat session AND broadcasts `drone:log` to all monitoring sockets
4. **Frontend** receives `drone:log` events and renders via `LogRenderer`
The monitor tracking lives in `SocketService.droneMonitorIndex` — a `Map<registrationId, Set<socketId>>`. Cleanup happens automatically on socket disconnect.
### Polling Interval
- **DroneManager**: 1-second interval polling `requestProcessStats`
- **DroneInspector (dashboard)**: 1-second interval polling
- Resource gauges (CPU, Network, File I/O): 200ms internal animation (mocked data)
- All timers are killed on unmount or drone deselection