Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/service/service.tsx
2500 views
1
/**
2
* Copyright (c) 2021 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
import {
8
Emitter,
9
GitpodClient,
10
GitpodServer,
11
GitpodServerPath,
12
GitpodService,
13
GitpodServiceImpl,
14
Disposable,
15
} from "@gitpod/gitpod-protocol";
16
import { WebSocketConnectionProvider } from "@gitpod/gitpod-protocol/lib/messaging/browser/connection";
17
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
18
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
19
import { IDEFrontendDashboardService } from "@gitpod/gitpod-protocol/lib/frontend-dashboard-service";
20
import { RemoteTrackMessage } from "@gitpod/gitpod-protocol/lib/analytics";
21
import { converter, helloService, stream, userClient, workspaceClient } from "./public-api";
22
import { getExperimentsClient } from "../experiments/client";
23
import { instrumentWebSocket } from "./metrics";
24
import { LotsOfRepliesResponse } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_pb";
25
import { User } from "@gitpod/public-api/lib/gitpod/v1/user_pb";
26
import {
27
WatchWorkspaceStatusPriority,
28
watchWorkspaceStatusInOrder,
29
} from "../data/workspaces/listen-to-workspace-ws-messages2";
30
import { Workspace, WorkspaceSpec_WorkspaceType, WorkspaceStatus } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
31
import { sendTrackEvent } from "../Analytics";
32
33
export const gitpodHostUrl = new GitpodHostUrl(window.location.toString());
34
35
function createGitpodService<C extends GitpodClient, S extends GitpodServer>() {
36
const host = gitpodHostUrl.asWebsocket().with({ pathname: GitpodServerPath }).withApi();
37
38
const connectionProvider = new WebSocketConnectionProvider();
39
instrumentWebSocketConnection(connectionProvider);
40
let numberOfErrors = 0;
41
let onReconnect = () => {};
42
const proxy = connectionProvider.createProxy<S>(host.toString(), undefined, {
43
onerror: (event: any) => {
44
log.error(event);
45
// don't show alert if dashboard is inside iframe (workspace origin)
46
if (window.top !== window.self && process.env.NODE_ENV === "production") {
47
return;
48
}
49
if (numberOfErrors++ === 5) {
50
alert(
51
"We are having trouble connecting to the server.\nEither you are offline or websocket connections are blocked.",
52
);
53
}
54
},
55
onListening: (socket) => {
56
onReconnect = () => socket.reconnect();
57
},
58
});
59
60
return new GitpodServiceImpl<C, S>(proxy, { onReconnect });
61
}
62
63
function instrumentWebSocketConnection(connectionProvider: WebSocketConnectionProvider): void {
64
const originalCreateWebSocket = connectionProvider["createWebSocket"];
65
connectionProvider["createWebSocket"] = (url: string) => {
66
return originalCreateWebSocket.call(
67
connectionProvider,
68
url,
69
new Proxy(WebSocket, {
70
construct(target: any, argArray) {
71
const webSocket = new target(...argArray);
72
instrumentWebSocket(webSocket, "gitpod");
73
return webSocket;
74
},
75
}),
76
);
77
};
78
}
79
80
export function getGitpodService(): GitpodService {
81
const w = window as any;
82
const _gp = w._gp || (w._gp = {});
83
let service = _gp.gitpodService;
84
if (!service) {
85
service = _gp.gitpodService = createGitpodService();
86
testPublicAPI(service);
87
}
88
return service;
89
}
90
91
/**
92
* Emulates getWorkspace calls and listen to workspace statuses with Public API.
93
* // TODO(ak): remove after reliability of Public API is confirmed
94
*/
95
function testPublicAPI(service: any): void {
96
let user: any;
97
service.server = new Proxy(service.server, {
98
get(target, propKey) {
99
return async function (...args: any[]) {
100
if (propKey === "getLoggedInUser") {
101
user = await target[propKey](...args);
102
return user;
103
}
104
if (propKey === "getWorkspace") {
105
try {
106
return await target[propKey](...args);
107
} finally {
108
// emulates frequent unary calls to public API
109
const isTest = await getExperimentsClient().getValueAsync(
110
"public_api_dummy_reliability_test",
111
false,
112
{
113
user,
114
gitpodHost: window.location.host,
115
},
116
);
117
if (isTest) {
118
helloService.sayHello({}).catch((e) => console.error(e));
119
}
120
}
121
}
122
return target[propKey](...args);
123
};
124
},
125
});
126
(async () => {
127
let previousCount = 0;
128
const watchLotsOfReplies = () =>
129
stream<LotsOfRepliesResponse>(
130
(options) => {
131
return helloService.lotsOfReplies({ previousCount }, options);
132
},
133
(response) => {
134
previousCount = response.count;
135
},
136
);
137
138
// emulates server side streaming with public API
139
let watching: Disposable | undefined;
140
while (true) {
141
const isTest =
142
!!user &&
143
(await getExperimentsClient().getValueAsync("public_api_dummy_reliability_test", false, {
144
user,
145
gitpodHost: window.location.host,
146
}));
147
if (isTest) {
148
if (!watching) {
149
watching = watchLotsOfReplies();
150
}
151
} else if (watching) {
152
watching.dispose();
153
watching = undefined;
154
}
155
await new Promise((resolve) => setTimeout(resolve, 3000));
156
}
157
})();
158
}
159
let ideFrontendService: IDEFrontendService | undefined;
160
export function getIDEFrontendService(workspaceID: string, sessionId: string, service: GitpodService) {
161
if (!ideFrontendService) {
162
ideFrontendService = new IDEFrontendService(workspaceID, sessionId, service, window.parent);
163
}
164
return ideFrontendService;
165
}
166
167
export class IDEFrontendService implements IDEFrontendDashboardService.IServer {
168
private instanceID: string | undefined;
169
private ownerId: string | undefined;
170
private user: User | undefined;
171
private ideCredentials!: string;
172
private workspace!: Workspace;
173
private isDesktopIDE: boolean = false;
174
175
private latestInfo?: IDEFrontendDashboardService.Info;
176
177
private readonly onDidChangeEmitter = new Emitter<IDEFrontendDashboardService.SetStateData>();
178
readonly onSetState = this.onDidChangeEmitter.event;
179
180
constructor(
181
private workspaceID: string,
182
private sessionId: string,
183
private service: GitpodService,
184
private clientWindow: Window,
185
) {
186
this.processServerInfo();
187
this.sendFeatureFlagsUpdate();
188
window.addEventListener("message", (event: MessageEvent) => {
189
if (IDEFrontendDashboardService.isTrackEventData(event.data)) {
190
this.trackEvent(event.data.msg);
191
}
192
if (IDEFrontendDashboardService.isHeartbeatEventData(event.data)) {
193
this.activeHeartbeat();
194
}
195
if (IDEFrontendDashboardService.isSetStateEventData(event.data)) {
196
this.onDidChangeEmitter.fire(event.data.state);
197
if (event.data.state.desktopIDE) {
198
this.isDesktopIDE = true;
199
}
200
}
201
if (IDEFrontendDashboardService.isOpenDesktopIDE(event.data)) {
202
this.openDesktopIDE(event.data.url);
203
}
204
if (IDEFrontendDashboardService.isFeatureFlagsRequestEventData(event.data)) {
205
this.sendFeatureFlagsUpdate();
206
}
207
});
208
window.addEventListener("unload", () => {
209
if (!this.instanceID) {
210
return;
211
}
212
if (this.ownerId !== this.user?.id) {
213
return;
214
}
215
// we only send the close heartbeat if we are in a web IDE
216
if (this.isDesktopIDE) {
217
return;
218
}
219
// send last heartbeat (wasClosed: true)
220
const data = { sessionId: this.sessionId };
221
const blob = new Blob([JSON.stringify(data)], { type: "application/json" });
222
const gitpodHostUrl = new GitpodHostUrl(window.location.toString());
223
const url = gitpodHostUrl.withApi({ pathname: `/auth/workspacePageClose/${this.instanceID}` }).toString();
224
navigator.sendBeacon(url, blob);
225
});
226
}
227
228
private async processServerInfo() {
229
const [user, workspaceResponse, ideCredentials] = await Promise.all([
230
userClient.getAuthenticatedUser({}).then((r) => r.user),
231
workspaceClient.getWorkspace({ workspaceId: this.workspaceID }),
232
workspaceClient
233
.getWorkspaceEditorCredentials({ workspaceId: this.workspaceID })
234
.then((resp) => resp.editorCredentials),
235
]);
236
this.workspace = workspaceResponse.workspace!;
237
this.user = user;
238
this.ideCredentials = ideCredentials;
239
const reconcile = async (status?: WorkspaceStatus) => {
240
const info = this.parseInfo(status ?? this.workspace.status!);
241
this.latestInfo = info;
242
const oldInstanceID = this.instanceID;
243
this.instanceID = info.instanceId;
244
this.ownerId = info.ownerId;
245
246
if (info.instanceId && oldInstanceID !== info.instanceId) {
247
this.auth();
248
}
249
250
// Redirect to custom url
251
if (
252
(info.statusPhase === "stopping" || info.statusPhase === "stopped") &&
253
info.workspaceType === "regular"
254
) {
255
await this.redirectToCustomUrl(info);
256
}
257
258
this.sendInfoUpdate(this.latestInfo);
259
};
260
reconcile();
261
watchWorkspaceStatusInOrder(this.workspaceID, WatchWorkspaceStatusPriority.SupervisorService, (response) => {
262
if (response.status) {
263
reconcile(response.status);
264
}
265
});
266
}
267
268
private parseInfo(status: WorkspaceStatus): IDEFrontendDashboardService.Info {
269
return {
270
loggedUserId: this.user!.id,
271
workspaceID: this.workspaceID,
272
instanceId: status.instanceId,
273
ideUrl: status.workspaceUrl,
274
statusPhase: status.phase?.name ? converter.fromPhase(status.phase?.name) : "unknown",
275
workspaceDescription: this.workspace.metadata?.name ?? "",
276
workspaceType: this.workspace.spec?.type === WorkspaceSpec_WorkspaceType.PREBUILD ? "prebuild" : "regular",
277
credentialsToken: this.ideCredentials,
278
ownerId: this.workspace.metadata?.ownerId ?? "",
279
};
280
}
281
282
private async redirectToCustomUrl(info: IDEFrontendDashboardService.Info) {
283
const isDataOps = await getExperimentsClient().getValueAsync("dataops", false, {
284
user: { id: this.user!.id },
285
gitpodHost: gitpodHostUrl.toString(),
286
});
287
const dataOpsRedirectUrl = await getExperimentsClient().getValueAsync("dataops_redirect_url", "undefined", {
288
user: { id: this.user!.id },
289
gitpodHost: gitpodHostUrl.toString(),
290
});
291
292
if (!isDataOps) {
293
return;
294
}
295
296
try {
297
const params: Record<string, string> = { workspaceID: info.workspaceID };
298
let redirectURL: string;
299
if (dataOpsRedirectUrl === "undefined") {
300
redirectURL = this.workspace.metadata?.originalContextUrl ?? "";
301
} else {
302
redirectURL = dataOpsRedirectUrl;
303
params.contextURL = this.workspace.metadata?.originalContextUrl ?? "";
304
}
305
const url = new URL(redirectURL);
306
url.search = new URLSearchParams([
307
...Array.from(url.searchParams.entries()),
308
...Object.entries(params),
309
]).toString();
310
this.relocate(url.toString());
311
} catch {
312
console.error("Invalid redirect URL");
313
}
314
}
315
316
// implements
317
318
private async auth() {
319
if (!this.instanceID) {
320
return;
321
}
322
const url = gitpodHostUrl.asWorkspaceAuth(this.instanceID).toString();
323
await fetch(url, {
324
credentials: "include",
325
});
326
}
327
328
private trackEvent(msg: RemoteTrackMessage): void {
329
msg.properties = {
330
...msg.properties,
331
sessionId: this.sessionId,
332
instanceId: this.latestInfo?.instanceId,
333
workspaceId: this.workspaceID,
334
type: this.latestInfo?.workspaceType,
335
};
336
sendTrackEvent(msg);
337
}
338
339
private activeHeartbeat(): void {
340
if (this.workspaceID) {
341
workspaceClient.sendHeartBeat({ workspaceId: this.workspaceID });
342
}
343
}
344
345
openDesktopIDE(url: string): void {
346
let redirect = false;
347
try {
348
const desktopLink = new URL(url);
349
// allow to redirect only for whitelisted trusted protocols
350
// IDE-69
351
const trustedProtocols = ["vscode:", "vscode-insiders:", "jetbrains-gateway:", "jetbrains:"];
352
redirect = trustedProtocols.includes(desktopLink.protocol);
353
if (
354
redirect &&
355
desktopLink.protocol === "jetbrains:" &&
356
!desktopLink.href.startsWith("jetbrains://gateway/io.gitpod.toolbox.gateway/")
357
) {
358
redirect = false;
359
}
360
} catch (e) {
361
console.error("invalid desktop link:", e);
362
}
363
// redirect only if points to desktop application
364
// don't navigate browser to another page
365
if (redirect) {
366
window.location.href = url;
367
} else {
368
window.open(url, "_blank", "noopener");
369
}
370
}
371
372
sendInfoUpdate(info: IDEFrontendDashboardService.Info): void {
373
this.clientWindow.postMessage(
374
{
375
version: 1,
376
type: "ide-info-update",
377
info,
378
} as IDEFrontendDashboardService.InfoUpdateEventData,
379
"*",
380
);
381
}
382
383
private async sendFeatureFlagsUpdate() {
384
const supervisor_check_ready_retry = await getExperimentsClient().getValueAsync(
385
"supervisor_check_ready_retry",
386
false,
387
{
388
gitpodHost: gitpodHostUrl.toString(),
389
},
390
);
391
this.clientWindow.postMessage(
392
{
393
type: "ide-feature-flag-update",
394
flags: { supervisor_check_ready_retry },
395
} as IDEFrontendDashboardService.FeatureFlagsUpdateEventData,
396
"*",
397
);
398
}
399
400
relocate(url: string): void {
401
this.clientWindow.postMessage(
402
{ type: "ide-relocate", url } as IDEFrontendDashboardService.RelocateEventData,
403
"*",
404
);
405
}
406
407
openBrowserIDE(): void {
408
this.clientWindow.postMessage({ type: "ide-open-browser" } as IDEFrontendDashboardService.OpenBrowserIDE, "*");
409
}
410
}
411
412