Skip to content

Commit

Permalink
Add support for port.onOpen (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
filiptronicek authored Jul 25, 2024
1 parent 07a9b13 commit 8f10c5f
Show file tree
Hide file tree
Showing 10 changed files with 462 additions and 34 deletions.
5 changes: 5 additions & 0 deletions assets/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ pre {
padding-bottom: 1em;
}

dialog form {
display: flex;
justify-content: space-between;
}

#terminal-container {
min-width: 100vw;
min-height: 100vh;
Expand Down
19 changes: 19 additions & 0 deletions example/workspace/.gitpod.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
image:
file: Dockerfile

tasks:
- name: Start server
command: curl lama.sh | LAMA_PORT=3000 sh
- name: Start server 2
command: curl lama.sh | LAMA_PORT=3001 sh
- name: Launch JupyterLab
init: pip install jupyterlab
command: gp timeout extend;
jupyter lab --port 8888 --ServerApp.token='' --ServerApp.allow_remote_access=true --no-browser

ports:
- port: 3000
onOpen: ignore
- port: 3001
onOpen: ignore
- port: 8888
name: JupyterLab
onOpen: notify
4 changes: 2 additions & 2 deletions example/workspace/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
FROM ubuntu:rolling
FROM gitpod/workspace-full

RUN apt-get update -y && apt-get install -y git tmux
# RUN apt-get update -y && apt-get install -y git tmux curl
7 changes: 0 additions & 7 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,6 @@
</head>
<body>
<main id="terminal-container"></main>

<dialog id="output">
<p id="outputContent"></p>
<form method="dialog">
<button value="cancel">Cancel</button>
</form>
</dialog>
<script type="text/javascript" src="/_supervisor/frontend/main.js" charset="utf-8"></script>
<script type="module">
import { Buffer } from "buffer";
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"format": "prettier -w . --experimental-ternaries",
"start": "yarn package && node dist/index.cjs",
"package:client": "rimraf out/ && yarn build && mkdir -p out/ && cp -r index.html dist/ assets/ out/",
"package:server": "ncc build -m server.cjs -o dist/ --target es2022 --source-map",
"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",
"inject-commit": "git rev-parse HEAD > dist/commit.txt",
"package": "yarn build && yarn package:server && yarn inject-commit"
},
Expand All @@ -29,6 +29,8 @@
"homepage": "https://github.com/gitpod-io/xterm-web-ide#readme",
"dependencies": {
"@gitpod/gitpod-protocol": "^0.1.5-main.6983",
"@gitpod/supervisor-api-grpc": "0.1.5-main-gha.10852",
"@grpc/grpc-js": "^1.11.1",
"@xterm/addon-attach": "^0.11.0",
"@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "^0.10.0",
Expand All @@ -45,7 +47,6 @@
"reconnecting-websocket": "^4.4.0"
},
"devDependencies": {
"prettier": "^3.3.3",
"@open-wc/building-rollup": "^3.0.2",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-terser": "^0.4.4",
Expand All @@ -57,6 +58,7 @@
"express": "^4.19.2",
"express-ws": "^5.0.2",
"node-pty": "^1.0.0",
"prettier": "^3.3.3",
"rimraf": "^4.1.2",
"rollup": "^4.19.0",
"rollup-plugin-node-polyfills": "^0.2.1",
Expand Down
34 changes: 32 additions & 2 deletions server.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
const express = require("express");
const expressWs = require("express-ws");
const pty = require("node-pty");
const events = require("events");
const crypto = require("crypto");

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

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

const { getOpenablePorts } = require("./out/supervisor-helper.cjs");
const { PortsStatus } = require("@gitpod/supervisor-api-grpc/lib/status_pb");
const { EventEmitter } = require("events");

const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 23000;
const host = "0.0.0.0";

Expand Down Expand Up @@ -137,7 +140,7 @@ function startServer() {
res.end();
});

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

Expand All @@ -156,13 +159,40 @@ function startServer() {
em.on("message", (msg) => {
ws.send(JSON.stringify(msg));
});

async function sendPortUpdates() {
for await (const ports of getOpenablePorts()) {
for (const port of ports) {
if (!port.exposed || !port.exposed.url) {
continue;
}
const id = crypto.randomUUID();
if (port.onOpen === PortsStatus.OnOpenAction.NOTIFY) {
ws.send(
JSON.stringify({
action: "notifyAboutUrl",
data: { url: port.exposed.url, port: port.localPort, name: port.name },
id,
}),
);
} else {
ws.send(JSON.stringify({ action: "openUrl", data: port.exposed.url, id }));
}
}
}
}

sendPortUpdates();
});

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

clientForExternalMessages = term.pid;

