Skip to content

Commit 03de270

Browse files
authored
Improve workspace restart (#75)
1 parent 50f0b20 commit 03de270

File tree

12 files changed

+184
-152
lines changed

12 files changed

+184
-152
lines changed

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,10 @@
175175
},
176176
"dependencies": {
177177
"@bufbuild/connect-node": "^0.8.4",
178-
"@gitpod/gitpod-protocol": "main",
179-
"@gitpod/local-app-api-grpcweb": "main",
180-
"@gitpod/public-api": "^0.1.5-main.6530",
181-
"@gitpod/supervisor-api-grpc": "0.1.5-main.6711",
178+
"@gitpod/gitpod-protocol": "main-gha",
179+
"@gitpod/local-app-api-grpcweb": "main-gha",
180+
"@gitpod/public-api": "main-gha",
181+
"@gitpod/supervisor-api-grpc": "main-gha",
182182
"@grpc/grpc-js": "^1.8.8",
183183
"@improbable-eng/grpc-web-node-http-transport": "^0.14.0",
184184
"@microsoft/dev-tunnels-ssh": "^3.11.8",

src/extension.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { CommandManager } from './commandManager';
2121
import { SignInCommand } from './commands/account';
2222
import { ExportLogsCommand } from './commands/logs';
2323
import { Configuration } from './configuration';
24-
import { LocalSSHService } from './services/localSSHService';
24+
import { RemoteService } from './services/remoteService';
2525

2626
// connect-web uses fetch api, so we need to polyfill it
2727
if (!global.fetch) {
@@ -38,13 +38,12 @@ const FIRST_INSTALL_KEY = 'gitpod-desktop.firstInstall';
3838
let telemetryService: TelemetryService | undefined;
3939
let remoteSession: RemoteSession | undefined;
4040
let logger: vscode.LogOutputChannel | undefined;
41-
let hostService: HostService | undefined;
4241

4342
export async function activate(context: vscode.ExtensionContext) {
4443
const extensionId = context.extension.id;
4544
const packageJSON = context.extension.packageJSON;
4645

47-
let remoteConnectionInfo: { remoteAuthority: string; connectionInfo: SSHConnectionParams } | undefined;
46+
let remoteConnectionInfo: { connectionInfo: SSHConnectionParams; remoteUri: vscode.Uri; sshDestStr: string } | undefined;
4847
let success = false;
4948
try {
5049
logger = vscode.window.createOutputChannel('Gitpod', { log: true });
@@ -76,22 +75,22 @@ export async function activate(context: vscode.ExtensionContext) {
7675
const authProvider = new GitpodAuthenticationProvider(context, logger, telemetryService, notificationService);
7776
context.subscriptions.push(authProvider);
7877

79-
hostService = new HostService(context, notificationService, logger);
78+
const hostService = new HostService(context, notificationService, logger);
8079
context.subscriptions.push(hostService);
8180

8281
const sessionService = new SessionService(hostService, logger);
8382
context.subscriptions.push(sessionService);
8483

85-
const localSSHService = new LocalSSHService(context, hostService, telemetryService, sessionService, logger);
86-
context.subscriptions.push(localSSHService);
84+
const remoteService = new RemoteService(context, hostService, telemetryService, sessionService, logger);
85+
context.subscriptions.push(remoteService);
8786

8887
const experiments = new ExperimentalSettings(packageJSON.configcatKey, packageJSON.version, context, sessionService, hostService, logger);
8988
context.subscriptions.push(experiments);
9089

9190
const settingsSync = new SettingsSync(commandManager, logger, telemetryService, notificationService);
9291
context.subscriptions.push(settingsSync);
9392

94-
const remoteConnector = new RemoteConnector(context, sessionService, hostService, experiments, logger, telemetryService, notificationService, localSSHService);
93+
const remoteConnector = new RemoteConnector(context, sessionService, hostService, experiments, logger, telemetryService, notificationService, remoteService);
9594
context.subscriptions.push(remoteConnector);
9695

9796
context.subscriptions.push(vscode.window.registerUriHandler({
@@ -120,17 +119,17 @@ export async function activate(context: vscode.ExtensionContext) {
120119
if (remoteConnectionInfo) {
121120
commandManager.register({ id: 'gitpod.api.autoTunnel', execute: () => remoteConnector.autoTunnelCommand });
122121

123-
remoteSession = new RemoteSession(remoteConnectionInfo.remoteAuthority, remoteConnectionInfo.connectionInfo, context, hostService!, sessionService, settingsSync, experiments, logger!, telemetryService!, notificationService);
122+
remoteSession = new RemoteSession(remoteConnectionInfo.connectionInfo, context, hostService, sessionService, settingsSync, experiments, logger!, telemetryService!, notificationService);
124123
await remoteSession.initialize();
125124
}
126125
});
127126

128127
success = true;
129128
} finally {
130129
const rawActivateProperties = {
131-
gitpodHost: remoteConnectionInfo?.connectionInfo.gitpodHost || hostService?.gitpodHost || Configuration.getGitpodHost(),
130+
gitpodHost: remoteConnectionInfo?.connectionInfo.gitpodHost || Configuration.getGitpodHost(),
132131
isRemoteSSH: String(vscode.env.remoteName === 'ssh-remote'),
133-
remoteUri: vscode.workspace.workspaceFile?.toString() || vscode.workspace.workspaceFolders?.[0].uri.toString() || '',
132+
remoteUri: remoteConnectionInfo?.remoteUri?.toString(),
134133
workspaceId: remoteConnectionInfo?.connectionInfo.workspaceId || '',
135134
instanceId: remoteConnectionInfo?.connectionInfo.instanceId || '',
136135
debugWorkspace: remoteConnectionInfo ? String(!!remoteConnectionInfo.connectionInfo.debugWorkspace) : '',

src/heartbeat.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export class HeartbeatManager extends Disposable {
128128
this.ideHeartbeatTelemetryHandle = setInterval(() => this.sendIDEHeartbeatTelemetry(), HeartbeatManager.IDE_HEARTBEAT_INTERVAL);
129129

130130
if (this.workspaceState) {
131-
this._register(this.workspaceState.onWorkspaceStopped(() => {
131+
this._register(this.workspaceState.onWorkspaceWillStop(() => {
132132
this.logService.trace('Stopping heartbeat as workspace is not running');
133133
this.stopHeartbeat();
134134
this.stopIDEHeartbeatTelemetry();
@@ -151,7 +151,7 @@ export class HeartbeatManager extends Disposable {
151151
let heartbeatSucceed = false;
152152
try {
153153
if (this.workspaceState) {
154-
if (this.workspaceState.isWorkspaceRunning()) {
154+
if (this.workspaceState.isWorkspaceRunning) {
155155
if (!wasClosed) {
156156
await this.sessionService.getAPI().sendHeartbeat(this.connectionInfo.workspaceId);
157157
this.logService.trace(`Send heartbeat, triggered by ${this.lastActivityEvent} event`);
@@ -234,7 +234,7 @@ export class HeartbeatManager extends Disposable {
234234
super.dispose();
235235
this.stopIDEHeartbeatTelemetry();
236236
this.stopHeartbeat();
237-
if (this.workspaceState?.isWorkspaceRunning() ?? this.isWorkspaceRunning) {
237+
if (this.workspaceState?.isWorkspaceRunning ?? this.isWorkspaceRunning) {
238238
this.sendIDEHeartbeatTelemetry();
239239
await this.sendHeartBeat(true);
240240
}

src/local-ssh/proxy.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { NodeStream, SshClientCredentials, SshClientSession, SshDisconnectReason
88
import { importKey, importKeyBytes } from '@microsoft/dev-tunnels-ssh-keys';
99
import { ExtensionServiceDefinition, GetWorkspaceAuthInfoResponse } from '../proto/typescript/ipc/v1/ipc';
1010
import { Client, ClientError, Status, createChannel, createClient } from 'nice-grpc';
11-
import { retry } from '../common/async';
11+
import { retry, timeout } from '../common/async';
1212
import { WebSocket } from 'ws';
1313
import * as stream from 'stream';
1414
import { ILogService } from '../services/logService';
@@ -154,6 +154,9 @@ class WebSocketSSHProxy {
154154
this.sendErrorReport(this.flow, err, 'failed to authenticate proxy with username: ' + e.username ?? '');
155155
}
156156

157+
// Await a few seconds to delay showing ssh extension error modal dialog
158+
await timeout(5000);
159+
157160
this.logService.error('failed to authenticate proxy with username: ' + e.username ?? '', err);
158161
await session.close(SshDisconnectReason.byApplication, err.toString(), err instanceof Error ? err : undefined);
159162
return null;

src/publicApi.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import * as vscode from 'vscode';
1414
import { Disposable } from './common/dispose';
1515
import { WorkspacesServiceClient, WorkspaceStatus } from './lib/gitpod/experimental/v1/workspaces.pb';
1616
import * as grpc from '@grpc/grpc-js';
17+
import { getErrorCode } from '@grpc/grpc-js/build/src/error';
1718
import { timeout } from './common/async';
1819
import { MetricsReporter, getConnectMetricsInterceptor, getGrpcMetricsInterceptor } from './metrics';
1920
import { ILogService } from './services/logService';
@@ -33,7 +34,8 @@ function isTelemetryEnabled(): boolean {
3334
}
3435

3536
export interface IGitpodAPI {
36-
getWorkspace(workspaceId: string): Promise<Workspace | undefined>;
37+
getWorkspace(workspaceId: string): Promise<Workspace>;
38+
startWorkspace(workspaceId: string): Promise<Workspace>;
3739
getOwnerToken(workspaceId: string): Promise<string>;
3840
getSSHKeys(): Promise<SSHKey[]>;
3941
sendHeartbeat(workspaceId: string): Promise<void>;
@@ -91,10 +93,14 @@ export class GitpodPublicApi extends Disposable implements IGitpodAPI {
9193
this.metricsReporter.startReporting();
9294
}
9395
}
94-
95-
async getWorkspace(workspaceId: string): Promise<Workspace | undefined> {
96+
async getWorkspace(workspaceId: string): Promise<Workspace> {
9697
const response = await this.workspaceService.getWorkspace({ workspaceId });
97-
return response.result;
98+
return response.result!;
99+
}
100+
101+
async startWorkspace(workspaceId: string): Promise<Workspace> {
102+
const response = await this.workspaceService.startWorkspace({ workspaceId });
103+
return response.result!;
98104
}
99105

100106
async getOwnerToken(workspaceId: string): Promise<string> {
@@ -131,7 +137,16 @@ export class GitpodPublicApi extends Disposable implements IGitpodAPI {
131137
clearTimeout(stopTimer);
132138
await timeout(1000);
133139
if (isDisposed) { return; }
134-
[stream, stopTimer] = this._streamWorkspaceStatus(workspaceId, emitter, onStreamEnd);
140+
this.grpcWorkspaceClient.getWorkspace({ workspaceId }, this.grpcMetadata, (err, resp) => {
141+
if (isDisposed) { return; }
142+
if (err) {
143+
this.logger.error(`Error in streamWorkspaceStatus(getWorkspace) for ${workspaceId}`, err);
144+
onStreamEnd();
145+
return;
146+
}
147+
emitter.fire(resp.result!.status!);
148+
[stream, stopTimer] = this._streamWorkspaceStatus(workspaceId, emitter, onStreamEnd);
149+
});
135150
};
136151
let [stream, stopTimer] = this._streamWorkspaceStatus(workspaceId, emitter, onStreamEnd);
137152

@@ -165,7 +180,9 @@ export class GitpodPublicApi extends Disposable implements IGitpodAPI {
165180
onStreamEnd();
166181
});
167182
stream.on('error', (err) => {
168-
this.logger.trace(`Error in streamWorkspaceStatus for ${workspaceId}`, err);
183+
if (getErrorCode(err) !== grpc.status.CANCELLED) {
184+
this.logger.error(`Error in streamWorkspaceStatus for ${workspaceId}`, err);
185+
}
169186
});
170187

171188
// force reconnect after 7m to avoid unexpected 10m reconnection (internal error)

src/remote.ts

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7-
import { UserFlowTelemetryProperties } from './common/telemetry';
8-
import { INotificationService } from './services/notificationService';
9-
import { ILogService } from './services/logService';
107

118
export interface SSHConnectionParams {
129
workspaceId: string;
@@ -16,11 +13,6 @@ export interface SSHConnectionParams {
1613
connType?: 'local-app' | 'local-ssh' | 'ssh-gateway';
1714
}
1815

19-
export interface WorkspaceRestartInfo {
20-
workspaceId: string;
21-
gitpodHost: string;
22-
}
23-
2416
export class NoRunningInstanceError extends Error {
2517
code = 'NoRunningInstanceError';
2618
constructor(readonly workspaceId: string, readonly phase?: string) {
@@ -51,37 +43,19 @@ export class NoLocalSSHSupportError extends Error {
5143

5244
export const SSH_DEST_KEY = 'ssh-dest:';
5345

54-
export function getGitpodRemoteWindowConnectionInfo(context: vscode.ExtensionContext): { remoteAuthority: string; connectionInfo: SSHConnectionParams } | undefined {
46+
export function getGitpodRemoteWindowConnectionInfo(context: vscode.ExtensionContext): { connectionInfo: SSHConnectionParams; remoteUri: vscode.Uri; sshDestStr:string } | undefined {
5547
const remoteUri = vscode.workspace.workspaceFile || vscode.workspace.workspaceFolders?.[0].uri;
5648
if (vscode.env.remoteName === 'ssh-remote' && context.extension.extensionKind === vscode.ExtensionKind.UI && remoteUri) {
5749
const [, sshDestStr] = remoteUri.authority.split('+');
5850
const connectionInfo = context.globalState.get<SSHConnectionParams>(`${SSH_DEST_KEY}${sshDestStr}`);
5951
if (connectionInfo) {
60-
return { remoteAuthority: remoteUri.authority, connectionInfo };
52+
return { connectionInfo, remoteUri, sshDestStr };
6153
}
6254
}
6355

6456
return undefined;
6557
}
6658

67-
export async function showWsNotRunningDialog(workspaceId: string, gitpodHost: string, flow: UserFlowTelemetryProperties, notificationService: INotificationService, logService: ILogService) {
68-
const msg = `Workspace ${workspaceId} is not running. Please restart the workspace.`;
69-
logService.error(msg);
70-
71-
const workspaceUrl = new URL(gitpodHost);
72-
workspaceUrl.pathname = '/start';
73-
workspaceUrl.hash = workspaceId;
74-
75-
const openUrl = 'Restart workspace';
76-
const resp = await notificationService.showErrorMessage(msg, { id: 'ws_not_running', flow, modal: true }, openUrl);
77-
if (resp === openUrl) {
78-
const opened = await vscode.env.openExternal(vscode.Uri.parse(workspaceUrl.toString()));
79-
if (opened) {
80-
vscode.commands.executeCommand('workbench.action.closeWindow');
81-
}
82-
}
83-
}
84-
8559
export function getLocalSSHDomain(gitpodHost: string): string {
8660
const scope = vscode.env.appName.includes('Insiders') ? 'vsi' : 'vss';
8761
return `${scope}.` + (new URL(gitpodHost)).hostname;

src/remoteConnector.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { ILogService } from './services/logService';
3939
import { IHostService } from './services/hostService';
4040
import { Configuration } from './configuration';
4141
import { WrapError, getServiceURL } from './common/utils';
42-
import { ILocalSSHService } from './services/localSSHService';
42+
import { IRemoteService } from './services/remoteService';
4343

4444
interface LocalAppConfig {
4545
gitpodHost: string;
@@ -110,7 +110,7 @@ export class RemoteConnector extends Disposable {
110110
private readonly logService: ILogService,
111111
private readonly telemetryService: ITelemetryService,
112112
private readonly notificationService: INotificationService,
113-
private readonly localSSHService: ILocalSSHService,
113+
private readonly remoteService: IRemoteService,
114114
) {
115115
super();
116116

@@ -701,10 +701,10 @@ export class RemoteConnector extends Disposable {
701701
// If needed, revert local-app changes first
702702
await this.updateRemoteSSHConfig(true, undefined);
703703

704-
this.localSSHService.flow = sshFlow;
704+
this.remoteService.flow = sshFlow;
705705
const [isSupportLocalSSH, isExtensionServerReady] = await Promise.all([
706-
this.localSSHService.initialize(),
707-
this.localSSHService.extensionServerReady()
706+
this.remoteService.setupSSHProxy(),
707+
this.remoteService.extensionServerReady()
708708
]);
709709
if (!isExtensionServerReady) {
710710
throw new NoExtensionIPCServerError();

0 commit comments

Comments
 (0)