Skip to content

Commit a58ad7a

Browse files
committed
Use OpenShift Terminal for Serverless
- Also fix a race condition that caused the terminal spawn to fail Signed-off-by: David Thompson <davthomp@redhat.com>
1 parent 84247da commit a58ad7a

File tree

4 files changed

+70
-151
lines changed

4 files changed

+70
-151
lines changed

.vscode/tasks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"base": "$tsc-watch",
1818
},
1919
"presentation": {
20-
"reveal": "silent",
20+
"reveal": "never",
2121
},
2222
},
2323
{

src/cli.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export interface CliExitData {
2222
export interface Cli {
2323
execute(cmd: string, opts?: cp.ExecOptions): Promise<CliExitData>;
2424
executeTool(command: CommandText, opts?: cp.ExecOptions): Promise<CliExitData>;
25-
spawn(cmd: string, params: string[], opts: cp.SpawnOptions): cp.ChildProcess;
2625
spawnTool(cmd: CommandText, opts: cp.SpawnOptions): Promise<cp.ChildProcess>;
2726
executeInTerminal(cmd: CommandText, cwd?: string, terminalName?: string, addEnv?: {[key : string]: string}): void;
2827
}
@@ -157,10 +156,6 @@ export class CliChannel implements Cli {
157156
await OpenShiftTerminalManager.getInstance().createTerminal(command, name, cwd, merged);
158157
}
159158

160-
spawn(cmd: string, params: string[], opts: cp.SpawnOptions = {cwd: undefined, env: process.env}): cp.ChildProcess {
161-
return cp.spawn(cmd, params, opts);
162-
}
163-
164159
async spawnTool(cmd: CommandText, opts: cp.SpawnOptions = {cwd: undefined, env: process.env}): Promise<cp.ChildProcess> {
165160
const toolLocation = await ToolsConfig.detect(cmd.command);
166161
const optWithTelemetryEnv = CliChannel.applyEnv(opts, CliChannel.createTelemetryEnv());

src/serveressFunction/build-run-deploy.ts

Lines changed: 61 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,20 @@
33
* Licensed under the MIT License. See LICENSE file in the project root for license information.
44
*-----------------------------------------------------------------------------------------------*/
55

6-
import { EventEmitter, Terminal, Uri, commands, window } from 'vscode';
7-
import { ClusterVersion, FunctionObject } from './types';
8-
import { ServerlessCommand, Utils } from './commands';
9-
import { Platform } from '../util/platform';
6+
import { commands, Uri, window } from 'vscode';
107
import { OdoImpl } from '../odo';
11-
import { CliChannel } from '../cli';
12-
import { ChildProcess, SpawnOptions } from 'child_process';
8+
import { Platform } from '../util/platform';
9+
import { OpenShiftTerminalApi, OpenShiftTerminalManager } from '../webview/openshift-terminal/openShiftTerminal';
10+
import { ServerlessCommand, Utils } from './commands';
11+
import { ClusterVersion, FunctionObject } from './types';
1312
import { ServerlessFunctionView } from './view';
1413

1514
export class BuildAndDeploy {
1615

1716
private static instance: BuildAndDeploy;
1817

19-
private buildTerminalMap: Map<string, Terminal> = new Map<string, Terminal>();
20-
public runTerminalMap: Map<string, Terminal> = new Map<string, Terminal>();
21-
private buildEmiterMap: Map<string, EventEmitter<string>> = new Map<string, EventEmitter<string>>();
22-
private buildPrcessMap: Map<Terminal, ChildProcess> = new Map<Terminal, ChildProcess>();
18+
private buildTerminalMap = new Map<string, OpenShiftTerminalApi>();
19+
public runTerminalMap = new Map<string, OpenShiftTerminalApi>();
2320

2421
public static imageRegex = RegExp('[^/]+\\.[^/.]+\\/([^/.]+)(?:\\/[\\w\\s._-]*([\\w\\s._-]))*(?::[a-z0-9\\.-]+)?$');
2522

@@ -46,38 +43,33 @@ export class BuildAndDeploy {
4643
}
4744
}
4845

46+
private pollForBuildTerminalDead(resolve: () => void, functionUri: Uri, timeout: number) {
47+
return () => {
48+
if (!this.buildTerminalMap.get(`build-${functionUri.fsPath}`)) {
49+
resolve();
50+
} else {
51+
setTimeout(this.pollForBuildTerminalDead(resolve, functionUri, timeout * 2), timeout * 2);
52+
}
53+
}
54+
}
55+
4956
public async buildFunction(functionName: string, functionUri: Uri): Promise<void> {
50-
const exisitingTerminal = this.buildTerminalMap.get(`build-${functionUri.fsPath}`);
51-
const outputEmitter = this.buildEmiterMap.get(`build-${functionUri.fsPath}`);
52-
if (exisitingTerminal) {
53-
let exisitingProcess = this.buildPrcessMap.get(exisitingTerminal);
57+
const existingTerminal = this.buildTerminalMap.get(`build-${functionUri.fsPath}`);
58+
59+
if (existingTerminal) {
5460
void window.showWarningMessage(`Do you want to restart ${functionName} build ?`, 'Yes', 'No').then(async (value: string) => {
5561
if (value === 'Yes') {
56-
exisitingTerminal.show(true);
57-
await commands.executeCommand('workbench.action.terminal.clear')
58-
outputEmitter.fire(`Start Building ${functionName} \r\n`);
59-
exisitingProcess.kill('SIGINT')
60-
this.buildPrcessMap.delete(exisitingTerminal);
61-
const clusterVersion: ClusterVersion | null = await this.checkOpenShiftCluster();
62-
const buildImage = await this.getImage(functionUri);
63-
const opt: SpawnOptions = { cwd: functionUri.fsPath };
64-
void CliChannel.getInstance().spawnTool(ServerlessCommand.buildFunction(functionUri.fsPath, buildImage, clusterVersion), opt).then((cp) => {
65-
exisitingProcess = cp;
66-
this.buildPrcessMap.set(exisitingTerminal, cp);
67-
exisitingProcess.on('error', (err) => {
68-
void window.showErrorMessage(err.message);
69-
});
70-
exisitingProcess.stdout.on('data', (chunk) => {
71-
outputEmitter.fire(`${chunk as string}`.replaceAll('\n', '\r\n'));
72-
});
73-
exisitingProcess.stderr.on('data', (errChunk) => {
74-
outputEmitter.fire(`\x1b[31m${errChunk as string}\x1b[0m`.replaceAll('\n', '\r\n'));
75-
void window.showErrorMessage(`${errChunk as string}`);
76-
});
77-
exisitingProcess.on('exit', () => {
78-
outputEmitter.fire('\r\nPress any key to close this terminal\r\n');
79-
});
62+
existingTerminal.focusTerminal();
63+
existingTerminal.sendText('\u0003');
64+
65+
// wait for old build to exit using polling with a back off
66+
await new Promise<void>((resolve) => {
67+
const INIT_TIMEOUT = 100;
68+
setTimeout(this.pollForBuildTerminalDead(resolve, functionUri, INIT_TIMEOUT), INIT_TIMEOUT);
8069
});
70+
71+
// start new build
72+
await this.buildProcess(functionUri, functionName);
8173
}
8274
});
8375
} else {
@@ -88,115 +80,48 @@ export class BuildAndDeploy {
8880
private async buildProcess(functionUri: Uri, functionName: string) {
8981
const clusterVersion: ClusterVersion | null = await this.checkOpenShiftCluster();
9082
const buildImage = await this.getImage(functionUri);
91-
const outputEmitter = new EventEmitter<string>();
92-
let devProcess: ChildProcess;
93-
let terminal = window.createTerminal({
94-
name: `Build ${functionName}`,
95-
pty: {
96-
onDidWrite: outputEmitter.event,
97-
open: () => {
98-
outputEmitter.fire(`Start Building ${functionName} \r\n`);
99-
const opt: SpawnOptions = { cwd: functionUri.fsPath };
100-
void CliChannel.getInstance().spawnTool(ServerlessCommand.buildFunction(functionUri.fsPath, buildImage, clusterVersion), opt).then((cp) => {
101-
this.buildPrcessMap.set(terminal, cp);
102-
devProcess = cp;
103-
devProcess.on('spawn', () => {
104-
terminal.show();
105-
});
106-
devProcess.on('error', (err) => {
107-
void window.showErrorMessage(err.message);
108-
});
109-
devProcess.stdout.on('data', (chunk) => {
110-
outputEmitter.fire(`${chunk as string}`.replaceAll('\n', '\r\n'));
111-
});
112-
devProcess.stderr.on('data', (errChunk) => {
113-
outputEmitter.fire(`\x1b[31m${errChunk as string}\x1b[0m`.replaceAll('\n', '\r\n'));
114-
});
115-
devProcess.on('exit', () => {
116-
outputEmitter.fire('\r\nPress any key to close this terminal\r\n');
117-
});
118-
});
119-
},
120-
close: () => {
121-
if (devProcess && devProcess.exitCode === null) { // if process is still running and user closed terminal
122-
devProcess.kill('SIGINT');
123-
}
124-
this.buildTerminalMap.delete(`build-${functionUri.fsPath}`);
125-
this.buildEmiterMap.delete(`build-${functionUri.fsPath}`);
126-
this.buildPrcessMap.delete(terminal);
127-
terminal = undefined;
128-
},
129-
handleInput: ((data: string) => {
130-
if (!devProcess) { // if any key pressed after process ends
131-
terminal.dispose();
132-
} else { // ctrl+C processed only once when there is no cleaning process
133-
outputEmitter.fire('^C\r\n');
134-
devProcess.kill('SIGINT');
135-
terminal.dispose();
136-
}
137-
})
138-
},
139-
});
140-
this.buildTerminalMap.set(`build-${functionUri.fsPath}`, terminal);
141-
this.buildEmiterMap.set(`build-${functionUri.fsPath}`, outputEmitter);
83+
const terminalKey = `build-${functionUri.fsPath}`;
84+
85+
const terminal = await OpenShiftTerminalManager.getInstance().createTerminal(
86+
ServerlessCommand.buildFunction(functionUri.fsPath, buildImage, clusterVersion),
87+
`Build ${functionName}`,
88+
functionUri.fsPath,
89+
process.env,
90+
true,
91+
{
92+
onExit: () => {
93+
this.buildTerminalMap.delete(terminalKey)
94+
}
95+
}
96+
);
97+
98+
this.buildTerminalMap.set(terminalKey, terminal);
14299
}
143100

144-
public runFunction(context: FunctionObject, runBuild = false) {
145-
const outputEmitter = new EventEmitter<string>();
146-
let runProcess: ChildProcess;
147-
let terminal = window.createTerminal({
148-
name: `Run ${context.name}`,
149-
pty: {
150-
onDidWrite: outputEmitter.event,
151-
open: () => {
152-
outputEmitter.fire(`Running ${context.name} \r\n`);
153-
const opt: SpawnOptions = { cwd: context.folderURI.fsPath };
154-
void CliChannel.getInstance().spawnTool(ServerlessCommand.runFunction(context.folderURI.fsPath, runBuild), opt).then((cp) => {
155-
runProcess = cp;
156-
runProcess.on('spawn', () => {
157-
terminal.show();
158-
});
159-
runProcess.on('error', (err) => {
160-
void window.showErrorMessage(err.message);
161-
});
162-
runProcess.stdout.on('data', (chunk) => {
163-
outputEmitter.fire(`${chunk as string}`.replaceAll('\n', '\r\n'));
164-
void commands.executeCommand('openshift.Serverless.refresh', context);
165-
});
166-
runProcess.stderr.on('data', (errChunk) => {
167-
outputEmitter.fire(`\x1b[31m${errChunk as string}\x1b[0m`.replaceAll('\n', '\r\n'));
168-
});
169-
runProcess.on('exit', () => {
170-
outputEmitter.fire('\r\nPress any key to close this terminal\r\n');
171-
});
172-
});
101+
public async runFunction(context: FunctionObject, runBuild = false) {
102+
const terminal = await OpenShiftTerminalManager.getInstance().createTerminal(
103+
ServerlessCommand.runFunction(context.folderURI.fsPath, runBuild),
104+
`${runBuild ? 'Build and ' : ''}Run ${context.name}`,
105+
context.folderURI.fsPath,
106+
process.env,
107+
true,
108+
{
109+
onSpawn: () => {
110+
void commands.executeCommand('openshift.Serverless.refresh', context);
173111
},
174-
close: () => {
175-
if (runProcess && runProcess.exitCode === null) { // if process is still running and user closed terminal
176-
runProcess.kill('SIGINT');
177-
}
112+
onExit: () => {
178113
this.runTerminalMap.delete(`run-${context.folderURI.fsPath}`);
179-
terminal = undefined;
180114
void commands.executeCommand('openshift.Serverless.refresh', context);
181-
},
182-
handleInput: ((_data: string) => {
183-
if (!runProcess) {
184-
terminal.dispose();
185-
} else {
186-
outputEmitter.fire('^C\r\n');
187-
runProcess.kill('SIGINT');
188-
terminal.dispose()
189-
}
190-
})
191-
},
192-
});
115+
}
116+
}
117+
);
193118
this.runTerminalMap.set(`run-${context.folderURI.fsPath}`, terminal);
194119
}
195120

196121
public stopFunction(context: FunctionObject) {
197122
const terminal = this.runTerminalMap.get(`run-${context.folderURI.fsPath}`);
198123
if (terminal) {
199-
terminal.sendText('^C\r\n');
124+
terminal.sendText('\u0003');
200125
}
201126
}
202127

src/webview/openshift-terminal/openShiftTerminal.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,12 @@ import type * as pty from 'node-pty';
99
import { platform } from 'os';
1010
import {
1111
CancellationToken,
12-
ColorTheme,
13-
Disposable,
12+
ColorTheme, commands, Disposable,
1413
Webview,
1514
WebviewView,
1615
WebviewViewProvider,
17-
WebviewViewResolveContext,
18-
commands,
19-
window,
20-
workspace,
16+
WebviewViewResolveContext, window,
17+
workspace
2118
} from 'vscode';
2219
import { SerializeAddon } from 'xterm-addon-serialize';
2320
import { Terminal } from 'xterm-headless';
@@ -522,9 +519,6 @@ export class OpenShiftTerminalManager implements WebviewViewProvider {
522519
await commands.executeCommand('openShiftTerminalView.focus');
523520
// wait until the webview is ready to receive requests to create terminals
524521
await this.webviewResolved;
525-
// issue request to create terminal in the webview
526-
const newTermUUID = randomUUID();
527-
await this.sendMessage({ kind: 'createTerminal', data: { uuid: newTermUUID, name } });
528522

529523
const [cmd, ...args] = `${commandText}`.split(' ');
530524
let toolLocation: string | undefined;
@@ -547,6 +541,8 @@ export class OpenShiftTerminalManager implements WebviewViewProvider {
547541
throw new Error(msg);
548542
}
549543

544+
const newTermUUID = randomUUID();
545+
550546
// create the object that manages the headless terminal and the pty.
551547
// the process is run as a child process under node.
552548
// the webview is synchronized to the pty and headless terminal using message passing
@@ -569,6 +565,9 @@ export class OpenShiftTerminalManager implements WebviewViewProvider {
569565
),
570566
);
571567

568+
// issue request to create terminal in the webview
569+
await this.sendMessage({ kind: 'createTerminal', data: { uuid: newTermUUID, name } });
570+
572571
return {
573572
sendText: (text: string) => this.openShiftTerminals.get(newTermUUID).write(text),
574573
focusTerminal: () =>

0 commit comments

Comments
 (0)