// binary message buffering
function bufferUtf8(socket, timeout) {
let buffer = [];
Expand Down
74 changes: 60 additions & 14 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { initiateRemoteCommunicationChannelSocket } from "./lib/remote";
import { Emitter } from "@gitpod/gitpod-protocol/lib/util/event";
import { DisposableCollection } from "@gitpod/gitpod-protocol/lib/util/disposable";
import debounce from "lodash/debounce";
import { type UUID } from "node:crypto";

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

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

await runRealTerminal(term, socket as WebSocket);
Expand Down Expand Up @@ -228,14 +229,18 @@ function handleDisconnected(e: CloseEvent | ErrorEvent, socket: ReconnectingWebS
case 1005:
output("For some reason the WebSocket closed. Reload?", {
formActions: [reconnectButton, reloadButton],
reason: "error",
});
case 1006:
if (navigator.onLine) {
output("Cannot reach workspace, consider reloading", {
formActions: [reloadButton],
reason: "error",
});
} else {
output("You are offline, please connect to the internet and refresh this page");
output("You are offline, please connect to the internet and refresh this page", {
reason: "error",
});
}
break;
default:
Expand All @@ -246,19 +251,57 @@ function handleDisconnected(e: CloseEvent | ErrorEvent, socket: ReconnectingWebS
console.error(e);
}

const outputDialog = document.getElementById("output") as HTMLDialogElement;
const outputContent = document.getElementById("outputContent")!;
function output(message: string, options?: { formActions: HTMLInputElement[] | HTMLButtonElement[] }) {
if (typeof outputDialog.showModal === "function") {
outputContent.innerText = message;
if (options?.formActions) {
for (const action of options.formActions) {
outputDialog.querySelector("form")!.appendChild(action);
}
}
outputDialog.showModal();
type OutputReason = "info" | "error";

const outputStack = new Set<UUID>();
export const output = (
message: string,
options?: { formActions?: (HTMLInputElement | HTMLButtonElement)[]; reason?: OutputReason },
): UUID => {
const outputId = crypto.randomUUID();
const dialogElement = document.createElement("dialog");
dialogElement.id = outputId;

const outputContent = document.createElement("p");
outputContent.innerText = message;
dialogElement.appendChild(outputContent);

const outputForm = document.createElement("form");
outputForm.method = "dialog";
const formActions = options?.formActions ?? [];
const dismissButton = document.createElement("button");
dismissButton.innerText = "Dismiss";
formActions.push(dismissButton);
for (const action of formActions) {
outputForm.appendChild(action);
}
}

const outputReasonInput = document.createElement("input");
outputReasonInput.type = "hidden";
outputReasonInput.value = options?.reason ?? "info";
outputForm.appendChild(outputReasonInput);

dialogElement.appendChild(outputForm);

document.body.appendChild(dialogElement);
dialogElement.showModal();

outputStack.add(outputId);

return outputId;
};

export const closeModal = (id: UUID) => {
const dialogElement = document.getElementById(id) as HTMLDialogElement;
if (!dialogElement) {
console.warn(`Could not find dialog with ID ${id}`);
return;
}

dialogElement.close();
dialogElement.remove();
outputStack.delete(id);
};

let attachAddon: AttachAddon;

Expand Down Expand Up @@ -306,6 +349,9 @@ window.gitpod.ideService = {
toDispose.push({
dispose: () => {
socket.close();
for (const id of outputStack) {
closeModal(id);
}
},
});
});
Expand Down
32 changes: 27 additions & 5 deletions src/lib/remote.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { webSocketSettings } from "../client";
import { output, webSocketSettings } from "../client";
import { IXtermWindow } from "./types";

declare let window: IXtermWindow;
Expand Down Expand Up @@ -42,10 +42,32 @@ export const initiateRemoteCommunicationChannelSocket = async (protocol: string)
return;
}

if (messageData.action === "openUrl") {
const url = messageData.data;
console.debug(`Opening URL: ${url}`);
window.open(url, "_blank");
switch (messageData.action) {
case "openUrl": {
const url = messageData.data;
console.debug(`Opening URL: ${url}`);
window.open(url, "_blank");
break;
}
case "notifyAboutUrl": {
const { url, port, name } = messageData.data;

const openUrlButton = document.createElement("button");
openUrlButton.innerText = "Open URL";
openUrlButton.onclick = () => {
window.open(url, "_blank");
};

if (name) {
output(`${name} on port ${port} has been opened`, { formActions: [openUrlButton], reason: "info" });
break;
}

output(`Port ${port} has been opened`, { formActions: [openUrlButton], reason: "info" });
break;
}
default:
console.debug("Unhandled message", messageData);
}

window.handledMessages.push(messageData.id);
Expand Down
Loading

0 comments on commit 8f10c5f

Please sign in to comment.