Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/supervisor/frontend/src/index.ts
2500 views
1
/**
2
* Copyright (c) 2020 Gitpod GmbH. All rights reserved.
3
* Licensed under the GNU Affero General Public License (AGPL).
4
* See License.AGPL.txt in the project root for license information.
5
*/
6
7
/**
8
* <script type="text/javascript" src="/_supervisor/frontend/main.js" charset="utf-8"></script> should be inserted to index.html as first body script,
9
* all other IDE scripts should go afterwards, head element should not have scripts
10
*/
11
12
import { IDEMetricsServiceClient, MetricsName } from "./ide/ide-metrics-service-client";
13
IDEMetricsServiceClient.addCounter(MetricsName.SupervisorFrontendClientTotal).catch(() => {});
14
15
//#region supervisor frontend error capture
16
function isElement(obj: any): obj is Element {
17
return typeof obj.getAttribute === "function";
18
}
19
20
window.addEventListener("error", (event) => {
21
const labels: Record<string, string> = {};
22
let resourceSource: string | null | undefined;
23
if (isElement(event.target)) {
24
// We take a look at what is the resource that was attempted to load;
25
resourceSource = event.target.getAttribute("src") || event.target.getAttribute("href");
26
// If the event has a `target`, it means that it wasn't a script error
27
if (resourceSource) {
28
if (resourceSource.match(new RegExp(/\/build\/ide\/code:.+\/__files__\//g))) {
29
// TODO(ak) reconsider how to hide knowledge of VS Code from supervisor frontend, i.e instrument amd loader instead
30
labels["resource"] = "vscode-web-workbench";
31
}
32
labels["error"] = "LoadError";
33
}
34
}
35
if (event.error) {
36
IDEMetricsServiceClient.reportError(event.error).catch(() => {});
37
} else if (labels["error"] == "LoadError") {
38
let error = new Error("LoadError");
39
IDEMetricsServiceClient.reportError(error, {
40
resource: labels["resource"],
41
url: resourceSource ?? "",
42
}).catch(() => {});
43
}
44
IDEMetricsServiceClient.addCounter(MetricsName.SupervisorFrontendErrorTotal, labels).catch(() => {});
45
});
46
//#endregion
47
48
require("../src/shared/index.css");
49
50
import { WorkspaceInstancePhase } from "@gitpod/gitpod-protocol";
51
import { DisposableCollection } from "@gitpod/gitpod-protocol/lib/util/disposable";
52
import * as heartBeat from "./ide/heart-beat";
53
import * as IDEFrontendService from "./ide/ide-frontend-service-impl";
54
import * as IDEWorker from "./ide/ide-worker";
55
import * as IDEWebSocket from "./ide/ide-web-socket";
56
import { SupervisorServiceClient } from "./ide/supervisor-service-client";
57
import * as LoadingFrame from "./shared/loading-frame";
58
import { workspaceUrl } from "./shared/urls";
59
60
window.gitpod = {} as any;
61
IDEWorker.install();
62
IDEWebSocket.install();
63
const ideService = IDEFrontendService.create();
64
const loadingIDE = new Promise((resolve) => window.addEventListener("DOMContentLoaded", resolve, { once: true }));
65
const toStop = new DisposableCollection();
66
let willRedirect = false;
67
68
document.body.style.visibility = "hidden";
69
LoadingFrame.load().then(async (loading) => {
70
const frontendDashboardServiceClient = loading.frontendDashboardServiceClient;
71
await frontendDashboardServiceClient.initialize();
72
73
if (frontendDashboardServiceClient.latestInfo.workspaceType !== "regular") {
74
return;
75
}
76
77
frontendDashboardServiceClient.onWillRedirect(() => {
78
willRedirect = true;
79
});
80
81
document.title = frontendDashboardServiceClient.latestInfo.workspaceDescription ?? "gitpod";
82
window.gitpod.loggedUserID = frontendDashboardServiceClient.latestInfo.loggedUserId;
83
window.gitpod.openDesktopIDE = frontendDashboardServiceClient.openDesktopIDE.bind(frontendDashboardServiceClient);
84
window.gitpod.decrypt = frontendDashboardServiceClient.decrypt.bind(frontendDashboardServiceClient);
85
window.gitpod.encrypt = frontendDashboardServiceClient.encrypt.bind(frontendDashboardServiceClient);
86
window.gitpod.isEncryptedData = frontendDashboardServiceClient.isEncryptedData.bind(frontendDashboardServiceClient);
87
88
const supervisorServiceClient = new SupervisorServiceClient(frontendDashboardServiceClient);
89
90
(async () => {
91
let hideDesktopIde = false;
92
const hideDesktopIdeEventListener = frontendDashboardServiceClient.onOpenBrowserIDE(() => {
93
hideDesktopIdeEventListener.dispose();
94
hideDesktopIde = true;
95
toStop.push(ideService.start());
96
});
97
toStop.push(hideDesktopIdeEventListener);
98
99
//#region gitpod browser telemetry
100
// TODO(ak) get rid of it
101
// it is bad usage of window.postMessage
102
// VS Code should use Segment directly here and publish to production/staging untrusted
103
// supervisor frontend should not care about IDE specifics
104
window.addEventListener("message", async (event) => {
105
const type = event.data.type;
106
if (type === "vscode_telemetry") {
107
const { event: eventName, properties } = event.data;
108
frontendDashboardServiceClient.trackEvent({
109
event: eventName,
110
properties,
111
});
112
}
113
});
114
//#endregion
115
116
type DesktopIDEStatus = { link: string; label: string; clientID?: string; kind?: String };
117
let isDesktopIde: undefined | boolean = undefined;
118
let ideStatus: undefined | { desktop: DesktopIDEStatus } = undefined;
119
120
//#region current-frame
121
let current: HTMLElement = loading.frame;
122
let desktopRedirected = false;
123
let currentInstanceId = "";
124
const nextFrame = () => {
125
const { instanceId, ideUrl, statusPhase } = frontendDashboardServiceClient.latestInfo ?? {};
126
127
if (instanceId) {
128
// refresh web page when instanceId changed
129
if (currentInstanceId !== "") {
130
if (instanceId !== currentInstanceId && ideUrl !== "") {
131
currentInstanceId = instanceId;
132
window.location.href = ideUrl!;
133
}
134
} else {
135
currentInstanceId = instanceId;
136
}
137
if (statusPhase === "running") {
138
if (!hideDesktopIde) {
139
if (isDesktopIde == undefined) {
140
return loading.frame;
141
}
142
if (isDesktopIde && !!ideStatus) {
143
trackDesktopIDEReady(ideStatus.desktop);
144
frontendDashboardServiceClient.setState({
145
desktopIDE: {
146
link: ideStatus.desktop.link,
147
label: ideStatus.desktop.label || "Open Desktop IDE",
148
clientID: ideStatus.desktop.clientID!,
149
},
150
});
151
if (!desktopRedirected) {
152
desktopRedirected = true;
153
frontendDashboardServiceClient.openDesktopIDE(ideStatus.desktop.link);
154
}
155
return loading.frame;
156
}
157
}
158
if (ideService.state === "ready") {
159
return document.body;
160
}
161
}
162
}
163
return loading.frame;
164
};
165
const updateCurrentFrame = () => {
166
const newCurrent = nextFrame();
167
if (current === newCurrent || willRedirect) {
168
return;
169
}
170
current.style.visibility = "hidden";
171
newCurrent.style.visibility = "visible";
172
if (current === document.body) {
173
while (document.body.firstChild && document.body.firstChild !== newCurrent) {
174
document.body.removeChild(document.body.firstChild);
175
}
176
while (document.body.lastChild && document.body.lastChild !== newCurrent) {
177
document.body.removeChild(document.body.lastChild);
178
}
179
}
180
current = newCurrent;
181
};
182
183
const updateLoadingState = () => {
184
frontendDashboardServiceClient.setState({
185
ideFrontendFailureCause: ideService.failureCause?.message,
186
});
187
};
188
const trackStatusRenderedEvent = (
189
phase: string,
190
properties?: {
191
[prop: string]: any;
192
},
193
) => {
194
frontendDashboardServiceClient.trackEvent({
195
event: "status_rendered",
196
properties: {
197
phase,
198
...properties,
199
},
200
});
201
};
202
let trackedDesktopIDEReady = false;
203
const trackDesktopIDEReady = ({ clientID, kind }: DesktopIDEStatus) => {
204
if (trackedDesktopIDEReady) {
205
return;
206
}
207
trackedDesktopIDEReady = true;
208
trackStatusRenderedEvent("desktop-ide-ready", { clientID, kind });
209
};
210
const trackIDEStatusRenderedEvent = () => {
211
let error: string | undefined;
212
if (ideService.failureCause) {
213
error = `${ideService.failureCause.message}\n${ideService.failureCause.stack}`;
214
}
215
trackStatusRenderedEvent(`ide-${ideService.state}`, { error });
216
};
217
218
updateCurrentFrame();
219
updateLoadingState();
220
trackIDEStatusRenderedEvent();
221
frontendDashboardServiceClient.onInfoUpdate(() => updateCurrentFrame());
222
ideService.onDidChange(() => {
223
updateLoadingState();
224
updateCurrentFrame();
225
trackIDEStatusRenderedEvent();
226
});
227
supervisorServiceClient.ideReady
228
.then((newIdeStatus) => {
229
ideStatus = newIdeStatus;
230
isDesktopIde = !!ideStatus && !!ideStatus.desktop && !!ideStatus.desktop.link;
231
updateCurrentFrame();
232
})
233
.catch((error) => console.error(`Unexpected error from supervisorServiceClient.ideReady: ${error}`));
234
window.addEventListener("unload", () => trackStatusRenderedEvent("window-unload"), { capture: true });
235
//#endregion
236
237
//#region heart-beat
238
heartBeat.track(window);
239
let isOwner = false;
240
supervisorServiceClient.getWorkspaceInfoPromise.then((info) => {
241
isOwner = frontendDashboardServiceClient.latestInfo.loggedUserId === info.ownerId;
242
updateHeartBeat();
243
});
244
const updateHeartBeat = () => {
245
if (frontendDashboardServiceClient.latestInfo?.statusPhase === "running" && isOwner) {
246
heartBeat.schedule(frontendDashboardServiceClient);
247
} else {
248
heartBeat.cancel();
249
}
250
};
251
updateHeartBeat();
252
frontendDashboardServiceClient.onInfoUpdate(() => updateHeartBeat());
253
//#endregion
254
})();
255
256
(async () => {
257
const debugWorkspace = workspaceUrl.debugWorkspace;
258
//#region ide lifecycle
259
function isWorkspaceInstancePhase(phase: WorkspaceInstancePhase): boolean {
260
return frontendDashboardServiceClient.latestInfo?.statusPhase === phase;
261
}
262
if (!isWorkspaceInstancePhase("running")) {
263
if (debugWorkspace && frontendDashboardServiceClient.latestInfo) {
264
window.open("", "_self")?.close();
265
}
266
await new Promise<void>((resolve) => {
267
frontendDashboardServiceClient.onInfoUpdate((status) => {
268
if (status.statusPhase === "running") {
269
resolve();
270
}
271
});
272
});
273
}
274
if (debugWorkspace) {
275
supervisorServiceClient.supervisorWillShutdown.then(() => {
276
window.open("", "_self")?.close();
277
});
278
}
279
const [ideStatus] = await Promise.all([
280
supervisorServiceClient.ideReady,
281
supervisorServiceClient.contentReady,
282
loadingIDE,
283
]);
284
if (isWorkspaceInstancePhase("stopping") || isWorkspaceInstancePhase("stopped")) {
285
return;
286
}
287
toStop.pushAll([
288
IDEWebSocket.connectWorkspace(),
289
frontendDashboardServiceClient.onInfoUpdate((status) => {
290
if (status.statusPhase === "stopping" || status.statusPhase === "stopped") {
291
toStop.dispose();
292
}
293
}),
294
]);
295
const isDesktopIde = ideStatus && ideStatus.desktop && ideStatus.desktop.link;
296
if (!isDesktopIde) {
297
toStop.push(ideService.start());
298
}
299
//#endregion
300
})();
301
});
302
303