Skip to content

Commit cc0b35e

Browse files
committed
feat: in UI allow pty execution to be canceled
Signed-off-by: Nick Mitchell <[email protected]>
1 parent 3374d92 commit cc0b35e

File tree

3 files changed

+68
-6
lines changed

3 files changed

+68
-6
lines changed

pdl-live-react/src/view/masonry/MasonryCombo.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import MasonryTileWrapper from "./MasonryTileWrapper"
2828
import Toolbar, { type SML } from "./Toolbar"
2929

3030
import computeModel from "./model"
31+
import ConditionVariable from "./condvar"
3132
import {
3233
hasContextInformation,
3334
hasTimingInformation,
@@ -83,11 +84,17 @@ export default function MasonryCombo({ value, setValue }: Props) {
8384
cmd: string
8485
args?: string[]
8586
onExit?: (exitCode: number) => void
87+
cancelCondVar?: ConditionVariable
8688
}>(null)
89+
const cancelModal = useCallback(
90+
() => modalContent?.cancelCondVar?.signal(),
91+
[modalContent?.cancelCondVar],
92+
)
8793
const closeModal = useCallback(() => {
94+
modalContent?.cancelCondVar?.signal()
8895
setModalContent(null)
8996
setModalIsDone(-1)
90-
}, [setModalContent, setModalIsDone])
97+
}, [modalContent?.cancelCondVar, setModalContent, setModalIsDone])
9198
const onExit = useCallback(
9299
(exitCode: number) => {
93100
setModalIsDone(exitCode)
@@ -97,6 +104,7 @@ export default function MasonryCombo({ value, setValue }: Props) {
97104
},
98105
[setModalIsDone, modalContent],
99106
)
107+
useEffect(() => setModalIsDone(-2), [modalContent])
100108

101109
// special form of setModalContent for running a PDL program
102110
const run = useCallback<Runner>(
@@ -132,6 +140,7 @@ export default function MasonryCombo({ value, setValue }: Props) {
132140
...(!data ? [] : ["--data", JSON.stringify(data)]),
133141
input,
134142
],
143+
cancelCondVar: new ConditionVariable(),
135144
onExit: async () => {
136145
onExit()
137146
try {
@@ -206,7 +215,7 @@ export default function MasonryCombo({ value, setValue }: Props) {
206215
<ModalHeader
207216
title={modalContent?.header}
208217
titleIconVariant={
209-
modalIsDone === -1
218+
modalIsDone < 0
210219
? RunningIcon
211220
: modalIsDone === 0
212221
? "success"
@@ -218,6 +227,7 @@ export default function MasonryCombo({ value, setValue }: Props) {
218227
<RunTerminal
219228
cmd={modalContent?.cmd ?? ""}
220229
args={modalContent?.args}
230+
cancel={modalContent?.cancelCondVar}
221231
onExit={onExit}
222232
/>
223233
</Suspense>
@@ -228,10 +238,18 @@ export default function MasonryCombo({ value, setValue }: Props) {
228238
key="Close"
229239
variant={modalIsDone > 0 ? "danger" : "primary"}
230240
onClick={closeModal}
231-
isDisabled={modalIsDone === -1}
241+
isDisabled={modalIsDone < 0}
232242
>
233243
Close
234244
</Button>
245+
<Button
246+
key="Cancel"
247+
variant="secondary"
248+
onClick={cancelModal}
249+
isDisabled={modalIsDone !== -2}
250+
>
251+
Cancel
252+
</Button>
235253
</ModalFooter>
236254
</Modal>
237255
</>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export default class ConditionVariable {
2+
private condition: boolean = false
3+
private waitingPromises: {
4+
resolve: () => void
5+
reject: (reason?: unknown) => void
6+
}[] = []
7+
8+
public async wait(): Promise<void> {
9+
if (this.condition) {
10+
return Promise.resolve()
11+
}
12+
13+
return new Promise((resolve, reject) => {
14+
this.waitingPromises.push({ resolve, reject })
15+
})
16+
}
17+
18+
public signal(): void {
19+
if (this.waitingPromises.length > 0) {
20+
const { resolve } = this.waitingPromises.shift()!
21+
resolve()
22+
this.condition = true
23+
}
24+
}
25+
26+
public reset(): void {
27+
this.condition = false
28+
}
29+
}

pdl-live-react/src/view/term/RunTerminal.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,25 @@ import { ClipboardAddon } from "@xterm/addon-clipboard"
88
import "./RunTerminal.css"
99

1010
type Props = {
11+
/** The cmd part of `cmd ...args` */
1112
cmd: string
13+
14+
/** The args part of `cmd ...args */
1215
args?: string[]
16+
17+
/** A callback provided by caller to be invoked upon pty completion */
1318
onExit?: (exitCode: number) => void
19+
20+
/** A condition variable used by caller to request pty cancellation */
21+
cancel?: import("../masonry/condvar").default
1422
}
1523

16-
export default function RunTerminal({ cmd, args = [], onExit }: Props) {
24+
export default function RunTerminal({ cmd, args = [], onExit, cancel }: Props) {
1725
const ref = createRef<HTMLDivElement>()
1826
const [term, setTerm] = useState<null | Terminal>(null)
1927
const [exitCode, setExitCode] = useState(-1)
2028

29+
/** Schema adapter from our props.onExit to that of tauri-pty */
2130
const onExit2 = useCallback(
2231
({ exitCode }: { exitCode: number }) => {
2332
setExitCode(exitCode)
@@ -28,7 +37,8 @@ export default function RunTerminal({ cmd, args = [], onExit }: Props) {
2837
[onExit, setExitCode],
2938
)
3039

31-
useEffect(() => setExitCode(-1), [cmd, args, onExit])
40+
/** Re-initialization of exit code if props change */
41+
useEffect(() => setExitCode(-1), [cmd, args, onExit, cancel])
3242

3343
// Why a two-stage useEffect? Otherwise: cannot read properties of
3444
// undefined (reading 'dimensions')
@@ -64,6 +74,11 @@ export default function RunTerminal({ cmd, args = [], onExit }: Props) {
6474
rows: term.rows,
6575
})
6676

77+
/** Respond to cancellation request by killing the pty */
78+
cancel?.wait().then(() => {
79+
pty.kill()
80+
})
81+
6782
pty.onData((data) => term.write(data))
6883
term.onData((data) => pty.write(data))
6984

@@ -78,7 +93,7 @@ export default function RunTerminal({ cmd, args = [], onExit }: Props) {
7893
}
7994
}
8095
}
81-
}, [term, ref, exitCode, args, cmd, onExit2])
96+
}, [term, ref, exitCode, cmd, args, cancel, onExit2])
8297

8398
return (
8499
<div className="pdl-run-terminal" ref={ref} style={{ height: "600px" }} />

0 commit comments

Comments
 (0)