Add streaming log viewer with service switching and filtering (step 6)
ddev logs -f runs indefinitely rather than completing, so it doesn't
fit runStreamed's resolve/reject-on-exit model or the terminal panel's
operation semantics — added a parallel startLogStream/logs:data path
in commandRunner.ts that shares the same process-tracking map (so
stopping a log stream reuses the existing terminal:cancel IPC) but
pushes chunks over a dedicated channel decoupled from the status
bar/toast system. Kill all tracked processes on app quit so a
forgotten open log viewer doesn't leave an orphaned `ddev logs -f`.
Two real bugs found and fixed via live testing:
- react-hooks/set-state-in-effect flagged synchronous setState calls
used only to reset state on service change. Fixed by keying the
streaming component by service (LogPane key={service}) so switching
services remounts it and state resets via useState initializers
instead — the React-recommended pattern for this.
- Filtering operated on raw stream chunks, not lines: a single data
chunk can bundle many log lines or split one across chunk
boundaries, so filtering by chunk let unrelated lines through
whenever a match happened to share a chunk. Fixed by buffering
partial lines per stream and only filtering/rendering once complete
lines are assembled.
Verified end-to-end against the real scratch project via CDP-driven
clicks: live streaming from real containers (db and web service logs
both confirmed with distinct real content), service switching,
filtering (confirmed both the false-positive case is fixed and real
matches still work), and confirmed closing the viewer actually kills
the underlying `ddev logs -f` process rather than leaving it orphaned.
This commit is contained in:
@@ -6,6 +6,8 @@ import type {
|
||||
DdevProjectDetail,
|
||||
DdevProjectSummary,
|
||||
DdevSnapshot,
|
||||
LogDataEvent,
|
||||
LogExitEvent,
|
||||
TerminalDataEvent,
|
||||
TerminalExitEvent
|
||||
} from '../shared/types'
|
||||
@@ -63,6 +65,20 @@ const api = {
|
||||
ipcRenderer.invoke('addons:install', operationId, name, addonRepo),
|
||||
remove: (operationId: string, name: string, addonName: string): Promise<void> =>
|
||||
ipcRenderer.invoke('addons:remove', operationId, name, addonName)
|
||||
},
|
||||
logs: {
|
||||
start: (operationId: string, name: string, service: string): Promise<void> =>
|
||||
ipcRenderer.invoke('logs:start', operationId, name, service),
|
||||
onData: (callback: (event: LogDataEvent) => void): (() => void) => {
|
||||
const listener = (_event: IpcRendererEvent, data: LogDataEvent): void => callback(data)
|
||||
ipcRenderer.on('logs:data', listener)
|
||||
return () => ipcRenderer.removeListener('logs:data', listener)
|
||||
},
|
||||
onExit: (callback: (event: LogExitEvent) => void): (() => void) => {
|
||||
const listener = (_event: IpcRendererEvent, data: LogExitEvent): void => callback(data)
|
||||
ipcRenderer.on('logs:exit', listener)
|
||||
return () => ipcRenderer.removeListener('logs:exit', listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user