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