Path: blob/main/components/supervisor/frontend/src/ide/supervisor-service-client.ts
2501 views
/**1* Copyright (c) 2020 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 {7SupervisorStatusResponse,8IDEStatusResponse,9ContentStatusResponse,10} from "@gitpod/supervisor-api-grpc/lib/status_pb";11import { WorkspaceInfoResponse } from "@gitpod/supervisor-api-grpc/lib/info_pb";12import { workspaceUrl } from "../shared/urls";13import { FrontendDashboardServiceClient } from "../shared/frontend-dashboard-service";14import { Timeout } from "@gitpod/gitpod-protocol/lib/util/timeout";1516export class SupervisorServiceClient {17readonly supervisorReady = this.checkReady("supervisor");18readonly ideReady = this.supervisorReady.then(() => this.checkReady("ide"));19readonly contentReady = Promise.all([this.supervisorReady]).then(() => this.checkReady("content"));20readonly getWorkspaceInfoPromise = this.supervisorReady.then(() => this.getWorkspaceInfo());21private _supervisorWillShutdown: Promise<void> | undefined;2223constructor(readonly serviceClient: FrontendDashboardServiceClient) {}2425public get supervisorWillShutdown() {26if (!this._supervisorWillShutdown) {27this._supervisorWillShutdown = this.supervisorReady.then(() => this.checkWillShutdown());28}29return this._supervisorWillShutdown;30}3132private async checkWillShutdown(delay = false): Promise<void> {33if (delay) {34await new Promise((resolve) => setTimeout(resolve, 1000));35}36try {37const wsSupervisorStatusUrl = workspaceUrl.with(() => {38return {39pathname: "/_supervisor/v1/status/supervisor/willShutdown/true",40};41});42const response = await fetch(wsSupervisorStatusUrl.toString(), { credentials: "include" });43let result;44if (response.ok) {45result = await response.json();46if ((result as SupervisorStatusResponse.AsObject).ok) {47return;48}49}50if (response.status === 502) {51// bad gateway, supervisor is gone52return;53}54if (response.status === 302 && response.headers.get("location")?.includes("/start/")) {55// redirect to start page, workspace is closed56return;57}58console.debug(59`failed to check whether is about to shutdown, trying again...`,60response.status,61response.statusText,62JSON.stringify(result, undefined, 2),63);64} catch (e) {65// network errors66console.debug(`failed to check whether is about to shutdown, trying again...`, e);67}68await this.checkWillShutdown(true);69}7071private async checkReady(kind: "content" | "ide" | "supervisor", delay?: boolean): Promise<any> {72if (delay) {73await new Promise((resolve) => setTimeout(resolve, 1000));74}7576let wait = "/wait/true";77if (kind == "supervisor") {78wait = "";79}8081// track whenever a) we are done, or b) we try to connect (again)82const trackCheckReady = (p: { aborted?: boolean }, err?: any): void => {83if (!this.serviceClient.isCheckReadyRetryEnabled()) {84return;85}8687const props: Record<string, string> = {88component: "supervisor-frontend",89instanceId: this.serviceClient.latestInfo?.instanceId ?? "",90userId: this.serviceClient.latestInfo?.loggedUserId ?? "",91readyKind: kind,92};93if (err) {94props.errorName = err.name;95props.errorStack = err.message ?? String(err);96}9798props.aborted = String(!!p.aborted);99props.wait = wait;100101this.serviceClient.trackEvent({102event: "supervisor_check_ready",103properties: props,104});105};106107// setup a timeout, which is meant to re-establish the connection every 5 seconds108let isError = false;109const timeout = new Timeout(5000, () => this.serviceClient.isCheckReadyRetryEnabled());110try {111timeout.restart();112113const wsSupervisorStatusUrl = workspaceUrl.with(() => {114return {115pathname: "/_supervisor/v1/status/" + kind + wait,116};117});118const response = await fetch(wsSupervisorStatusUrl.toString(), {119credentials: "include",120signal: timeout.signal,121});122let result;123if (response.ok) {124result = await response.json();125if (kind === "supervisor" && (result as SupervisorStatusResponse.AsObject).ok) {126return;127}128if (kind === "content" && (result as ContentStatusResponse.AsObject).available) {129return;130}131if (kind === "ide" && (result as IDEStatusResponse.AsObject).ok) {132return result;133}134}135console.debug(136`failed to check whether ${kind} is ready, trying again...`,137response.status,138response.statusText,139JSON.stringify(result, undefined, 2),140);141} catch (e) {142console.debug(`failed to check whether ${kind} is ready, trying again...`, e);143144// we want to track this kind of errors, as they are on the critical path (of revealing the workspace)145isError = true;146trackCheckReady({ aborted: timeout.signal?.aborted }, e);147} finally {148if (!isError) {149// make sure we don't track twice in case of an error150trackCheckReady({ aborted: timeout.signal?.aborted });151}152timeout.clear();153}154return this.checkReady(kind, true);155}156157private async getWorkspaceInfo(delay?: boolean): Promise<WorkspaceInfoResponse.AsObject> {158if (delay) {159await new Promise((resolve) => setTimeout(resolve, 1000));160}161try {162const getWorkspaceInfoUrl = workspaceUrl.with(() => {163return {164pathname: "_supervisor/v1/info/workspace",165};166});167const response = await fetch(getWorkspaceInfoUrl.toString(), { credentials: "include" });168let result;169if (response.ok) {170result = await response.json();171return result;172}173console.debug(174`failed to get workspace info, trying again...`,175response.status,176response.statusText,177JSON.stringify(result, undefined, 2),178);179} catch (e) {180console.debug(`failed to get workspace info, trying again...`, e);181}182return this.getWorkspaceInfo(true);183}184}185186187