Skip to content

Commit 8f10c5f

Browse files
Add support for port.onOpen (gitpod-io#21)
1 parent 07a9b13 commit 8f10c5f

File tree

10 files changed

+462
-34
lines changed

10 files changed

+462
-34
lines changed

assets/styles.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ pre {
104104
padding-bottom: 1em;
105105
}
106106

107+
dialog form {
108+
display: flex;
109+
justify-content: space-between;
110+
}
111+
107112
#terminal-container {
108113
min-width: 100vw;
109114
min-height: 100vh;

example/workspace/.gitpod.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,21 @@
11
image:
22
file: Dockerfile
3+
4+
tasks:
5+
- name: Start server
6+
command: curl lama.sh | LAMA_PORT=3000 sh
7+
- name: Start server 2
8+
command: curl lama.sh | LAMA_PORT=3001 sh
9+
- name: Launch JupyterLab
10+
init: pip install jupyterlab
11+
command: gp timeout extend;
12+
jupyter lab --port 8888 --ServerApp.token='' --ServerApp.allow_remote_access=true --no-browser
13+
14+
ports:
15+
- port: 3000
16+
onOpen: ignore
17+
- port: 3001
18+
onOpen: ignore
19+
- port: 8888
20+
name: JupyterLab
21+
onOpen: notify

example/workspace/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
FROM ubuntu:rolling
1+
FROM gitpod/workspace-full
22

3-
RUN apt-get update -y && apt-get install -y git tmux
3+
# RUN apt-get update -y && apt-get install -y git tmux curl

index.html

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,6 @@
1010
</head>
1111
<body>
1212
<main id="terminal-container"></main>
13-
14-
<dialog id="output">
15-
<p id="outputContent"></p>
16-
<form method="dialog">
17-
<button value="cancel">Cancel</button>
18-
</form>
19-
</dialog>
2013
<script type="text/javascript" src="/_supervisor/frontend/main.js" charset="utf-8"></script>
2114
<script type="module">
2215
import { Buffer } from "buffer";

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"format": "prettier -w . --experimental-ternaries",
1111
"start": "yarn package && node dist/index.cjs",
1212
"package:client": "rimraf out/ && yarn build && mkdir -p out/ && cp -r index.html dist/ assets/ out/",
13-
"package:server": "ncc build -m server.cjs -o dist/ --target es2022 --source-map",
13+
"package:server": "tsc --module commonjs supervisor-helper.ts --outdir out --esmoduleinterop true --declaration && mv out/supervisor-helper.js out/supervisor-helper.cjs && ncc build -m server.cjs -o dist/ --target es2022 --source-map",
1414
"inject-commit": "git rev-parse HEAD > dist/commit.txt",
1515
"package": "yarn build && yarn package:server && yarn inject-commit"
1616
},
@@ -29,6 +29,8 @@
2929
"homepage": "https://github.com/gitpod-io/xterm-web-ide#readme",
3030
"dependencies": {
3131
"@gitpod/gitpod-protocol": "^0.1.5-main.6983",
32+
"@gitpod/supervisor-api-grpc": "0.1.5-main-gha.10852",
33+
"@grpc/grpc-js": "^1.11.1",
3234
"@xterm/addon-attach": "^0.11.0",
3335
"@xterm/addon-canvas": "^0.7.0",
3436
"@xterm/addon-fit": "^0.10.0",
@@ -45,7 +47,6 @@
4547
"reconnecting-websocket": "^4.4.0"
4648
},
4749
"devDependencies": {
48-
"prettier": "^3.3.3",
4950
"@open-wc/building-rollup": "^3.0.2",
5051
"@rollup/plugin-commonjs": "^26.0.1",
5152
"@rollup/plugin-terser": "^0.4.4",
@@ -57,6 +58,7 @@
5758
"express": "^4.19.2",
5859
"express-ws": "^5.0.2",
5960
"node-pty": "^1.0.0",
61+
"prettier": "^3.3.3",
6062
"rimraf": "^4.1.2",
6163
"rollup": "^4.19.0",
6264
"rollup-plugin-node-polyfills": "^0.2.1",

server.cjs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
const express = require("express");
44
const expressWs = require("express-ws");
55
const pty = require("node-pty");
6-
const events = require("events");
76
const crypto = require("crypto");
87

98
const rateLimit = require("express-rate-limit").default;
109

1110
const WebSocket = require("ws");
1211
const argv = require("minimist")(process.argv.slice(2), { boolean: ["openExternal"] });
1312

13+
const { getOpenablePorts } = require("./out/supervisor-helper.cjs");
14+
const { PortsStatus } = require("@gitpod/supervisor-api-grpc/lib/status_pb");
15+
const { EventEmitter } = require("events");
16+
1417
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 23000;
1518
const host = "0.0.0.0";
1619

@@ -137,7 +140,7 @@ function startServer() {
137140
res.end();
138141
});
139142

