Skip to content

Commit e3537b5

Browse files
committed
fix: codeflare terminal should restart pty on exit
1 parent 5695aaa commit e3537b5

File tree

2 files changed

+131
-53
lines changed

2 files changed

+131
-53
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2022 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import React from "react"
18+
import { PassThrough } from "stream"
19+
import { Loading } from "@kui-shell/plugin-client-common"
20+
import { Arguments, Job, isResizable } from "@kui-shell/core"
21+
22+
import Terminal from "./Terminal"
23+
24+
/**
25+
* This is our impl of the `watch` property that our Terminal
26+
* component needs, in order to support live updates.
27+
*/
28+
function watch(stream: PassThrough, job: Job) {
29+
return {
30+
on: stream.on.bind(stream), // data from pty to terminal
31+
onInput: job.write.bind(job), // user input from terminal to pty
32+
unwatch: job.abort.bind(job), // unmount, abort pty job
33+
onResize: isResizable(job) ? job.resize.bind(job) : undefined,
34+
}
35+
}
36+
37+
type Props = {
38+
cmdline: string
39+
env: Record<string, string>
40+
tab: Arguments["tab"]
41+
repl: Arguments["REPL"]
42+
}
43+
44+
type State = {
45+
startCount?: number
46+
job?: Job
47+
watch?: () => ReturnType<typeof watch>
48+
catastrophicError?: unknown
49+
}
50+
51+
/**
52+
* A wrapper around <Terminal/> that handles pty exits. It restarts
53+
* tne pty and recreates the terminal if this happens.
54+
*/
55+
export default class RestartableTerminal extends React.PureComponent<Props, State> {
56+
private async initPty() {
57+
try {
58+
// we need this to wire the pty output through to the Terminal
59+
// component, which expects something stream-like
60+
const passthrough = new PassThrough()
61+
62+
await this.props.repl.qexec(this.props.cmdline, undefined, undefined, {
63+
tab: this.props.tab,
64+
env: this.props.env,
65+
quiet: true, // strange i know, but this forces PTY execution
66+
onExit: () => {
67+
// restart
68+
this.initPty()
69+
},
70+
onInit: () => (_) => {
71+
// hooks pty output to our passthrough stream
72+
passthrough.write(_)
73+
},
74+
onReady: (job) => {
75+
this.setState((curState) => ({
76+
job,
77+
watch: () => watch(passthrough, job),
78+
startCount: !curState || curState.startCount === undefined ? 0 : curState.startCount + 1,
79+
}))
80+
},
81+
})
82+
} catch (err) {
83+
console.error("Error in RestartableTerminal", err)
84+
// this.setState({ catastrophicError: err })
85+
}
86+
}
87+
88+
public componentDidMount() {
89+
this.initPty()
90+
}
91+
92+
public componentWillUnmount() {
93+
if (this.state.job) {
94+
this.state.job.abort()
95+
}
96+
}
97+
98+
public render() {
99+
if (!this.state) {
100+
return <Loading />
101+
}
102+
103+
const { watch, catastrophicError } = this.state
104+
105+
if (catastrophicError) {
106+
return "Internal Error"
107+
} else if (!watch) {
108+
return <Loading />
109+
} else {
110+
// force a new Terminal every restart
111+
const key = !this.state || !this.state.startCount ? 0 : this.state.startCount
112+
113+
return (
114+
<div
115+
className="kui--inverted-color-context flex-fill flex-layout flex-align-stretch"
116+
style={{ backgroundColor: "var(--color-sidecar-background-02)" }}
117+
>
118+
<Terminal watch={watch} key={key} />
119+
</div>
120+
)
121+
}
122+
}
123+
}

plugins/plugin-codeflare/src/controller/terminal.tsx

Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -14,67 +14,22 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Arguments, Job, ReactResponse, encodeComponent, isResizable } from "@kui-shell/core"
18-
1917
import React from "react"
20-
import { PassThrough } from "stream"
18+
import { Arguments, encodeComponent } from "@kui-shell/core"
2119

2220
import respawn from "./respawn"
23-
import Terminal from "../components/Terminal"
24-
25-
/**
26-
* This is our impl of the `watch` property that our Terminal
27-
* component needs, in order to support live updates.
28-
*/
29-
function watch(stream: PassThrough, job: Job) {
30-
return {
31-
on: stream.on.bind(stream), // data from pty to terminal
32-
onInput: job.write.bind(job), // user input from terminal to pty
33-
unwatch: job.abort.bind(job), // unmount, abort pty job
34-
onResize: isResizable(job) ? job.resize.bind(job) : undefined,
35-
}
36-
}
21+
import Terminal from "../components/RestartableTerminal"
3722

3823
/**
3924
* This is a command handler that opens up a terminal. The expectation
4025
* is that the command line to be executed is the "rest" after:
4126
* `codeflare terminal <rest...>`.
4227
*/
4328
export default function openTerminal(args: Arguments) {
44-
// eslint-disable-next-line no-async-promise-executor
45-
return new Promise<ReactResponse>(async (resolve, reject) => {
46-
try {
47-
// we need this to wire the pty output through to the Terminal
48-
// component, which expects something stream-like
49-
const passthrough = new PassThrough()
50-
51-
// respawn, meaning launch it with codeflare
52-
const { argv, env } = respawn(args.argv.slice(2))
53-
const cmdline = argv.map((_) => encodeComponent(_)).join(" ")
54-
55-
await args.REPL.qexec(cmdline, undefined, undefined, {
56-
tab: args.tab,
57-
env,
58-
quiet: true,
59-
onInit: () => (_) => {
60-
// hooks pty output to our passthrough stream
61-
passthrough.write(_)
62-
},
63-
onReady: (job) => {
64-
resolve({
65-
react: (
66-
<div
67-
className="kui--inverted-color-context flex-fill flex-layout flex-align-stretch"
68-
style={{ backgroundColor: "var(--color-sidecar-background-02)" }}
69-
>
70-
<Terminal watch={() => watch(passthrough, job)} />
71-
</div>
72-
),
73-
})
74-
},
75-
})
76-
} catch (err) {
77-
reject(err)
78-
}
79-
})
29+
// respawn, meaning launch it with codeflare
30+
const { argv, env } = respawn(args.argv.slice(2))
31+
const cmdline = argv.map((_) => encodeComponent(_)).join(" ")
32+
return {
33+
react: <Terminal cmdline={cmdline} env={env} repl={args.REPL} tab={args.tab} />,
34+
}
8035
}

0 commit comments

Comments
 (0)