Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initialize remote extensions #16

Merged
merged 4 commits into from
Aug 24, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as vscode from 'vscode';
import Log from './common/logger';
import GitpodAuthenticationProvider from './authentication';
import RemoteConnector from './remoteConnector';
import SettingsSync from './settingsSync';
import { SettingsSync } from './settingsSync';
import GitpodServer from './gitpodServer';
import TelemetryReporter from './telemetryReporter';
import { exportLogs } from './exportLogs';
Expand Down Expand Up @@ -42,10 +42,11 @@ export async function activate(context: vscode.ExtensionContext) {
}
}));

context.subscriptions.push(new SettingsSync(logger, telemetry));
const settingsSync = new SettingsSync(logger, telemetry);
context.subscriptions.push(settingsSync);

const authProvider = new GitpodAuthenticationProvider(context, logger, telemetry);
remoteConnector = new RemoteConnector(context, experiments, logger, telemetry);
remoteConnector = new RemoteConnector(context, settingsSync, experiments, logger, telemetry);
context.subscriptions.push(authProvider);
context.subscriptions.push(vscode.window.registerUriHandler({
handleUri(uri: vscode.Uri) {
Expand Down
123 changes: 92 additions & 31 deletions src/remoteConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ import TelemetryReporter from './telemetryReporter';
import { addHostToHostFile, checkNewHostInHostkeys } from './ssh/hostfile';
import { DEFAULT_IDENTITY_FILES } from './ssh/identityFiles';
import { HeartbeatManager } from './heartbeat';
import { getGitpodVersion, isFeatureSupported } from './featureSupport';
import { getGitpodVersion, GitpodVersion, isFeatureSupported } from './featureSupport';
import SSHConfiguration from './ssh/sshConfig';
import { isWindows } from './common/platform';
import { untildify } from './common/files';
import { ExperimentalSettings, isUserOverrideSetting } from './experiments';
import { ISyncExtension, NoSettingsSyncSession, NoSyncStoreError, parseSyncData, SettingsSync, SyncResource } from './settingsSync';
import { retry } from './common/async';

interface SSHConnectionParams {
workspaceId: string;
Expand Down Expand Up @@ -121,6 +123,7 @@ export default class RemoteConnector extends Disposable {

constructor(
private readonly context: vscode.ExtensionContext,
private readonly settingsSync: SettingsSync,
private readonly experiments: ExperimentalSettings,
private readonly logger: Log,
private readonly telemetry: TelemetryReporter
Expand Down Expand Up @@ -900,12 +903,96 @@ export default class RemoteConnector extends Disposable {
}
}

private startHeartBeat(accessToken: string, connectionInfo: SSHConnectionParams) {
private async startHeartBeat(accessToken: string, connectionInfo: SSHConnectionParams, gitpodVersion: GitpodVersion) {
if (this.heartbeatManager) {
return;
}

this.heartbeatManager = new HeartbeatManager(connectionInfo.gitpodHost, connectionInfo.workspaceId, connectionInfo.instanceId, accessToken, this.logger, this.telemetry);

// gitpod remote extension installation is async so sometimes gitpod-desktop will activate before gitpod-remote
// let's try a few times for it to finish install
try {
await retry(async () => {
await vscode.commands.executeCommand('__gitpod.cancelGitpodRemoteHeartbeat');
}, 3000, 15);
this.telemetry.sendTelemetryEvent('vscode_desktop_heartbeat_state', { enabled: String(true), gitpodHost: connectionInfo.gitpodHost, workspaceId: connectionInfo.workspaceId, instanceId: connectionInfo.instanceId, gitpodVersion: gitpodVersion.raw });
} catch {
this.logger.error(`Could not execute '__gitpod.cancelGitpodRemoteHeartbeat' command`);
this.telemetry.sendTelemetryEvent('vscode_desktop_heartbeat_state', { enabled: String(false), gitpodHost: connectionInfo.gitpodHost, workspaceId: connectionInfo.workspaceId, instanceId: connectionInfo.instanceId, gitpodVersion: gitpodVersion.raw });
}
}

private async initializeRemoteExtensions() {
let syncData: { ref: string; content: string } | undefined;
try {
syncData = await this.settingsSync.readResource(SyncResource.Extensions);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder should not we rather provider gitpodHost here from connectionInfo instead of reading from settings. I think for remote window connectionInfo is source of the truth.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Settings sync config is generated from gitpod.host config not gitpodHost, ideally they are the same but it's possible they are not

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were discussing yesterday with self hosting team that having different configurations per host will be good, i.e. one should be able to specify different values for different hosts. Like use SSH Gateway for gitpod.io, but local app for SH solution and so on. How we designed it with one host does not really fit to it.

} catch (e) {
if (e instanceof NoSyncStoreError) {
const action = 'Settings Sync: Enable Sign In with Gitpod';
const result = await vscode.window.showInformationMessage(`Couldn't initialize remote extensions, Settings Sync with Gitpod is required.`, action);
if (result === action) {
vscode.commands.executeCommand('gitpod.syncProvider.add');
}
} else if (e instanceof NoSettingsSyncSession) {
const action = 'Enable Settings Sync';
const result = await vscode.window.showInformationMessage(`Couldn't initialize remote extensions, please enable Settings Sync.`, action);
if (result === action) {
vscode.commands.executeCommand('workbench.userDataSync.actions.turnOn');
}
} else {
this.logger.error('Error while fetching settings sync extension data:', e);

const seeLogs = 'See Logs';
const action = await vscode.window.showErrorMessage(`Error while fetching settings sync extension data.`, seeLogs);
if (action === seeLogs) {
this.logger.show();
}
}
return;
}

const syncDataContent = parseSyncData(syncData.content);
if (!syncDataContent) {
this.logger.error('Error while parsing sync data');
return;
}

let extensions: ISyncExtension[];
try {
extensions = JSON.parse(syncDataContent.content);
} catch {
jeanp413 marked this conversation as resolved.
Show resolved Hide resolved
this.logger.error('Error while parsing settings sync extension data, malformed json');
return;
}

extensions = extensions.filter(e => e.installed);
if (!extensions.length) {
return;
}

try {
await vscode.window.withProgress<void>({
title: 'Installing extensions on remote',
location: vscode.ProgressLocation.Notification
}, async () => {
try {
this.logger.trace(`Installing extensions on remote: `, extensions.map(e => e.identifier.id).join('\n'));
await retry(async () => {
await vscode.commands.executeCommand('__gitpod.initializeRemoteExtensions', extensions);
}, 3000, 15);
} catch (e) {
this.logger.error(`Could not execute '__gitpod.initializeRemoteExtensions' command`);
throw e;
}
});
} catch {
const seeLogs = 'See Logs';
const action = await vscode.window.showErrorMessage(`Error while installing extensions on remote.`, seeLogs);
if (action === seeLogs) {
this.logger.show();
}
}
}

private async onGitpodRemoteConnection() {
Expand Down Expand Up @@ -939,27 +1026,12 @@ export default class RemoteConnector extends Disposable {

const gitpodVersion = await getGitpodVersion(connectionInfo.gitpodHost, this.logger);
if (isFeatureSupported(gitpodVersion, 'localHeartbeat')) {
// gitpod remote extension installation is async so sometimes gitpod-desktop will activate before gitpod-remote
// let's try a few times for it to finish install
let retryCount = 15;
const tryStopRemoteHeartbeat = async () => {
// Check for gitpod remote extension version to avoid sending heartbeat in both extensions at the same time
const isGitpodRemoteHeartbeatCancelled = await cancelGitpodRemoteHeartbeat();
if (isGitpodRemoteHeartbeatCancelled) {
this.telemetry.sendTelemetryEvent('vscode_desktop_heartbeat_state', { enabled: String(true), gitpodHost: connectionInfo.gitpodHost, workspaceId: connectionInfo.workspaceId, instanceId: connectionInfo.instanceId, gitpodVersion: gitpodVersion.raw });
} else if (retryCount > 0) {
retryCount--;
setTimeout(tryStopRemoteHeartbeat, 3000);
} else {
this.telemetry.sendTelemetryEvent('vscode_desktop_heartbeat_state', { enabled: String(false), gitpodHost: connectionInfo.gitpodHost, workspaceId: connectionInfo.workspaceId, instanceId: connectionInfo.instanceId, gitpodVersion: gitpodVersion.raw });
}
};

this.startHeartBeat(session.accessToken, connectionInfo);
tryStopRemoteHeartbeat();
this.startHeartBeat(session.accessToken, connectionInfo, gitpodVersion);
} else {
this.logger.warn(`Local heatbeat not supported in ${connectionInfo.gitpodHost}, using version ${gitpodVersion.version}`);
}

this.initializeRemoteExtensions();
}

public override async dispose(): Promise<void> {
Expand All @@ -980,17 +1052,6 @@ function isGitpodRemoteWindow(context: vscode.ExtensionContext) {
return false;
}

async function cancelGitpodRemoteHeartbeat() {
let result = false;
try {
// Invoke command from gitpot-remote extension
result = await vscode.commands.executeCommand('__gitpod.cancelGitpodRemoteHeartbeat');
} catch {
// Ignore if not found
}
return result;
}

function getServiceURL(gitpodHost: string): string {
return new URL(gitpodHost).toString().replace(/\/$/, '');
}
Loading