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:
@@ -97,3 +97,39 @@ export function cancelCommand(operationId: string): boolean {
|
||||
child.kill()
|
||||
return true
|
||||
}
|
||||
|
||||
// Log tailing (`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 (no "success"/"failure" toast makes sense for
|
||||
// an open-ended stream). It shares the same running/cancel bookkeeping —
|
||||
// stopping a log stream reuses cancelCommand — but pushes chunks over a
|
||||
// dedicated logs:data channel instead of terminal:data.
|
||||
export function startLogStream(operationId: string, args: string[], sender: WebContents): void {
|
||||
const child = spawn('ddev', args, { env: ENV_WITH_DDEV_PATH })
|
||||
running.set(operationId, child)
|
||||
|
||||
const forward = (stream: 'stdout' | 'stderr') => (data: Buffer) => {
|
||||
if (!sender.isDestroyed()) {
|
||||
sender.send('logs:data', { operationId, stream, chunk: data.toString() })
|
||||
}
|
||||
}
|
||||
child.stdout.on('data', forward('stdout'))
|
||||
child.stderr.on('data', forward('stderr'))
|
||||
|
||||
const finish = (): void => {
|
||||
running.delete(operationId)
|
||||
cancelledIds.delete(operationId)
|
||||
if (!sender.isDestroyed()) {
|
||||
sender.send('logs:exit', { operationId })
|
||||
}
|
||||
}
|
||||
child.on('close', finish)
|
||||
child.on('error', finish)
|
||||
}
|
||||
|
||||
export function killAllRunningCommands(): void {
|
||||
for (const child of running.values()) {
|
||||
child.kill()
|
||||
}
|
||||
running.clear()
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { registerProjectsIpc } from './ipc/projects'
|
||||
import { registerTerminalIpc } from './ipc/terminal'
|
||||
import { registerDatabaseIpc } from './ipc/database'
|
||||
import { registerAddonsIpc } from './ipc/addons'
|
||||
import { registerLogsIpc } from './ipc/logs'
|
||||
import { killAllRunningCommands } from './commandRunner'
|
||||
|
||||
function createWindow(): void {
|
||||
// Create the browser window.
|
||||
@@ -58,6 +60,7 @@ app.whenReady().then(() => {
|
||||
registerTerminalIpc()
|
||||
registerDatabaseIpc()
|
||||
registerAddonsIpc()
|
||||
registerLogsIpc()
|
||||
|
||||
createWindow()
|
||||
|
||||
@@ -77,5 +80,11 @@ app.on('window-all-closed', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Kill any still-running ddev processes (e.g. an open `ddev logs -f` stream)
|
||||
// so they don't linger as orphans after the app exits.
|
||||
app.on('before-quit', () => {
|
||||
killAllRunningCommands()
|
||||
})
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { startLogStream } from '../commandRunner'
|
||||
|
||||
export function registerLogsIpc(): void {
|
||||
ipcMain.handle('logs:start', (event, operationId: string, name: string, service: string) =>
|
||||
startLogStream(operationId, ['logs', name, '-f', '-s', service, '--tail', '200'], event.sender)
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user