Skip to content

Commit 2d94bb5

Browse files
committed
stop debugger when navigating away
1 parent 36b41da commit 2d94bb5

File tree

12 files changed

+208
-25
lines changed

12 files changed

+208
-25
lines changed

src/handlers/script/index.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,18 @@ export function initialize() {
8383
})
8484
})
8585

86-
ipcMain.on(ScriptHandler.Stop, () => {
86+
ipcMain.handle(ScriptHandler.Stop, async () => {
8787
console.info(`${ScriptHandler.Stop} event received`)
88-
if (currentTestRun) {
89-
currentTestRun.stop().catch((error) => {
90-
log.error('Failed to stop the test run', error)
91-
})
9288

89+
if (!currentTestRun) {
90+
return
91+
}
92+
93+
try {
94+
await currentTestRun.stop()
95+
} catch (error) {
96+
log.error('Failed to stop the test run', error)
97+
} finally {
9398
currentTestRun = null
9499
}
95100
})

src/handlers/script/preload.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function runScript(scriptPath: string) {
3939
}
4040

4141
export function stopScript() {
42-
ipcRenderer.send(ScriptHandler.Stop)
42+
return ipcRenderer.invoke(ScriptHandler.Stop) as Promise<void>
4343
}
4444

4545
export function onScriptLog(callback: (data: LogEntry) => void) {

src/hooks/useViewBlocker.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
2+
import { useBlocker } from 'react-router-dom'
3+
4+
/**
5+
* This hook is similar to react-router's useBlocker, but it also handles the case where the app is closing.
6+
* It returns a boolean indicating if the view is blocked, and two functions to cancel or confirm the blocking.
7+
*/
8+
export function useViewBlocker(block: boolean) {
9+
const [isAppClosing, setIsAppClosing] = useState(false)
10+
11+
const isBlockingRef = useRef(block)
12+
const isConfirmedRef = useRef(false)
13+
14+
const blocker = useBlocker(block)
15+
16+
useEffect(() => {
17+
isBlockingRef.current = block
18+
}, [block])
19+
20+
// After confirm(), `isConfirmedRef` prevents double submission until the block condition
21+
// clears (e.g. debugging stopped). Reset so a later blocking session can confirm again.
22+
useEffect(() => {
23+
if (!block) {
24+
isConfirmedRef.current = false
25+
}
26+
}, [block])
27+
28+
useEffect(() => {
29+
return window.studio.app.onApplicationClose(() => {
30+
if (isBlockingRef.current) {
31+
setIsAppClosing(true)
32+
33+
return
34+
}
35+
36+
window.studio.app.closeApplication()
37+
})
38+
}, [])
39+
40+
const cancel = useCallback(() => {
41+
setIsAppClosing(false)
42+
43+
blocker.reset?.()
44+
}, [blocker.reset])
45+
46+
const confirm = useCallback(() => {
47+
if (isConfirmedRef.current) {
48+
return
49+
}
50+
51+
isConfirmedRef.current = true
52+
53+
if (isAppClosing) {
54+
window.studio.app.closeApplication()
55+
56+
return
57+
}
58+
59+
blocker.proceed?.()
60+
}, [blocker.proceed, isAppClosing])
61+
62+
const blocked = blocker.state === 'blocked' || isAppClosing
63+
64+
return useMemo(() => {
65+
return {
66+
blocked,
67+
cancel,
68+
confirm,
69+
}
70+
}, [blocked, cancel, confirm])
71+
}

src/main/script.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,14 @@ export const runScript = async ({
9696
HTTP_PROXY: `http://localhost:${proxySettings.port}`,
9797
HTTPS_PROXY: `http://localhost:${proxySettings.port}`,
9898
NO_PROXY: 'jslib.k6.io',
99-
K6_TRACKING_SERVER_PORT: String(trackingServer?.port),
99+
K6_TRACKING_SERVER_PORT: String(trackingServer.port),
100100
K6_BROWSER_ARGS: proxyArgs.join(','),
101101
K6_TESTING_COLORIZE: 'false',
102102
},
103103
})
104104

105+
testRun.addDisposable(trackingServer)
106+
105107
testRun.on('log', ({ entry }) => {
106108
browserWindow.webContents.send(ScriptHandler.Log, entry)
107109
})
@@ -123,8 +125,6 @@ export const runScript = async ({
123125

124126
testRun.on('stop', () => {
125127
browserWindow.webContents.send(ScriptHandler.Stopped)
126-
127-
trackingServer?.dispose()
128128
})
129129

130130
return testRun

src/utils/k6/testRun.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export class TestRun extends EventEmitter<TestRunEventMap> {
127127
#process: ChildProcessWithoutNullStreams
128128

129129
#checks: Check[] = []
130+
#disposables: Array<AsyncDisposable | Disposable> = []
130131

131132
constructor(process: ChildProcessWithoutNullStreams) {
132133
super()
@@ -176,16 +177,33 @@ export class TestRun extends EventEmitter<TestRunEventMap> {
176177
return this.#process.pid != undefined && this.#process.exitCode === null
177178
}
178179

179-
stop(): Promise<void> {
180+
async stop(): Promise<void> {
181+
await Promise.all([
182+
this.#kill(),
183+
...this.#disposables.map((disposable) => {
184+
if (Symbol.asyncDispose in disposable) {
185+
return disposable[Symbol.asyncDispose]()
186+
}
187+
188+
return disposable[Symbol.dispose]()
189+
}),
190+
])
191+
}
192+
193+
#kill() {
180194
if (!this.isRunning()) {
181195
return Promise.resolve()
182196
}
183197

184-
return new Promise((resolve) => {
185-
this.#process.once('close', resolve)
198+
const { promise, resolve } = Promise.withResolvers<void>()
186199

187-
this.#process.kill()
200+
this.#process.once('close', () => {
201+
resolve()
188202
})
203+
204+
this.#process.kill()
205+
206+
return promise
189207
}
190208

191209
#handleStart = () => {
@@ -237,4 +255,8 @@ export class TestRun extends EventEmitter<TestRunEventMap> {
237255
#emitStop = () => {
238256
this.emit('stop', undefined)
239257
}
258+
259+
addDisposable(disposable: AsyncDisposable | Disposable) {
260+
this.#disposables.push(disposable)
261+
}
240262
}

src/utils/k6/tracking.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ interface ReportingServerEventMap {
3838
}
3939
}
4040

41-
class TestRunTrackingServer extends EventEmitter<ReportingServerEventMap> {
41+
export class TestRunTrackingServer
42+
extends EventEmitter<ReportingServerEventMap>
43+
implements AsyncDisposable
44+
{
4245
#server: Server
4346

4447
get port() {
@@ -55,8 +58,15 @@ class TestRunTrackingServer extends EventEmitter<ReportingServerEventMap> {
5558
})
5659
}
5760

58-
dispose() {
59-
this.#server.close()
61+
[Symbol.asyncDispose](): Promise<void> {
62+
return new Promise((resolve) => {
63+
this.#server.close((err) => {
64+
if (err) {
65+
log.warn('Tracking server close:', err)
66+
}
67+
resolve()
68+
})
69+
})
6070
}
6171
}
6272

src/utils/validateScript.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export async function validateScript(
2828
if (signal) {
2929
signal.addEventListener('abort', () => {
3030
cleanup()
31-
window.studio.script.stopScript()
31+
void window.studio.script.stopScript()
3232
reject(new DOMException('Aborted', 'AbortError'))
3333
})
3434
}

src/views/BrowserTestEditor/BrowserTestEditor.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { css } from '@emotion/react'
22
import { Flex, Tabs } from '@radix-ui/themes'
3+
import { useEffect } from 'react'
34
import { useNavigate } from 'react-router-dom'
45

56
import { FileNameHeader } from '@/components/FileNameHeader'
67
import { View } from '@/components/Layout/View'
78
import { ReadOnlyEditor } from '@/components/Monaco/ReadOnlyEditor'
89
import { LogsSection } from '@/components/Validator/LogsSection'
910
import { Group, Panel, Separator } from '@/components/primitives/ResizablePanel'
11+
import { useViewBlocker } from '@/hooks/useViewBlocker'
1012
import { routeMap } from '@/routeMap'
1113
import { BrowserTestFile } from '@/schemas/browserTest/v1'
1214
import { StudioFile } from '@/types'
@@ -39,12 +41,25 @@ function BrowserTestEditorView({ file, data }: BrowserTestEditorViewProps) {
3941
const test = useBrowserTestState(data)
4042

4143
const preview = useBrowserScriptPreview(test.actions)
42-
const { session, startDebugging } = useDebugSession({
44+
45+
const { session, startDebugging, stopDebugging } = useDebugSession({
4346
type: 'raw',
4447
content: preview,
4548
name: file.fileName,
4649
})
4750

51+
const blocker = useViewBlocker(session.state === 'running')
52+
53+
useEffect(() => {
54+
if (!blocker.blocked) {
55+
return
56+
}
57+
58+
void stopDebugging().finally(() => {
59+
blocker.confirm()
60+
})
61+
}, [blocker, stopDebugging])
62+
4863
const handleSave = () => {
4964
if (!test.isDirty || !data) {
5065
return

src/views/Generator/ValidatorDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function ValidatorDialog({
3737
const handleOpenChange = useCallback(
3838
(open: boolean) => {
3939
if (!open) {
40-
window.studio.script.stopScript()
40+
void window.studio.script.stopScript()
4141
setIsRunning(false)
4242
resetState()
4343
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Box, Button, Dialog, Flex, Text } from '@radix-ui/themes'
2+
3+
interface AbortDialogProps {
4+
open: boolean
5+
onCancel: () => void
6+
onAbort: () => void
7+
}
8+
9+
export function AbortDialog({ open, onCancel, onAbort }: AbortDialogProps) {
10+
return (
11+
<Dialog.Root
12+
open={open}
13+
onOpenChange={(nextOpen) => {
14+
if (!nextOpen) {
15+
onCancel()
16+
}
17+
}}
18+
>
19+
<Dialog.Content size="3">
20+
<Flex direction="column" minHeight="150px">
21+
<Dialog.Title>Debugging</Dialog.Title>
22+
<Box flexGrow="1" flexShrink="1" flexBasis="0">
23+
<Text>
24+
Leaving this view will abort the current debugging session.
25+
</Text>
26+
</Box>
27+
<Flex justify="end" gap="3">
28+
<Dialog.Close>
29+
<Button variant="outline" color="orange">
30+
Cancel
31+
</Button>
32+
</Dialog.Close>
33+
<Button color="red" onClick={onAbort}>
34+
Abort
35+
</Button>
36+
</Flex>
37+
</Flex>
38+
</Dialog.Content>
39+
</Dialog.Root>
40+
)
41+
}

0 commit comments

Comments
 (0)