140-
const em = new events.EventEmitter();
143+
const em = new EventEmitter();
141144
app.ws("/terminals/remote-communication-channel/", (ws, _req) => {
142145
console.info(`Client joined remote communication channel`);
143146

@@ -156,13 +159,40 @@ function startServer() {
156159
em.on("message", (msg) => {
157160
ws.send(JSON.stringify(msg));
158161
});
162+
163+
async function sendPortUpdates() {
164+
for await (const ports of getOpenablePorts()) {
165+
for (const port of ports) {
166+
if (!port.exposed || !port.exposed.url) {
167+
continue;
168+
}
169+
const id = crypto.randomUUID();
170+
if (port.onOpen === PortsStatus.OnOpenAction.NOTIFY) {
171+
ws.send(
172+
JSON.stringify({
173+
action: "notifyAboutUrl",
174+
data: { url: port.exposed.url, port: port.localPort, name: port.name },
175+
id,
176+
}),
177+
);
178+
} else {
179+
ws.send(JSON.stringify({ action: "openUrl", data: port.exposed.url, id }));
180+
}
181+
}
182+
}
183+
}
184+
185+
sendPortUpdates();
159186
});
160187

188+
let clientForExternalMessages = null;
161189
app.ws("/terminals/:pid", (ws, req) => {
162190
const term = terminals[parseInt(req.params.pid)];
163191
console.log(`Client connected to terminal ${term.pid}`);
164192
ws.send(logs[term.pid]);
165193

194+
clientForExternalMessages = term.pid;
195+
166196
// binary message buffering
167197
function bufferUtf8(socket, timeout) {
168198
let buffer = [];

src/client.ts

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { initiateRemoteCommunicationChannelSocket } from "./lib/remote";
1818
import { Emitter } from "@gitpod/gitpod-protocol/lib/util/event";
1919
import { DisposableCollection } from "@gitpod/gitpod-protocol/lib/util/disposable";
2020
import debounce from "lodash/debounce";
21+
import { type UUID } from "node:crypto";
2122

2223
const onDidChangeState = new Emitter<void>();
2324
let state: IDEFrontendState = "initializing" as IDEFrontendState;
@@ -104,6 +105,7 @@ async function initiateRemoteTerminal(terminal: Terminal): Promise<void | Reconn
104105
if (!initialTerminalResizeRequest.ok) {
105106
output("Could not setup IDE. Retry?", {
106107
formActions: [reloadButton],
108+
reason: "error",
107109
});
108110
return;
109111
}
@@ -118,7 +120,6 @@ async function initiateRemoteTerminal(terminal: Terminal): Promise<void | Reconn
118120

119121
const socket = new ReconnectingWebSocket(socketURL, [], webSocketSettings);
120122
socket.onopen = async () => {
121-
outputDialog.close();
122123
(document.querySelector(".xterm-helper-textarea") as HTMLTextAreaElement).focus();
123124

124125
await runRealTerminal(term, socket as WebSocket);
@@ -228,14 +229,18 @@ function handleDisconnected(e: CloseEvent | ErrorEvent, socket: ReconnectingWebS
228229
case 1005:
229230
output("For some reason the WebSocket closed. Reload?", {
230231
formActions: [reconnectButton, reloadButton],
232+
reason: "error",
231233
});
232234
case 1006:
233235
if (navigator.onLine) {
234236
output("Cannot reach workspace, consider reloading", {
235237
formActions: [reloadButton],
238+
reason: "error",
236239
});
237240
} else {
238-
output("You are offline, please connect to the internet and refresh this page");
241+
output("You are offline, please connect to the internet and refresh this page", {
242+
reason: "error",
243+
});
239244
}
240245
break;
241246
default:
@@ -246,19 +251,57 @@ function handleDisconnected(e: CloseEvent | ErrorEvent, socket: ReconnectingWebS
246251
console.error(e);
247252
}
248253

249-
const outputDialog = document.getElementById("output") as HTMLDialogElement;
250-
const outputContent = document.getElementById("outputContent")!;
251-
function output(message: string, options?: { formActions: HTMLInputElement[] | HTMLButtonElement[] }) {
252-
if (typeof outputDialog.showModal === "function") {
253-
outputContent.innerText = message;
254-
if (options?.formActions) {
255-
for (const action of options.formActions) {
256-
outputDialog.querySelector("form")!.appendChild(action);
257-
}
258-
}
259-
outputDialog.showModal();
254+
type OutputReason = "info" | "error";
255+
256+
const outputStack = new Set<UUID>();
257+
export const output = (
258+
message: string,
259+
options?: { formActions?: (HTMLInputElement | HTMLButtonElement)[]; reason?: OutputReason },
260+
): UUID => {
261+
const outputId = crypto.randomUUID();
262+
const dialogElement = document.createElement("dialog");
263+
dialogElement.id = outputId;
264+
265+
const outputContent = document.createElement("p");
266+
outputContent.innerText = message;
267+
dialogElement.appendChild(outputContent);
268+
269+
const outputForm = document.createElement("form");
270+
outputForm.method = "dialog";
271+
const formActions = options?.formActions ?? [];
272+
const dismissButton = document.createElement("button");
273+
dismissButton.innerText = "Dismiss";
274+
formActions.push(dismissButton);
275+
for (const action of formActions) {
276+
outputForm.appendChild(action);
260277
}
261-
}
278+
279+
const outputReasonInput = document.createElement("input");
280+
outputReasonInput.type = "hidden";
281+
outputReasonInput.value = options?.reason ?? "info";
282+
outputForm.appendChild(outputReasonInput);
283+
284+
dialogElement.appendChild(outputForm);
285+
286+
document.body.appendChild(dialogElement);
287+
dialogElement.showModal();
288+
289+
outputStack.add(outputId);
290+
291+
return outputId;
292+
};
293+
294+
export const closeModal = (id: UUID) => {
295+
const dialogElement = document.getElementById(id) as HTMLDialogElement;
296+
if (!dialogElement) {
297+
console.warn(`Could not find dialog with ID ${id}`);
298+
return;
299+
}
300+
301+
dialogElement.close();
302+
dialogElement.remove();
303+
outputStack.delete(id);
304+
};
262305

263306
let attachAddon: AttachAddon;
264307

@@ -306,6 +349,9 @@ window.gitpod.ideService = {
306349
toDispose.push({
307350
dispose: () => {
308351
socket.close();
352+
for (const id of outputStack) {
353+
closeModal(id);
354+
}
309355
},
310356
});
311357
});

src/lib/remote.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { webSocketSettings } from "../client";
1+
import { output, webSocketSettings } from "../client";
22
import { IXtermWindow } from "./types";
33

44
declare let window: IXtermWindow;
@@ -42,10 +42,32 @@ export const initiateRemoteCommunicationChannelSocket = async (protocol: string)
4242
return;
4343
}
4444

45-
if (messageData.action === "openUrl") {
46-
const url = messageData.data;
47-
console.debug(`Opening URL: ${url}`);
48-
window.open(url, "_blank");
45+
switch (messageData.action) {
46+
case "openUrl": {
47+
const url = messageData.data;
48+
console.debug(`Opening URL: ${url}`);
49+
window.open(url, "_blank");
50+
break;
51+
}
52+
case "notifyAboutUrl": {
53+
const { url, port, name } = messageData.data;
54+
55+
const openUrlButton = document.createElement("button");
56+
openUrlButton.innerText = "Open URL";
57+
openUrlButton.onclick = () => {
58+
window.open(url, "_blank");
59+
};
60+
61+
if (name) {
62+
output(`${name} on port ${port} has been opened`, { formActions: [openUrlButton], reason: "info" });
63+
break;
64+
}
65+
66+
output(`Port ${port} has been opened`, { formActions: [openUrlButton], reason: "info" });
67+
break;
68+
}
69+
default:
70+
console.debug("Unhandled message", messageData);
4971
}
5072

5173
window.handledMessages.push(messageData.id);

0 commit comments

Comments
 (0)