Archived
1
0

feat: apply patch after setting up subtree

This commit is contained in:
Joe Previte
2020-12-15 15:53:52 -07:00
parent 41bee49d07
commit 51a2a2ad2d
84 changed files with 3360 additions and 191 deletions

View File

@ -0,0 +1,241 @@
import { Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { Registry } from 'vs/platform/registry/common/platform';
import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { INodeProxyService, NodeProxyChannelClient } from 'vs/server/common/nodeProxy';
import { TelemetryChannelClient } from 'vs/server/common/telemetry';
import 'vs/workbench/contrib/localizations/browser/localizations.contribution';
import { LocalizationsService } from 'vs/workbench/services/localizations/electron-browser/localizationsService';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { Options } from 'vs/server/ipc.d';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { ILogService } from 'vs/platform/log/common/log';
import * as path from 'vs/base/common/path';
class TelemetryService extends TelemetryChannelClient {
public constructor(
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
) {
super(remoteAgentService.getConnection()!.getChannel('telemetry'));
}
}
/**
* Remove extra slashes in a URL.
*/
export const normalize = (url: string, keepTrailing = false): string => {
return url.replace(/\/\/+/g, '/').replace(/\/+$/, keepTrailing ? '/' : '');
};
/**
* Get options embedded in the HTML.
*/
export const getOptions = <T extends Options>(): T => {
try {
return JSON.parse(document.getElementById('coder-options')!.getAttribute('data-settings')!);
} catch (error) {
return {} as T;
}
};
const options = getOptions();
const TELEMETRY_SECTION_ID = 'telemetry';
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
'id': TELEMETRY_SECTION_ID,
'order': 110,
'type': 'object',
'title': localize('telemetryConfigurationTitle', 'Telemetry'),
'properties': {
'telemetry.enableTelemetry': {
'type': 'boolean',
'description': localize('telemetry.enableTelemetry', 'Enable usage data and errors to be sent to a Microsoft online service.'),
'default': !options.disableTelemetry,
'tags': ['usesOnlineServices']
}
}
});
class NodeProxyService extends NodeProxyChannelClient implements INodeProxyService {
private readonly _onClose = new Emitter<void>();
public readonly onClose = this._onClose.event;
private readonly _onDown = new Emitter<void>();
public readonly onDown = this._onDown.event;
private readonly _onUp = new Emitter<void>();
public readonly onUp = this._onUp.event;
public constructor(
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
) {
super(remoteAgentService.getConnection()!.getChannel('nodeProxy'));
remoteAgentService.getConnection()!.onDidStateChange((state) => {
switch (state.type) {
case PersistentConnectionEventType.ConnectionGain:
return this._onUp.fire();
case PersistentConnectionEventType.ConnectionLost:
return this._onDown.fire();
case PersistentConnectionEventType.ReconnectionPermanentFailure:
return this._onClose.fire();
}
});
}
}
registerSingleton(ILocalizationsService, LocalizationsService);
registerSingleton(INodeProxyService, NodeProxyService);
registerSingleton(ITelemetryService, TelemetryService);
/**
* This is called by vs/workbench/browser/web.main.ts after the workbench has
* been initialized so we can initialize our own client-side code.
*/
export const initialize = async (services: ServiceCollection): Promise<void> => {
const event = new CustomEvent('ide-ready');
window.dispatchEvent(event);
if (parent) {
// Tell the parent loading has completed.
parent.postMessage({ event: 'loaded' }, window.location.origin);
// Proxy or stop proxing events as requested by the parent.
const listeners = new Map<string, (event: Event) => void>();
window.addEventListener('message', (parentEvent) => {
const eventName = parentEvent.data.bind || parentEvent.data.unbind;
if (eventName) {
const oldListener = listeners.get(eventName);
if (oldListener) {
document.removeEventListener(eventName, oldListener);
}
}
if (parentEvent.data.bind && parentEvent.data.prop) {
const listener = (event: Event) => {
parent.postMessage({
event: parentEvent.data.event,
[parentEvent.data.prop]: event[parentEvent.data.prop as keyof Event]
}, window.location.origin);
};
listeners.set(parentEvent.data.bind, listener);
document.addEventListener(parentEvent.data.bind, listener);
}
});
}
if (!window.isSecureContext) {
(services.get(INotificationService) as INotificationService).notify({
severity: Severity.Warning,
message: 'code-server is being accessed over an insecure domain. Web views, the clipboard, and other functionality will not work as expected.',
actions: {
primary: [{
id: 'understand',
label: 'I understand',
tooltip: '',
class: undefined,
enabled: true,
checked: true,
dispose: () => undefined,
run: () => {
return Promise.resolve();
}
}],
}
});
}
const logService = (services.get(ILogService) as ILogService);
const storageService = (services.get(IStorageService) as IStorageService);
const updateCheckEndpoint = path.join(options.base, '/update/check');
const getUpdate = async (): Promise<void> => {
logService.debug('Checking for update...');
const response = await fetch(updateCheckEndpoint, {
headers: { 'Accept': 'application/json' },
});
if (!response.ok) {
throw new Error(response.statusText);
}
const json = await response.json();
if (json.error) {
throw new Error(json.error);
}
if (json.isLatest) {
return;
}
const lastNoti = storageService.getNumber('csLastUpdateNotification', StorageScope.GLOBAL);
if (lastNoti) {
// Only remind them again after 1 week.
const timeout = 1000*60*60*24*7;
const threshold = lastNoti + timeout;
if (Date.now() < threshold) {
return;
}
}
storageService.store('csLastUpdateNotification', Date.now(), StorageScope.GLOBAL);
(services.get(INotificationService) as INotificationService).notify({
severity: Severity.Info,
message: `[code-server v${json.latest}](https://github.com/cdr/code-server/releases/tag/v${json.latest}) has been released!`,
});
};
const updateLoop = (): void => {
getUpdate().catch((error) => {
logService.debug(`failed to check for update: ${error}`);
}).finally(() => {
// Check again every 6 hours.
setTimeout(updateLoop, 1000*60*60*6);
});
};
if (!options.disableUpdateCheck) {
updateLoop();
}
// This will be used to set the background color while VS Code loads.
const theme = storageService.get('colorThemeData', StorageScope.GLOBAL);
if (theme) {
localStorage.setItem('colorThemeData', theme);
}
};
export interface Query {
[key: string]: string | undefined;
}
/**
* Split a string up to the delimiter. If the delimiter doesn't exist the first
* item will have all the text and the second item will be an empty string.
*/
export const split = (str: string, delimiter: string): [string, string] => {
const index = str.indexOf(delimiter);
return index !== -1 ? [str.substring(0, index).trim(), str.substring(index + 1)] : [str, ''];
};
/**
* Return the URL modified with the specified query variables. It's pretty
* stupid so it probably doesn't cover any edge cases. Undefined values will
* unset existing values. Doesn't allow duplicates.
*/
export const withQuery = (url: string, replace: Query): string => {
const uri = URI.parse(url);
const query = { ...replace };
uri.query.split('&').forEach((kv) => {
const [key, value] = split(kv, '=');
if (!(key in query)) {
query[key] = value;
}
});
return uri.with({
query: Object.keys(query)
.filter((k) => typeof query[k] !== 'undefined')
.map((k) => `${k}=${query[k]}`).join('&'),
}).toString(true);
};

View File

@ -0,0 +1,51 @@
import { Emitter } from 'vs/base/common/event';
import { UriComponents } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ExtHostNodeProxyShape, MainContext, MainThreadNodeProxyShape } from 'vs/workbench/api/common/extHost.protocol';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
export class ExtHostNodeProxy implements ExtHostNodeProxyShape {
_serviceBrand: any;
private readonly _onMessage = new Emitter<string>();
public readonly onMessage = this._onMessage.event;
private readonly _onClose = new Emitter<void>();
public readonly onClose = this._onClose.event;
private readonly _onDown = new Emitter<void>();
public readonly onDown = this._onDown.event;
private readonly _onUp = new Emitter<void>();
public readonly onUp = this._onUp.event;
private readonly proxy: MainThreadNodeProxyShape;
constructor(@IExtHostRpcService rpc: IExtHostRpcService) {
this.proxy = rpc.getProxy(MainContext.MainThreadNodeProxy);
}
public $onMessage(message: string): void {
this._onMessage.fire(message);
}
public $onClose(): void {
this._onClose.fire();
}
public $onUp(): void {
this._onUp.fire();
}
public $onDown(): void {
this._onDown.fire();
}
public send(message: string): void {
this.proxy.$send(message);
}
public async fetchExtension(extensionUri: UriComponents): Promise<Uint8Array> {
return this.proxy.$fetchExtension(extensionUri).then(b => b.buffer);
}
}
export interface IExtHostNodeProxy extends ExtHostNodeProxy { }
export const IExtHostNodeProxy = createDecorator<IExtHostNodeProxy>('IExtHostNodeProxy');

View File

@ -0,0 +1,55 @@
import { VSBuffer } from 'vs/base/common/buffer';
import { IDisposable } from 'vs/base/common/lifecycle';
import { FileAccess } from 'vs/base/common/network';
import { URI, UriComponents } from 'vs/base/common/uri';
import { INodeProxyService } from 'vs/server/common/nodeProxy';
import { ExtHostContext, IExtHostContext, MainContext, MainThreadNodeProxyShape } from 'vs/workbench/api/common/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
@extHostNamedCustomer(MainContext.MainThreadNodeProxy)
export class MainThreadNodeProxy implements MainThreadNodeProxyShape {
private disposed = false;
private disposables = <IDisposable[]>[];
constructor(
extHostContext: IExtHostContext,
@INodeProxyService private readonly proxyService: INodeProxyService,
) {
if (!extHostContext.remoteAuthority) { // HACK: A terrible way to detect if running in the worker.
const proxy = extHostContext.getProxy(ExtHostContext.ExtHostNodeProxy);
this.disposables = [
this.proxyService.onMessage((message: string) => proxy.$onMessage(message)),
this.proxyService.onClose(() => proxy.$onClose()),
this.proxyService.onDown(() => proxy.$onDown()),
this.proxyService.onUp(() => proxy.$onUp()),
];
}
}
$send(message: string): void {
if (!this.disposed) {
this.proxyService.send(message);
}
}
async $fetchExtension(extensionUri: UriComponents): Promise<VSBuffer> {
const fetchUri = URI.from({
scheme: window.location.protocol.replace(':', ''),
authority: window.location.host,
// Use FileAccess to get the static base path.
path: FileAccess.asBrowserUri('', require).path,
query: `tar=${encodeURIComponent(extensionUri.path)}`,
});
const response = await fetch(fetchUri.toString(true));
if (response.status !== 200) {
throw new Error(`Failed to download extension "${module}"`);
}
return VSBuffer.wrap(new Uint8Array(await response.arrayBuffer()));
}
dispose(): void {
this.disposables.forEach((d) => d.dispose());
this.disposables = [];
this.disposed = true;
}
}

View File

@ -0,0 +1,48 @@
import { Client } from '@coder/node-browser';
import { fromTar } from '@coder/requirefs';
import { URI } from 'vs/base/common/uri';
import { ILogService } from 'vs/platform/log/common/log';
import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHostExtensionActivator';
import { IExtHostNodeProxy } from './extHostNodeProxy';
export const loadCommonJSModule = async <T>(
module: URI,
activationTimesBuilder: ExtensionActivationTimesBuilder,
nodeProxy: IExtHostNodeProxy,
logService: ILogService,
vscode: any,
): Promise<T> => {
const client = new Client(nodeProxy, { logger: logService });
const [buffer, init] = await Promise.all([
nodeProxy.fetchExtension(module),
client.handshake(),
]);
const rfs = fromTar(buffer);
(<any>self).global = self;
rfs.provide('vscode', vscode);
Object.keys(client.modules).forEach((key) => {
const mod = (client.modules as any)[key];
if (key === 'process') {
(<any>self).process = mod;
(<any>self).process.env = init.env;
return;
}
rfs.provide(key, mod);
switch (key) {
case 'buffer':
(<any>self).Buffer = mod.Buffer;
break;
case 'timers':
(<any>self).setImmediate = mod.setImmediate;
break;
}
});
try {
activationTimesBuilder.codeLoadingStart();
return rfs.require('.');
} finally {
activationTimesBuilder.codeLoadingStop();
}
};

View File

@ -0,0 +1,47 @@
import { ReadWriteConnection } from '@coder/node-browser';
import { Event } from 'vs/base/common/event';
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const INodeProxyService = createDecorator<INodeProxyService>('nodeProxyService');
export interface INodeProxyService extends ReadWriteConnection {
_serviceBrand: any;
send(message: string): void;
onMessage: Event<string>;
onUp: Event<void>;
onClose: Event<void>;
onDown: Event<void>;
}
export class NodeProxyChannel implements IServerChannel {
constructor(private service: INodeProxyService) {}
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onMessage': return this.service.onMessage;
}
throw new Error(`Invalid listen ${event}`);
}
async call(_: unknown, command: string, args?: any): Promise<any> {
switch (command) {
case 'send': return this.service.send(args[0]);
}
throw new Error(`Invalid call ${command}`);
}
}
export class NodeProxyChannelClient {
_serviceBrand: any;
public readonly onMessage: Event<string>;
constructor(private readonly channel: IChannel) {
this.onMessage = this.channel.listen<string>('onMessage');
}
public send(data: string): void {
this.channel.call('send', [data]);
}
}

View File

@ -0,0 +1,65 @@
import { ITelemetryData } from 'vs/base/common/actions';
import { Event } from 'vs/base/common/event';
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings';
import { ITelemetryInfo, ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
export class TelemetryChannel implements IServerChannel {
constructor(private service: ITelemetryService) {}
listen(_: unknown, event: string): Event<any> {
throw new Error(`Invalid listen ${event}`);
}
call(_: unknown, command: string, args?: any): Promise<any> {
switch (command) {
case 'publicLog': return this.service.publicLog(args[0], args[1], args[2]);
case 'publicLog2': return this.service.publicLog2(args[0], args[1], args[2]);
case 'publicLogError': return this.service.publicLogError(args[0], args[1]);
case 'publicLogError2': return this.service.publicLogError2(args[0], args[1]);
case 'setEnabled': return Promise.resolve(this.service.setEnabled(args[0]));
case 'getTelemetryInfo': return this.service.getTelemetryInfo();
case 'setExperimentProperty': return Promise.resolve(this.service.setExperimentProperty(args[0], args[1]));
}
throw new Error(`Invalid call ${command}`);
}
}
export class TelemetryChannelClient implements ITelemetryService {
_serviceBrand: any;
// These don't matter; telemetry is sent to the Node side which decides
// whether to send the telemetry event.
public isOptedIn = true;
public sendErrorTelemetry = true;
constructor(private readonly channel: IChannel) {}
public publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise<void> {
return this.channel.call('publicLog', [eventName, data, anonymizeFilePaths]);
}
public publicLog2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>, anonymizeFilePaths?: boolean): Promise<void> {
return this.channel.call('publicLog2', [eventName, data, anonymizeFilePaths]);
}
public publicLogError(errorEventName: string, data?: ITelemetryData): Promise<void> {
return this.channel.call('publicLogError', [errorEventName, data]);
}
public publicLogError2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>): Promise<void> {
return this.channel.call('publicLogError2', [eventName, data]);
}
public setEnabled(value: boolean): void {
this.channel.call('setEnable', [value]);
}
public getTelemetryInfo(): Promise<ITelemetryInfo> {
return this.channel.call('getTelemetryInfo');
}
public setExperimentProperty(name: string, value: string): void {
this.channel.call('setExperimentProperty', [name, value]);
}
}

View File

@ -0,0 +1,81 @@
import { field } from '@coder/logger';
import { setUnexpectedErrorHandler } from 'vs/base/common/errors';
import { CodeServerMessage, VscodeMessage } from 'vs/server/ipc';
import { logger } from 'vs/server/node/logger';
import { enableCustomMarketplace } from 'vs/server/node/marketplace';
import { Vscode } from 'vs/server/node/server';
import * as proxyAgent from 'vs/base/node/proxy_agent';
setUnexpectedErrorHandler((error) => logger.warn(error instanceof Error ? error.message : error));
enableCustomMarketplace();
proxyAgent.monkeyPatch(true);
/**
* Ensure we control when the process exits.
*/
const exit = process.exit;
process.exit = function(code?: number) {
logger.warn(`process.exit() was prevented: ${code || 'unknown code'}.`);
} as (code?: number) => never;
// Kill VS Code if the parent process dies.
if (typeof process.env.CODE_SERVER_PARENT_PID !== 'undefined') {
const parentPid = parseInt(process.env.CODE_SERVER_PARENT_PID, 10);
setInterval(() => {
try {
process.kill(parentPid, 0); // Throws an exception if the process doesn't exist anymore.
} catch (e) {
exit();
}
}, 5000);
} else {
logger.error('no parent process');
exit(1);
}
const vscode = new Vscode();
const send = (message: VscodeMessage): void => {
if (!process.send) {
throw new Error('not spawned with IPC');
}
process.send(message);
};
// Wait for the init message then start up VS Code. Subsequent messages will
// return new workbench options without starting a new instance.
process.on('message', async (message: CodeServerMessage, socket) => {
logger.debug('got message from code-server', field('type', message.type));
logger.trace('code-server message content', field('message', message));
switch (message.type) {
case 'init':
try {
const options = await vscode.initialize(message.options);
send({ type: 'options', id: message.id, options });
} catch (error) {
logger.error(error.message);
logger.error(error.stack);
exit(1);
}
break;
case 'cli':
try {
await vscode.cli(message.args);
exit(0);
} catch (error) {
logger.error(error.message);
logger.error(error.stack);
exit(1);
}
break;
case 'socket':
vscode.handleWebSocket(socket, message.query);
break;
}
});
if (!process.send) {
logger.error('not spawned with IPC');
exit(1);
} else {
// This lets the parent know the child is ready to receive messages.
send({ type: 'ready' });
}

View File

@ -0,0 +1,3 @@
// This must be a JS file otherwise when it gets compiled it turns into AMD
// syntax which will not work without the right loader.
require('../../bootstrap-amd').load('vs/server/entry');

140
lib/vscode/src/vs/server/ipc.d.ts vendored Normal file
View File

@ -0,0 +1,140 @@
/**
* External interfaces for integration into code-server over IPC. No vs imports
* should be made in this file.
*/
export interface Options {
base: string
disableTelemetry: boolean
disableUpdateCheck: boolean
}
export interface InitMessage {
type: 'init';
id: string;
options: VscodeOptions;
}
export type Query = { [key: string]: string | string[] | undefined | Query | Query[] };
export interface SocketMessage {
type: 'socket';
query: Query;
}
export interface CliMessage {
type: 'cli';
args: Args;
}
export interface OpenCommandPipeArgs {
type: 'open';
fileURIs?: string[];
folderURIs: string[];
forceNewWindow?: boolean;
diffMode?: boolean;
addMode?: boolean;
gotoLineMode?: boolean;
forceReuseWindow?: boolean;
waitMarkerFilePath?: string;
}
export type CodeServerMessage = InitMessage | SocketMessage | CliMessage;
export interface ReadyMessage {
type: 'ready';
}
export interface OptionsMessage {
id: string;
type: 'options';
options: WorkbenchOptions;
}
export type VscodeMessage = ReadyMessage | OptionsMessage;
export interface StartPath {
url: string;
workspace: boolean;
}
export interface Args {
'user-data-dir'?: string;
'enable-proposed-api'?: string[];
'extensions-dir'?: string;
'builtin-extensions-dir'?: string;
'extra-extensions-dir'?: string[];
'extra-builtin-extensions-dir'?: string[];
'ignore-last-opened'?: boolean;
locale?: string
log?: string;
verbose?: boolean;
home?: string;
_: string[];
}
export interface VscodeOptions {
readonly args: Args;
readonly remoteAuthority: string;
readonly startPath?: StartPath;
}
export interface VscodeOptionsMessage extends VscodeOptions {
readonly id: string;
}
export interface UriComponents {
readonly scheme: string;
readonly authority: string;
readonly path: string;
readonly query: string;
readonly fragment: string;
}
export interface NLSConfiguration {
locale: string;
availableLanguages: {
[key: string]: string;
};
pseudo?: boolean;
_languagePackSupport?: boolean;
}
export interface WorkbenchOptions {
readonly workbenchWebConfiguration: {
readonly remoteAuthority?: string;
readonly folderUri?: UriComponents;
readonly workspaceUri?: UriComponents;
readonly logLevel?: number;
readonly workspaceProvider?: {
payload: [
['userDataPath', string],
['enableProposedApi', string],
];
};
readonly homeIndicator?: {
href: string,
icon: string,
title: string,
},
};
readonly remoteUserDataUri: UriComponents;
readonly productConfiguration: {
codeServerVersion?: string;
readonly extensionsGallery?: {
readonly serviceUrl: string;
readonly itemUrl: string;
readonly controlUrl: string;
readonly recommendationsUrl: string;
};
};
readonly nlsConfiguration: NLSConfiguration;
readonly commit: string;
}
export interface WorkbenchOptionsMessage {
id: string;
}

View File

@ -0,0 +1,906 @@
import { field, logger } from '@coder/logger';
import { Server } from '@coder/node-browser';
import * as os from 'os';
import * as path from 'path';
import { VSBuffer } from 'vs/base/common/buffer';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as platform from 'vs/base/common/platform';
import * as resources from 'vs/base/common/resources';
import { ReadableStreamEventPayload } from 'vs/base/common/stream';
import { URI, UriComponents } from 'vs/base/common/uri';
import { transformOutgoingURIs } from 'vs/base/common/uriIpc';
import { IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileReadStreamOptions, FileType, FileWriteOptions, IStat, IWatchOptions } from 'vs/platform/files/common/files';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { ILogService } from 'vs/platform/log/common/log';
import product from 'vs/platform/product/common/product';
import { IRemoteAgentEnvironment, RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { INodeProxyService } from 'vs/server/common/nodeProxy';
import { getTranslations } from 'vs/server/node/nls';
import { getUriTransformer } from 'vs/server/node/util';
import { IFileChangeDto } from 'vs/workbench/api/common/extHost.protocol';
import { IEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection';
import { deserializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared';
import * as terminal from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel';
import { IShellLaunchConfig, ITerminalEnvironment, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering';
import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment';
import { getSystemShell } from 'vs/workbench/contrib/terminal/node/terminal';
import { getMainProcessParentEnv } from 'vs/workbench/contrib/terminal/node/terminalEnvironment';
import { TerminalProcess } from 'vs/workbench/contrib/terminal/node/terminalProcess';
import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver';
import { ExtensionScanner, ExtensionScannerInput } from 'vs/workbench/services/extensions/node/extensionPoints';
/**
* Extend the file provider to allow unwatching.
*/
class Watcher extends DiskFileSystemProvider {
public readonly watches = new Map<number, IDisposable>();
public dispose(): void {
this.watches.forEach((w) => w.dispose());
this.watches.clear();
super.dispose();
}
public _watch(req: number, resource: URI, opts: IWatchOptions): void {
this.watches.set(req, this.watch(resource, opts));
}
public unwatch(req: number): void {
this.watches.get(req)!.dispose();
this.watches.delete(req);
}
}
export class FileProviderChannel implements IServerChannel<RemoteAgentConnectionContext>, IDisposable {
private readonly provider: DiskFileSystemProvider;
private readonly watchers = new Map<string, Watcher>();
public constructor(
private readonly environmentService: INativeEnvironmentService,
private readonly logService: ILogService,
) {
this.provider = new DiskFileSystemProvider(this.logService);
}
public listen(context: RemoteAgentConnectionContext, event: string, args?: any): Event<any> {
switch (event) {
case 'filechange': return this.filechange(context, args[0]);
case 'readFileStream': return this.readFileStream(args[0], args[1]);
}
throw new Error(`Invalid listen '${event}'`);
}
private filechange(context: RemoteAgentConnectionContext, session: string): Event<IFileChangeDto[]> {
const emitter = new Emitter<IFileChangeDto[]>({
onFirstListenerAdd: () => {
const provider = new Watcher(this.logService);
this.watchers.set(session, provider);
const transformer = getUriTransformer(context.remoteAuthority);
provider.onDidChangeFile((events) => {
emitter.fire(events.map((event) => ({
...event,
resource: transformer.transformOutgoing(event.resource),
})));
});
provider.onDidErrorOccur((event) => this.logService.error(event));
},
onLastListenerRemove: () => {
this.watchers.get(session)!.dispose();
this.watchers.delete(session);
},
});
return emitter.event;
}
private readFileStream(resource: UriComponents, opts: FileReadStreamOptions): Event<ReadableStreamEventPayload<VSBuffer>> {
const cts = new CancellationTokenSource();
const fileStream = this.provider.readFileStream(this.transform(resource), opts, cts.token);
const emitter = new Emitter<ReadableStreamEventPayload<VSBuffer>>({
onFirstListenerAdd: () => {
fileStream.on('data', (data) => emitter.fire(VSBuffer.wrap(data)));
fileStream.on('error', (error) => emitter.fire(error));
fileStream.on('end', () => emitter.fire('end'));
},
onLastListenerRemove: () => cts.cancel(),
});
return emitter.event;
}
public call(_: unknown, command: string, args?: any): Promise<any> {
switch (command) {
case 'stat': return this.stat(args[0]);
case 'open': return this.open(args[0], args[1]);
case 'close': return this.close(args[0]);
case 'read': return this.read(args[0], args[1], args[2]);
case 'readFile': return this.readFile(args[0]);
case 'write': return this.write(args[0], args[1], args[2], args[3], args[4]);
case 'writeFile': return this.writeFile(args[0], args[1], args[2]);
case 'delete': return this.delete(args[0], args[1]);
case 'mkdir': return this.mkdir(args[0]);
case 'readdir': return this.readdir(args[0]);
case 'rename': return this.rename(args[0], args[1], args[2]);
case 'copy': return this.copy(args[0], args[1], args[2]);
case 'watch': return this.watch(args[0], args[1], args[2], args[3]);
case 'unwatch': return this.unwatch(args[0], args[1]);
}
throw new Error(`Invalid call '${command}'`);
}
public dispose(): void {
this.watchers.forEach((w) => w.dispose());
this.watchers.clear();
}
private async stat(resource: UriComponents): Promise<IStat> {
return this.provider.stat(this.transform(resource));
}
private async open(resource: UriComponents, opts: FileOpenOptions): Promise<number> {
return this.provider.open(this.transform(resource), opts);
}
private async close(fd: number): Promise<void> {
return this.provider.close(fd);
}
private async read(fd: number, pos: number, length: number): Promise<[VSBuffer, number]> {
const buffer = VSBuffer.alloc(length);
const bytesRead = await this.provider.read(fd, pos, buffer.buffer, 0, length);
return [buffer, bytesRead];
}
private async readFile(resource: UriComponents): Promise<VSBuffer> {
return VSBuffer.wrap(await this.provider.readFile(this.transform(resource)));
}
private write(fd: number, pos: number, buffer: VSBuffer, offset: number, length: number): Promise<number> {
return this.provider.write(fd, pos, buffer.buffer, offset, length);
}
private writeFile(resource: UriComponents, buffer: VSBuffer, opts: FileWriteOptions): Promise<void> {
return this.provider.writeFile(this.transform(resource), buffer.buffer, opts);
}
private async delete(resource: UriComponents, opts: FileDeleteOptions): Promise<void> {
return this.provider.delete(this.transform(resource), opts);
}
private async mkdir(resource: UriComponents): Promise<void> {
return this.provider.mkdir(this.transform(resource));
}
private async readdir(resource: UriComponents): Promise<[string, FileType][]> {
return this.provider.readdir(this.transform(resource));
}
private async rename(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
return this.provider.rename(this.transform(resource), URI.from(target), opts);
}
private copy(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
return this.provider.copy(this.transform(resource), URI.from(target), opts);
}
private async watch(session: string, req: number, resource: UriComponents, opts: IWatchOptions): Promise<void> {
this.watchers.get(session)!._watch(req, this.transform(resource), opts);
}
private async unwatch(session: string, req: number): Promise<void> {
this.watchers.get(session)!.unwatch(req);
}
private transform(resource: UriComponents): URI {
// Used for walkthrough content.
if (/^\/static[^/]*\//.test(resource.path)) {
return URI.file(this.environmentService.appRoot + resource.path.replace(/^\/static[^/]*\//, '/'));
// Used by the webview service worker to load resources.
} else if (resource.path === '/vscode-resource' && resource.query) {
try {
const query = JSON.parse(resource.query);
if (query.requestResourcePath) {
return URI.file(query.requestResourcePath);
}
} catch (error) { /* Carry on. */ }
}
return URI.from(resource);
}
}
// See ../../workbench/services/remote/common/remoteAgentEnvironmentChannel.ts
export class ExtensionEnvironmentChannel implements IServerChannel {
public constructor(
private readonly environment: INativeEnvironmentService,
private readonly log: ILogService,
private readonly telemetry: ITelemetryService,
private readonly connectionToken: string,
) {}
public listen(_: unknown, event: string): Event<any> {
throw new Error(`Invalid listen '${event}'`);
}
public async call(context: any, command: string, args: any): Promise<any> {
switch (command) {
case 'getEnvironmentData':
return transformOutgoingURIs(
await this.getEnvironmentData(),
getUriTransformer(context.remoteAuthority),
);
case 'scanExtensions':
return transformOutgoingURIs(
await this.scanExtensions(args.language),
getUriTransformer(context.remoteAuthority),
);
case 'getDiagnosticInfo': return this.getDiagnosticInfo();
case 'disableTelemetry': return this.disableTelemetry();
case 'logTelemetry': return this.logTelemetry(args[0], args[1]);
case 'flushTelemetry': return this.flushTelemetry();
}
throw new Error(`Invalid call '${command}'`);
}
private async getEnvironmentData(): Promise<IRemoteAgentEnvironment> {
return {
pid: process.pid,
connectionToken: this.connectionToken,
appRoot: URI.file(this.environment.appRoot),
settingsPath: this.environment.settingsResource,
logsPath: URI.file(this.environment.logsPath),
extensionsPath: URI.file(this.environment.extensionsPath!),
extensionHostLogsPath: URI.file(path.join(this.environment.logsPath, 'extension-host')),
globalStorageHome: this.environment.globalStorageHome,
workspaceStorageHome: this.environment.workspaceStorageHome,
userHome: this.environment.userHome,
os: platform.OS,
};
}
private async scanExtensions(language: string): Promise<IExtensionDescription[]> {
const translations = await getTranslations(language, this.environment.userDataPath);
const scanMultiple = (isBuiltin: boolean, isUnderDevelopment: boolean, paths: string[]): Promise<IExtensionDescription[][]> => {
return Promise.all(paths.map((path) => {
return ExtensionScanner.scanExtensions(new ExtensionScannerInput(
product.version,
product.commit,
language,
!!process.env.VSCODE_DEV,
path,
isBuiltin,
isUnderDevelopment,
translations,
), this.log);
}));
};
const scanBuiltin = async (): Promise<IExtensionDescription[][]> => {
return scanMultiple(true, false, [this.environment.builtinExtensionsPath, ...this.environment.extraBuiltinExtensionPaths]);
};
const scanInstalled = async (): Promise<IExtensionDescription[][]> => {
return scanMultiple(false, true, [this.environment.extensionsPath!, ...this.environment.extraExtensionPaths]);
};
return Promise.all([scanBuiltin(), scanInstalled()]).then((allExtensions) => {
const uniqueExtensions = new Map<string, IExtensionDescription>();
allExtensions.forEach((multipleExtensions) => {
multipleExtensions.forEach((extensions) => {
extensions.forEach((extension) => {
const id = ExtensionIdentifier.toKey(extension.identifier);
if (uniqueExtensions.has(id)) {
const oldPath = uniqueExtensions.get(id)!.extensionLocation.fsPath;
const newPath = extension.extensionLocation.fsPath;
this.log.warn(`${oldPath} has been overridden ${newPath}`);
}
uniqueExtensions.set(id, {
...extension,
// Force extensions that should run on the client due to latency
// issues.
extensionKind: extension.identifier.value === 'vscodevim.vim'
? [ 'web' ]
: extension.extensionKind,
});
});
});
});
return Array.from(uniqueExtensions.values());
});
}
private getDiagnosticInfo(): Promise<IDiagnosticInfo> {
throw new Error('not implemented');
}
private async disableTelemetry(): Promise<void> {
this.telemetry.setEnabled(false);
}
private async logTelemetry(eventName: string, data: ITelemetryData): Promise<void> {
this.telemetry.publicLog(eventName, data);
}
private async flushTelemetry(): Promise<void> {
// We always send immediately at the moment.
}
}
export class NodeProxyService implements INodeProxyService {
public _serviceBrand = undefined;
public readonly server: Server;
private readonly _onMessage = new Emitter<string>();
public readonly onMessage = this._onMessage.event;
private readonly _$onMessage = new Emitter<string>();
public readonly $onMessage = this._$onMessage.event;
public readonly _onDown = new Emitter<void>();
public readonly onDown = this._onDown.event;
public readonly _onUp = new Emitter<void>();
public readonly onUp = this._onUp.event;
// Unused because the server connection will never permanently close.
private readonly _onClose = new Emitter<void>();
public readonly onClose = this._onClose.event;
public constructor() {
// TODO: down/up
this.server = new Server({
onMessage: this.$onMessage,
onClose: this.onClose,
onDown: this.onDown,
onUp: this.onUp,
send: (message: string): void => {
this._onMessage.fire(message);
}
});
}
public send(message: string): void {
this._$onMessage.fire(message);
}
}
class VariableResolverService extends AbstractVariableResolverService {
constructor(
remoteAuthority: string,
args: terminal.ICreateTerminalProcessArguments,
env: platform.IProcessEnvironment,
) {
super({
getFolderUri: (name: string): URI | undefined => {
const folder = args.workspaceFolders.find((f) => f.name === name);
return folder && URI.revive(folder.uri);
},
getWorkspaceFolderCount: (): number => {
return args.workspaceFolders.length;
},
// In ../../workbench/contrib/terminal/common/remoteTerminalChannel.ts it
// looks like there are `config:` entries which must be for this? Not sure
// how/if the URI comes into play though.
getConfigurationValue: (_: URI, section: string): string | undefined => {
return args.resolvedVariables[`config:${section}`];
},
getExecPath: (): string | undefined => {
// Assuming that resolverEnv is just for use in the resolver and not for
// the terminal itself.
return (args.resolverEnv && args.resolverEnv['VSCODE_EXEC_PATH']) || env['VSCODE_EXEC_PATH'];
},
// This is just a guess; this is the only file-related thing we're sent
// and none of these resolver methods seem to get called so I don't know
// how to test.
getFilePath: (): string | undefined => {
const resource = transformIncoming(remoteAuthority, args.activeFileResource);
if (!resource) {
return undefined;
}
// See ../../editor/standalone/browser/simpleServices.ts;
// `BaseConfigurationResolverService` calls `getUriLabel` from there.
if (resource.scheme === 'file') {
return resource.fsPath;
}
return resource.path;
},
// It looks like these are set here although they aren't on the types:
// ../../workbench/contrib/terminal/common/remoteTerminalChannel.ts
getSelectedText: (): string | undefined => {
return args.resolvedVariables.selectedText;
},
getLineNumber: (): string | undefined => {
return args.resolvedVariables.selectedText;
},
}, undefined, env);
}
}
class Terminal {
private readonly process: TerminalProcess;
private _pid: number = -1;
private _title: string = '';
public readonly workspaceId: string;
public readonly workspaceName: string;
private readonly persist: boolean;
private readonly _onDispose = new Emitter<void>();
public get onDispose(): Event<void> { return this._onDispose.event; }
private _isOrphan = true;
public get isOrphan(): boolean { return this._isOrphan; }
// These are replayed when a client reconnects.
private cols: number;
private rows: number;
private replayData: string[] = [];
// This is based on string length and is pretty arbitrary.
private readonly maxReplayData = 10000;
private totalReplayData = 0;
// According to the release notes the terminals are supposed to dispose after
// a short timeout; in our case we'll use 48 hours so you can get them back
// the next day or over the weekend.
private disposeTimeout: NodeJS.Timeout | undefined;
private disposeDelay = 48 * 60 * 60 * 1000;
private buffering = false;
private readonly _onEvent = new Emitter<terminal.IRemoteTerminalProcessEvent>({
// Don't bind to data until something is listening.
onFirstListenerAdd: () => {
logger.debug('Terminal bound', field('id', this.id));
this._isOrphan = false;
if (!this.buffering) {
this.buffering = true;
this.bufferer.startBuffering(this.id, this.process.onProcessData);
}
},
// Replay stored events.
onFirstListenerDidAdd: () => {
// We only need to replay if the terminal is being reconnected which is
// true if there is a dispose timeout.
if (typeof this.disposeTimeout !== 'undefined') {
return;
}
clearTimeout(this.disposeTimeout);
this.disposeTimeout = undefined;
logger.debug('Terminal replaying', field('id', this.id));
this._onEvent.fire({
type: 'replay',
events: [{
cols: this.cols,
rows: this.rows,
data: this.replayData.join(''),
}]
});
},
onLastListenerRemove: () => {
logger.debug('Terminal unbound', field('id', this.id));
this._isOrphan = true;
if (!this.persist) { // Used by debug consoles.
this.dispose();
} else {
this.disposeTimeout = setTimeout(() => {
this.dispose();
}, this.disposeDelay);
}
}
});
public get onEvent(): Event<terminal.IRemoteTerminalProcessEvent> { return this._onEvent.event; }
// Buffer to reduce the number of messages going to the renderer.
private readonly bufferer = new TerminalDataBufferer((_, data) => {
this._onEvent.fire({
type: 'data',
data,
});
// No need to store data if we aren't persisting.
if (!this.persist) {
return;
}
this.replayData.push(data);
this.totalReplayData += data.length;
let overflow = this.totalReplayData - this.maxReplayData;
if (overflow <= 0) {
return;
}
// Drop events until doing so would put us under budget.
let deleteCount = 0;
for (; deleteCount < this.replayData.length
&& this.replayData[deleteCount].length <= overflow; ++deleteCount) {
overflow -= this.replayData[deleteCount].length;
}
if (deleteCount > 0) {
this.replayData.splice(0, deleteCount);
}
// Dropping any more events would put us under budget; trim the first event
// instead if still over budget.
if (overflow > 0 && this.replayData.length > 0) {
this.replayData[0] = this.replayData[0].substring(overflow);
}
this.totalReplayData = this.replayData.reduce((p, c) => p + c.length, 0);
});
public get pid(): number {
return this._pid;
}
public get title(): string {
return this._title;
}
public constructor(
public readonly id: number,
config: IShellLaunchConfig & { cwd: string },
args: terminal.ICreateTerminalProcessArguments,
env: platform.IProcessEnvironment,
logService: ILogService,
) {
this.workspaceId = args.workspaceId;
this.workspaceName = args.workspaceName;
this.cols = args.cols;
this.rows = args.rows;
// TODO: Don't persist terminals until we make it work with things like
// htop, vim, etc.
// this.persist = args.shouldPersistTerminal;
this.persist = false;
this.process = new TerminalProcess(
config,
config.cwd,
this.cols,
this.rows,
env,
process.env as platform.IProcessEnvironment, // Environment used for `findExecutable`.
false, // windowsEnableConpty: boolean,
logService,
);
// The current pid and title aren't exposed so they have to be tracked.
this.process.onProcessReady((event) => {
this._pid = event.pid;
this._onEvent.fire({
type: 'ready',
pid: event.pid,
cwd: event.cwd,
});
});
this.process.onProcessTitleChanged((title) => {
this._title = title;
this._onEvent.fire({
type: 'titleChanged',
title,
});
});
this.process.onProcessExit((exitCode) => {
logger.debug('Terminal exited', field('id', this.id), field('code', exitCode));
this._onEvent.fire({
type: 'exit',
exitCode,
});
this.dispose();
});
// TODO: I think `execCommand` must have something to do with running
// commands on the terminal that will do things in VS Code but we already
// have that functionality via a socket so I'm not sure what this is for.
// type: 'execCommand';
// reqId: number;
// commandId: string;
// commandArgs: any[];
// TODO: Maybe this is to ask if the terminal is currently attached to
// anything? But we already know that on account of whether anything is
// listening to our event emitter.
// type: 'orphan?';
}
public dispose() {
logger.debug('Terminal disposing', field('id', this.id));
this._onEvent.dispose();
this.bufferer.dispose();
this.process.dispose();
this.process.shutdown(true);
this._onDispose.fire();
this._onDispose.dispose();
}
public shutdown(immediate: boolean): void {
return this.process.shutdown(immediate);
}
public getCwd(): Promise<string> {
return this.process.getCwd();
}
public getInitialCwd(): Promise<string> {
return this.process.getInitialCwd();
}
public start(): Promise<ITerminalLaunchError | undefined> {
return this.process.start();
}
public input(data: string): void {
return this.process.input(data);
}
public resize(cols: number, rows: number): void {
this.cols = cols;
this.rows = rows;
return this.process.resize(cols, rows);
}
}
// References: - ../../workbench/api/node/extHostTerminalService.ts
// - ../../workbench/contrib/terminal/browser/terminalProcessManager.ts
export class TerminalProviderChannel implements IServerChannel<RemoteAgentConnectionContext>, IDisposable {
private readonly terminals = new Map<number, Terminal>();
private id = 0;
public constructor (private readonly logService: ILogService) {
}
public listen(_: RemoteAgentConnectionContext, event: string, args?: any): Event<any> {
switch (event) {
case '$onTerminalProcessEvent': return this.onTerminalProcessEvent(args);
}
throw new Error(`Invalid listen '${event}'`);
}
private onTerminalProcessEvent(args: terminal.IOnTerminalProcessEventArguments): Event<terminal.IRemoteTerminalProcessEvent> {
return this.getTerminal(args.id).onEvent;
}
public call(context: RemoteAgentConnectionContext, command: string, args?: any): Promise<any> {
switch (command) {
case '$createTerminalProcess': return this.createTerminalProcess(context.remoteAuthority, args);
case '$startTerminalProcess': return this.startTerminalProcess(args);
case '$sendInputToTerminalProcess': return this.sendInputToTerminalProcess(args);
case '$shutdownTerminalProcess': return this.shutdownTerminalProcess(args);
case '$resizeTerminalProcess': return this.resizeTerminalProcess(args);
case '$getTerminalInitialCwd': return this.getTerminalInitialCwd(args);
case '$getTerminalCwd': return this.getTerminalCwd(args);
case '$sendCommandResultToTerminalProcess': return this.sendCommandResultToTerminalProcess(args);
case '$orphanQuestionReply': return this.orphanQuestionReply(args[0]);
case '$listTerminals': return this.listTerminals(args[0]);
}
throw new Error(`Invalid call '${command}'`);
}
public dispose(): void {
this.terminals.forEach((t) => t.dispose());
}
private async createTerminalProcess(remoteAuthority: string, args: terminal.ICreateTerminalProcessArguments): Promise<terminal.ICreateTerminalProcessResult> {
const terminalId = this.id++;
logger.debug('Creating terminal', field('id', terminalId), field('terminals', this.terminals.size));
const shellLaunchConfig: IShellLaunchConfig = {
name: args.shellLaunchConfig.name,
executable: args.shellLaunchConfig.executable,
args: args.shellLaunchConfig.args,
// TODO: Should we transform if it's a string as well? The incoming
// transform only takes `UriComponents` so I suspect it's not necessary.
cwd: typeof args.shellLaunchConfig.cwd !== 'string'
? transformIncoming(remoteAuthority, args.shellLaunchConfig.cwd)
: args.shellLaunchConfig.cwd,
env: args.shellLaunchConfig.env,
};
const activeWorkspaceUri = transformIncoming(remoteAuthority, args.activeWorkspaceFolder?.uri);
const activeWorkspace = activeWorkspaceUri && args.activeWorkspaceFolder ? {
...args.activeWorkspaceFolder,
uri: activeWorkspaceUri,
toResource: (relativePath: string) => resources.joinPath(activeWorkspaceUri, relativePath),
} : undefined;
const resolverService = new VariableResolverService(remoteAuthority, args, process.env as platform.IProcessEnvironment);
const resolver = terminalEnvironment.createVariableResolver(activeWorkspace, resolverService);
const getDefaultShellAndArgs = (): { executable: string; args: string[] | string } => {
if (shellLaunchConfig.executable) {
const executable = resolverService.resolve(activeWorkspace, shellLaunchConfig.executable);
let resolvedArgs: string[] | string = [];
if (shellLaunchConfig.args && Array.isArray(shellLaunchConfig.args)) {
for (const arg of shellLaunchConfig.args) {
resolvedArgs.push(resolverService.resolve(activeWorkspace, arg));
}
} else if (shellLaunchConfig.args) {
resolvedArgs = resolverService.resolve(activeWorkspace, shellLaunchConfig.args);
}
return { executable, args: resolvedArgs };
}
const executable = terminalEnvironment.getDefaultShell(
(key) => args.configuration[key],
args.isWorkspaceShellAllowed,
getSystemShell(platform.platform),
process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'),
process.env.windir,
resolver,
this.logService,
false, // useAutomationShell
);
const resolvedArgs = terminalEnvironment.getDefaultShellArgs(
(key) => args.configuration[key],
args.isWorkspaceShellAllowed,
false, // useAutomationShell
resolver,
this.logService,
);
return { executable, args: resolvedArgs };
};
const getInitialCwd = (): string => {
return terminalEnvironment.getCwd(
shellLaunchConfig,
os.homedir(),
resolver,
activeWorkspaceUri,
args.configuration['terminal.integrated.cwd'],
this.logService,
);
};
// Use a separate var so Typescript recognizes these properties are no
// longer undefined.
const resolvedShellLaunchConfig = {
...shellLaunchConfig,
...getDefaultShellAndArgs(),
cwd: getInitialCwd(),
};
logger.debug('Resolved shell launch configuration', field('id', terminalId));
// Use instead of `terminal.integrated.env.${platform}` to make types work.
const getEnvFromConfig = (): terminal.ISingleTerminalConfiguration<ITerminalEnvironment> => {
if (platform.isWindows) {
return args.configuration['terminal.integrated.env.windows'];
} else if (platform.isMacintosh) {
return args.configuration['terminal.integrated.env.osx'];
}
return args.configuration['terminal.integrated.env.linux'];
};
const getNonInheritedEnv = async (): Promise<platform.IProcessEnvironment> => {
const env = await getMainProcessParentEnv();
env.VSCODE_IPC_HOOK_CLI = process.env['VSCODE_IPC_HOOK_CLI']!;
return env;
};
const env = terminalEnvironment.createTerminalEnvironment(
shellLaunchConfig,
getEnvFromConfig(),
resolver,
args.isWorkspaceShellAllowed,
product.version,
args.configuration['terminal.integrated.detectLocale'],
args.configuration['terminal.integrated.inheritEnv'] !== false
? process.env as platform.IProcessEnvironment
: await getNonInheritedEnv()
);
// Apply extension environment variable collections to the environment.
if (!shellLaunchConfig.strictEnv) {
// They come in an array and in serialized format.
const envVariableCollections = new Map<string, IEnvironmentVariableCollection>();
for (const [k, v] of args.envVariableCollections) {
envVariableCollections.set(k, { map: deserializeEnvironmentVariableCollection(v) });
}
const mergedCollection = new MergedEnvironmentVariableCollection(envVariableCollections);
mergedCollection.applyToProcessEnvironment(env);
}
logger.debug('Resolved terminal environment', field('id', terminalId));
const terminal = new Terminal(terminalId, resolvedShellLaunchConfig, args, env, this.logService);
this.terminals.set(terminalId, terminal);
logger.debug('Created terminal', field('id', terminalId));
terminal.onDispose(() => this.terminals.delete(terminalId));
return {
terminalId,
resolvedShellLaunchConfig,
};
}
private getTerminal(id: number): Terminal {
const terminal = this.terminals.get(id);
if (!terminal) {
throw new Error(`terminal with id ${id} does not exist`);
}
return terminal;
}
private async startTerminalProcess(args: terminal.IStartTerminalProcessArguments): Promise<ITerminalLaunchError | void> {
return this.getTerminal(args.id).start();
}
private async sendInputToTerminalProcess(args: terminal.ISendInputToTerminalProcessArguments): Promise<void> {
return this.getTerminal(args.id).input(args.data);
}
private async shutdownTerminalProcess(args: terminal.IShutdownTerminalProcessArguments): Promise<void> {
return this.getTerminal(args.id).shutdown(args.immediate);
}
private async resizeTerminalProcess(args: terminal.IResizeTerminalProcessArguments): Promise<void> {
return this.getTerminal(args.id).resize(args.cols, args.rows);
}
private async getTerminalInitialCwd(args: terminal.IGetTerminalInitialCwdArguments): Promise<string> {
return this.getTerminal(args.id).getInitialCwd();
}
private async getTerminalCwd(args: terminal.IGetTerminalCwdArguments): Promise<string> {
return this.getTerminal(args.id).getCwd();
}
private async sendCommandResultToTerminalProcess(_: terminal.ISendCommandResultToTerminalProcessArguments): Promise<void> {
// NOTE: Not required unless we implement the `execCommand` event, see above.
throw new Error('not implemented');
}
private async orphanQuestionReply(_: terminal.IOrphanQuestionReplyArgs): Promise<void> {
// NOTE: Not required unless we implement the `orphan?` event, see above.
throw new Error('not implemented');
}
private async listTerminals(_: terminal.IListTerminalsArgs): Promise<terminal.IRemoteTerminalDescriptionDto[]> {
// TODO: args.isInitialization. Maybe this is to have slightly different
// behavior when first listing terminals but I don't know what you'd want to
// do differently. Maybe it's to reset the terminal dispose timeouts or
// something like that, but why not do it each time you list?
const terminals = await Promise.all(Array.from(this.terminals).map(async ([id, terminal]) => {
const cwd = await terminal.getCwd();
return {
id,
pid: terminal.pid,
title: terminal.title,
cwd,
workspaceId: terminal.workspaceId,
workspaceName: terminal.workspaceName,
isOrphan: terminal.isOrphan,
};
}));
// Only returned orphaned terminals so we don't end up attaching to
// terminals already attached elsewhere.
return terminals.filter((t) => t.isOrphan);
}
}
function transformIncoming(remoteAuthority: string, uri: UriComponents | undefined): URI | undefined {
const transformer = getUriTransformer(remoteAuthority);
return uri ? URI.revive(transformer.transformIncoming(uri)) : uri;
}

View File

@ -0,0 +1,192 @@
import { field, Logger, logger } from '@coder/logger';
import * as cp from 'child_process';
import { VSBuffer } from 'vs/base/common/buffer';
import { Emitter } from 'vs/base/common/event';
import { FileAccess } from 'vs/base/common/network';
import { ISocket } from 'vs/base/parts/ipc/common/ipc.net';
import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { getNlsConfiguration } from 'vs/server/node/nls';
import { Protocol } from 'vs/server/node/protocol';
import { IExtHostReadyMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
export abstract class Connection {
private readonly _onClose = new Emitter<void>();
public readonly onClose = this._onClose.event;
private disposed = false;
private _offline: number | undefined;
public constructor(protected protocol: Protocol, public readonly token: string) {}
public get offline(): number | undefined {
return this._offline;
}
public reconnect(socket: ISocket, buffer: VSBuffer): void {
this._offline = undefined;
this.doReconnect(socket, buffer);
}
public dispose(): void {
if (!this.disposed) {
this.disposed = true;
this.doDispose();
this._onClose.fire();
}
}
protected setOffline(): void {
if (!this._offline) {
this._offline = Date.now();
}
}
/**
* Set up the connection on a new socket.
*/
protected abstract doReconnect(socket: ISocket, buffer: VSBuffer): void;
protected abstract doDispose(): void;
}
/**
* Used for all the IPC channels.
*/
export class ManagementConnection extends Connection {
public constructor(protected protocol: Protocol, token: string) {
super(protocol, token);
protocol.onClose(() => this.dispose()); // Explicit close.
protocol.onSocketClose(() => this.setOffline()); // Might reconnect.
}
protected doDispose(): void {
this.protocol.sendDisconnect();
this.protocol.dispose();
this.protocol.getUnderlyingSocket().destroy();
}
protected doReconnect(socket: ISocket, buffer: VSBuffer): void {
this.protocol.beginAcceptReconnection(socket, buffer);
this.protocol.endAcceptReconnection();
}
}
interface DisconnectedMessage {
type: 'VSCODE_EXTHOST_DISCONNECTED';
}
interface ConsoleMessage {
type: '__$console';
// See bootstrap-fork.js#L135.
severity: 'log' | 'warn' | 'error';
arguments: any[];
}
type ExtHostMessage = DisconnectedMessage | ConsoleMessage | IExtHostReadyMessage;
export class ExtensionHostConnection extends Connection {
private process?: cp.ChildProcess;
private readonly logger: Logger;
public constructor(
locale:string, protocol: Protocol, buffer: VSBuffer, token: string,
private readonly environment: INativeEnvironmentService,
) {
super(protocol, token);
this.logger = logger.named('exthost', field('token', token));
this.protocol.dispose();
this.spawn(locale, buffer).then((p) => this.process = p);
this.protocol.getUnderlyingSocket().pause();
}
protected doDispose(): void {
if (this.process) {
this.process.kill();
}
this.protocol.getUnderlyingSocket().destroy();
}
protected doReconnect(socket: ISocket, buffer: VSBuffer): void {
// This is just to set the new socket.
this.protocol.beginAcceptReconnection(socket, null);
this.protocol.dispose();
this.sendInitMessage(buffer);
}
private sendInitMessage(buffer: VSBuffer): void {
const socket = this.protocol.getUnderlyingSocket();
socket.pause();
this.logger.trace('Sending socket');
this.process!.send({ // Process must be set at this point.
type: 'VSCODE_EXTHOST_IPC_SOCKET',
initialDataChunk: (buffer.buffer as Buffer).toString('base64'),
skipWebSocketFrames: this.protocol.getSocket() instanceof NodeSocket,
}, socket);
}
private async spawn(locale: string, buffer: VSBuffer): Promise<cp.ChildProcess> {
this.logger.trace('Getting NLS configuration...');
const config = await getNlsConfiguration(locale, this.environment.userDataPath);
this.logger.trace('Spawning extension host...');
const proc = cp.fork(
FileAccess.asFileUri('bootstrap-fork', require).fsPath,
// While not technically necessary, makes it easier to tell which process
// bootstrap-fork is executing. Can also do pkill -f extensionHost
// Other spawns in the VS Code codebase behave similarly.
[ '--type=extensionHost' ],
{
env: {
...process.env,
AMD_ENTRYPOINT: 'vs/workbench/services/extensions/node/extensionHostProcess',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: 'true',
VSCODE_EXTHOST_WILL_SEND_SOCKET: 'true',
VSCODE_HANDLES_UNCAUGHT_ERRORS: 'true',
VSCODE_LOG_STACK: 'false',
VSCODE_LOG_LEVEL: process.env.LOG_LEVEL,
VSCODE_NLS_CONFIG: JSON.stringify(config),
},
silent: true,
},
);
proc.on('error', (error) => {
this.logger.error('Exited unexpectedly', field('error', error));
this.dispose();
});
proc.on('exit', (code) => {
this.logger.trace('Exited', field('code', code));
this.dispose();
});
if (proc.stdout && proc.stderr) {
proc.stdout.setEncoding('utf8').on('data', (d) => this.logger.info(d));
proc.stderr.setEncoding('utf8').on('data', (d) => this.logger.error(d));
}
proc.on('message', (event: ExtHostMessage) => {
switch (event.type) {
case '__$console':
const fn = this.logger[event.severity === 'log' ? 'info' : event.severity];
if (fn) {
fn.bind(this.logger)('console', field('arguments', event.arguments));
} else {
this.logger.error('Unexpected severity', field('event', event));
}
break;
case 'VSCODE_EXTHOST_DISCONNECTED':
this.logger.trace('Going offline');
this.setOffline();
break;
case 'VSCODE_EXTHOST_IPC_READY':
this.logger.trace('Got ready message');
this.sendInitMessage(buffer);
break;
default:
this.logger.error('Unexpected message', field('event', event));
break;
}
});
this.logger.trace('Waiting for handshake...');
return proc;
}
}

View File

@ -0,0 +1,124 @@
import * as appInsights from 'applicationinsights';
import * as https from 'https';
import * as http from 'http';
import * as os from 'os';
class Channel {
public get _sender() {
throw new Error('unimplemented');
}
public get _buffer() {
throw new Error('unimplemented');
}
public setUseDiskRetryCaching(): void {
throw new Error('unimplemented');
}
public send(): void {
throw new Error('unimplemented');
}
public triggerSend(): void {
throw new Error('unimplemented');
}
}
export class TelemetryClient {
public context: any = undefined;
public commonProperties: any = undefined;
public config: any = {};
public channel: any = new Channel();
public addTelemetryProcessor(): void {
throw new Error('unimplemented');
}
public clearTelemetryProcessors(): void {
throw new Error('unimplemented');
}
public runTelemetryProcessors(): void {
throw new Error('unimplemented');
}
public trackTrace(): void {
throw new Error('unimplemented');
}
public trackMetric(): void {
throw new Error('unimplemented');
}
public trackException(): void {
throw new Error('unimplemented');
}
public trackRequest(): void {
throw new Error('unimplemented');
}
public trackDependency(): void {
throw new Error('unimplemented');
}
public track(): void {
throw new Error('unimplemented');
}
public trackNodeHttpRequestSync(): void {
throw new Error('unimplemented');
}
public trackNodeHttpRequest(): void {
throw new Error('unimplemented');
}
public trackNodeHttpDependency(): void {
throw new Error('unimplemented');
}
public trackEvent(options: appInsights.Contracts.EventTelemetry): void {
if (!options.properties) {
options.properties = {};
}
if (!options.measurements) {
options.measurements = {};
}
try {
const cpus = os.cpus();
options.measurements.cores = cpus.length;
options.properties['common.cpuModel'] = cpus[0].model;
} catch (error) {}
try {
options.measurements.memoryFree = os.freemem();
options.measurements.memoryTotal = os.totalmem();
} catch (error) {}
try {
options.properties['common.shell'] = os.userInfo().shell;
options.properties['common.release'] = os.release();
options.properties['common.arch'] = os.arch();
} catch (error) {}
try {
const url = process.env.TELEMETRY_URL || 'https://v1.telemetry.coder.com/track';
const request = (/^http:/.test(url) ? http : https).request(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
request.on('error', () => { /* We don't care. */ });
request.write(JSON.stringify(options));
request.end();
} catch (error) {}
}
public flush(options: { callback: (v: string) => void }): void {
if (options.callback) {
options.callback('');
}
}
}

View File

@ -0,0 +1,61 @@
import * as cp from 'child_process';
import { Emitter } from 'vs/base/common/event';
enum ControlMessage {
okToChild = 'ok>',
okFromChild = 'ok<',
}
interface RelaunchMessage {
type: 'relaunch';
version: string;
}
export type Message = RelaunchMessage;
class IpcMain {
protected readonly _onMessage = new Emitter<Message>();
public readonly onMessage = this._onMessage.event;
public handshake(child?: cp.ChildProcess): Promise<void> {
return new Promise((resolve, reject) => {
const target = child || process;
if (!target.send) {
throw new Error('Not spawned with IPC enabled');
}
target.on('message', (message) => {
if (message === child ? ControlMessage.okFromChild : ControlMessage.okToChild) {
target.removeAllListeners();
target.on('message', (msg) => this._onMessage.fire(msg));
if (child) {
target.send!(ControlMessage.okToChild);
}
resolve();
}
});
if (child) {
child.once('error', reject);
child.once('exit', (code) => {
const error = new Error(`Unexpected exit with code ${code}`);
(error as any).code = code;
reject(error);
});
} else {
target.send(ControlMessage.okFromChild);
}
});
}
public relaunch(version: string): void {
this.send({ type: 'relaunch', version });
}
private send(message: Message): void {
if (!process.send) {
throw new Error('Not a child process with IPC enabled');
}
process.send(message);
}
}
export const ipcMain = new IpcMain();

View File

@ -0,0 +1,2 @@
import { logger as baseLogger } from '@coder/logger';
export const logger = baseLogger.named('vscode');

View File

@ -0,0 +1,174 @@
import * as fs from 'fs';
import * as path from 'path';
import * as tarStream from 'tar-stream';
import * as util from 'util';
import { CancellationToken } from 'vs/base/common/cancellation';
import { mkdirp } from 'vs/base/node/pfs';
import * as vszip from 'vs/base/node/zip';
import * as nls from 'vs/nls';
import product from 'vs/platform/product/common/product';
// We will be overriding these, so keep a reference to the original.
const vszipExtract = vszip.extract;
const vszipBuffer = vszip.buffer;
export interface IExtractOptions {
overwrite?: boolean;
/**
* Source path within the TAR/ZIP archive. Only the files
* contained in this path will be extracted.
*/
sourcePath?: string;
}
export interface IFile {
path: string;
contents?: Buffer | string;
localPath?: string;
}
export const tar = async (tarPath: string, files: IFile[]): Promise<string> => {
const pack = tarStream.pack();
const chunks: Buffer[] = [];
const ended = new Promise<Buffer>((resolve) => {
pack.on('end', () => resolve(Buffer.concat(chunks)));
});
pack.on('data', (chunk: Buffer) => chunks.push(chunk));
for (let i = 0; i < files.length; i++) {
const file = files[i];
pack.entry({ name: file.path }, file.contents);
}
pack.finalize();
await util.promisify(fs.writeFile)(tarPath, await ended);
return tarPath;
};
export const extract = async (archivePath: string, extractPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> => {
try {
await extractTar(archivePath, extractPath, options, token);
} catch (error) {
if (error.toString().includes('Invalid tar header')) {
await vszipExtract(archivePath, extractPath, options, token);
}
}
};
export const buffer = (targetPath: string, filePath: string): Promise<Buffer> => {
return new Promise<Buffer>(async (resolve, reject) => {
try {
let done: boolean = false;
await extractAssets(targetPath, new RegExp(filePath), (assetPath: string, data: Buffer) => {
if (path.normalize(assetPath) === path.normalize(filePath)) {
done = true;
resolve(data);
}
});
if (!done) {
throw new Error('couldn\'t find asset ' + filePath);
}
} catch (error) {
if (error.toString().includes('Invalid tar header')) {
vszipBuffer(targetPath, filePath).then(resolve).catch(reject);
} else {
reject(error);
}
}
});
};
const extractAssets = async (tarPath: string, match: RegExp, callback: (path: string, data: Buffer) => void): Promise<void> => {
return new Promise<void>((resolve, reject): void => {
const extractor = tarStream.extract();
const fail = (error: Error) => {
extractor.destroy();
reject(error);
};
extractor.once('error', fail);
extractor.on('entry', async (header, stream, next) => {
const name = header.name;
if (match.test(name)) {
extractData(stream).then((data) => {
callback(name, data);
next();
}).catch(fail);
} else {
stream.on('end', () => next());
stream.resume(); // Just drain it.
}
});
extractor.on('finish', resolve);
fs.createReadStream(tarPath).pipe(extractor);
});
};
const extractData = (stream: NodeJS.ReadableStream): Promise<Buffer> => {
return new Promise((resolve, reject): void => {
const fileData: Buffer[] = [];
stream.on('error', reject);
stream.on('end', () => resolve(Buffer.concat(fileData)));
stream.on('data', (data) => fileData.push(data));
});
};
const extractTar = async (tarPath: string, targetPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> => {
return new Promise<void>((resolve, reject): void => {
const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : '');
const extractor = tarStream.extract();
const fail = (error: Error) => {
extractor.destroy();
reject(error);
};
extractor.once('error', fail);
extractor.on('entry', async (header, stream, next) => {
const nextEntry = (): void => {
stream.on('end', () => next());
stream.resume();
};
const rawName = path.normalize(header.name);
if (token.isCancellationRequested || !sourcePathRegex.test(rawName)) {
return nextEntry();
}
const fileName = rawName.replace(sourcePathRegex, '');
const targetFileName = path.join(targetPath, fileName);
if (/\/$/.test(fileName)) {
return mkdirp(targetFileName).then(nextEntry);
}
const dirName = path.dirname(fileName);
const targetDirName = path.join(targetPath, dirName);
if (targetDirName.indexOf(targetPath) !== 0) {
return fail(new Error(nls.localize('invalid file', 'Error extracting {0}. Invalid file.', fileName)));
}
await mkdirp(targetDirName, undefined);
const fstream = fs.createWriteStream(targetFileName, { mode: header.mode });
fstream.once('close', () => next());
fstream.once('error', fail);
stream.pipe(fstream);
});
extractor.once('finish', resolve);
fs.createReadStream(tarPath).pipe(extractor);
});
};
/**
* Override original functionality so we can use a custom marketplace with
* either tars or zips.
*/
export const enableCustomMarketplace = (): void => {
(<any>product).extensionsGallery = { // Use `any` to override readonly.
serviceUrl: process.env.SERVICE_URL || 'https://extensions.coder.com/api',
itemUrl: process.env.ITEM_URL || '',
controlUrl: '',
recommendationsUrl: '',
...(product.extensionsGallery || {}),
};
const target = vszip as typeof vszip;
target.zip = tar;
target.extract = extract;
target.buffer = buffer;
};

View File

@ -0,0 +1,88 @@
import * as fs from 'fs';
import * as path from 'path';
import * as util from 'util';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import * as lp from 'vs/base/node/languagePacks';
import product from 'vs/platform/product/common/product';
import { Translations } from 'vs/workbench/services/extensions/common/extensionPoints';
const configurations = new Map<string, Promise<lp.NLSConfiguration>>();
const metadataPath = path.join(getPathFromAmdModule(require, ''), 'nls.metadata.json');
export const isInternalConfiguration = (config: lp.NLSConfiguration): config is lp.InternalNLSConfiguration => {
return config && !!(<lp.InternalNLSConfiguration>config)._languagePackId;
};
const DefaultConfiguration = {
locale: 'en',
availableLanguages: {},
};
export const getNlsConfiguration = async (locale: string, userDataPath: string): Promise<lp.NLSConfiguration> => {
const id = `${locale}: ${userDataPath}`;
if (!configurations.has(id)) {
configurations.set(id, new Promise(async (resolve) => {
const config = product.commit && await util.promisify(fs.exists)(metadataPath)
? await lp.getNLSConfiguration(product.commit, userDataPath, metadataPath, locale)
: DefaultConfiguration;
if (isInternalConfiguration(config)) {
config._languagePackSupport = true;
}
// If the configuration has no results keep trying since code-server
// doesn't restart when a language is installed so this result would
// persist (the plugin might not be installed yet or something).
if (config.locale !== 'en' && config.locale !== 'en-us' && Object.keys(config.availableLanguages).length === 0) {
configurations.delete(id);
}
resolve(config);
}));
}
return configurations.get(id)!;
};
export const getTranslations = async (locale: string, userDataPath: string): Promise<Translations> => {
const config = await getNlsConfiguration(locale, userDataPath);
if (isInternalConfiguration(config)) {
try {
return JSON.parse(await util.promisify(fs.readFile)(config._translationsConfigFile, 'utf8'));
} catch (error) { /* Nothing yet. */}
}
return {};
};
export const getLocaleFromConfig = async (userDataPath: string): Promise<string> => {
const files = ['locale.json', 'argv.json'];
for (let i = 0; i < files.length; ++i) {
try {
const localeConfigUri = path.join(userDataPath, 'User', files[i]);
const content = stripComments(await util.promisify(fs.readFile)(localeConfigUri, 'utf8'));
return JSON.parse(content).locale;
} catch (error) { /* Ignore. */ }
}
return 'en';
};
// Taken from src/main.js in the main VS Code source.
const stripComments = (content: string): string => {
const regexp = /('(?:[^\\']*(?:\\.)?)*')|('(?:[^\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g;
return content.replace(regexp, (match, _m1, _m2, m3, m4) => {
// Only one of m1, m2, m3, m4 matches
if (m3) {
// A block comment. Replace with nothing
return '';
} else if (m4) {
// A line comment. If it ends in \r?\n then keep it.
const length_1 = m4.length;
if (length_1 > 2 && m4[length_1 - 1] === '\n') {
return m4[length_1 - 2] === '\r' ? '\r\n' : '\n';
}
else {
return '';
}
} else {
// We match a string
return match;
}
});
};

View File

@ -0,0 +1,91 @@
import { field } from '@coder/logger';
import * as net from 'net';
import { VSBuffer } from 'vs/base/common/buffer';
import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net';
import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { AuthRequest, ConnectionTypeRequest, HandshakeMessage } from 'vs/platform/remote/common/remoteAgentConnection';
import { logger } from 'vs/server/node/logger';
export interface SocketOptions {
readonly reconnectionToken: string;
readonly reconnection: boolean;
readonly skipWebSocketFrames: boolean;
}
export class Protocol extends PersistentProtocol {
public constructor(socket: net.Socket, public readonly options: SocketOptions) {
super(
options.skipWebSocketFrames
? new NodeSocket(socket)
: new WebSocketNodeSocket(new NodeSocket(socket)),
);
}
public getUnderlyingSocket(): net.Socket {
const socket = this.getSocket();
return socket instanceof NodeSocket
? socket.socket
: (socket as WebSocketNodeSocket).socket.socket;
}
/**
* Perform a handshake to get a connection request.
*/
public handshake(): Promise<ConnectionTypeRequest> {
logger.trace('Protocol handshake', field('token', this.options.reconnectionToken));
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
logger.error('Handshake timed out', field('token', this.options.reconnectionToken));
reject(new Error('timed out'));
}, 10000); // Matches the client timeout.
const handler = this.onControlMessage((rawMessage) => {
try {
const raw = rawMessage.toString();
logger.trace('Protocol message', field('token', this.options.reconnectionToken), field('message', raw));
const message = JSON.parse(raw);
switch (message.type) {
case 'auth':
return this.authenticate(message);
case 'connectionType':
handler.dispose();
clearTimeout(timeout);
return resolve(message);
default:
throw new Error('Unrecognized message type');
}
} catch (error) {
handler.dispose();
clearTimeout(timeout);
reject(error);
}
});
// Kick off the handshake in case we missed the client's opening shot.
// TODO: Investigate why that message seems to get lost.
this.authenticate();
});
}
/**
* TODO: This ignores the authentication process entirely for now.
*/
private authenticate(_?: AuthRequest): void {
this.sendMessage({ type: 'sign', data: '' });
}
/**
* TODO: implement.
*/
public tunnel(): void {
throw new Error('Tunnel is not implemented yet');
}
/**
* Send a handshake message. In the case of the extension host, it just sends
* back a debug port.
*/
public sendMessage(message: HandshakeMessage | { debugPort?: number } ): void {
this.sendControl(VSBuffer.fromString(JSON.stringify(message)));
}
}

View File

@ -0,0 +1,308 @@
import { field } from '@coder/logger';
import * as fs from 'fs';
import * as net from 'net';
import * as path from 'path';
import { Emitter } from 'vs/base/common/event';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { getMachineId } from 'vs/base/node/id';
import { ClientConnectionEvent, createChannelReceiver, IPCServer, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner';
import { main } from 'vs/code/node/cliProcessMain';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ConfigurationService } from 'vs/platform/configuration/common/configurationService';
import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
import { IFileService } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
import { LocalizationsService } from 'vs/platform/localizations/node/localizations';
import { getLogLevel, ILoggerService, ILogService } from 'vs/platform/log/common/log';
import { LoggerChannel } from 'vs/platform/log/common/logIpc';
import { LoggerService } from 'vs/platform/log/node/loggerService';
import { SpdLogService } from 'vs/platform/log/node/spdlogService';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
import { ConnectionType, ConnectionTypeRequest } from 'vs/platform/remote/common/remoteAgentConnection';
import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { IRequestService } from 'vs/platform/request/common/request';
import { RequestChannel } from 'vs/platform/request/common/requestIpc';
import { RequestService } from 'vs/platform/request/node/requestService';
import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender';
import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
import { combinedAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender';
import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties';
import { INodeProxyService, NodeProxyChannel } from 'vs/server/common/nodeProxy';
import { TelemetryChannel } from 'vs/server/common/telemetry';
import { Query, VscodeOptions, WorkbenchOptions } from 'vs/server/ipc';
import { ExtensionEnvironmentChannel, FileProviderChannel, NodeProxyService, TerminalProviderChannel } from 'vs/server/node/channel';
import { Connection, ExtensionHostConnection, ManagementConnection } from 'vs/server/node/connection';
import { TelemetryClient } from 'vs/server/node/insights';
import { logger } from 'vs/server/node/logger';
import { getLocaleFromConfig, getNlsConfiguration } from 'vs/server/node/nls';
import { Protocol } from 'vs/server/node/protocol';
import { getUriTransformer } from 'vs/server/node/util';
import { REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel';
import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel';
import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService';
import { localize } from 'vs/nls';
export class Vscode {
public readonly _onDidClientConnect = new Emitter<ClientConnectionEvent>();
public readonly onDidClientConnect = this._onDidClientConnect.event;
private readonly ipc = new IPCServer<RemoteAgentConnectionContext>(this.onDidClientConnect);
private readonly maxExtraOfflineConnections = 0;
private readonly connections = new Map<ConnectionType, Map<string, Connection>>();
private readonly services = new ServiceCollection();
private servicesPromise?: Promise<void>;
public async cli(args: NativeParsedArgs): Promise<void> {
return main(args);
}
public async initialize(options: VscodeOptions): Promise<WorkbenchOptions> {
const transformer = getUriTransformer(options.remoteAuthority);
if (!this.servicesPromise) {
this.servicesPromise = this.initializeServices(options.args);
}
await this.servicesPromise;
const environment = this.services.get(IEnvironmentService) as INativeEnvironmentService;
const startPath = options.startPath;
const parseUrl = (url: string): URI => {
// This might be a fully-specified URL or just a path.
try {
return URI.parse(url, true);
} catch (error) {
return URI.from({
scheme: Schemas.vscodeRemote,
authority: options.remoteAuthority,
path: url,
});
}
};
return {
workbenchWebConfiguration: {
workspaceUri: startPath && startPath.workspace ? parseUrl(startPath.url) : undefined,
folderUri: startPath && !startPath.workspace ? parseUrl(startPath.url) : undefined,
remoteAuthority: options.remoteAuthority,
logLevel: getLogLevel(environment),
workspaceProvider: {
payload: [
['userDataPath', environment.userDataPath],
['enableProposedApi', JSON.stringify(options.args['enable-proposed-api'] || [])]
],
},
homeIndicator: {
href: options.args.home || 'https://github.com/cdr/code-server',
icon: 'code',
title: localize('home', "Home"),
},
},
remoteUserDataUri: transformer.transformOutgoing(URI.file(environment.userDataPath)),
productConfiguration: product,
nlsConfiguration: await getNlsConfiguration(environment.args.locale || await getLocaleFromConfig(environment.userDataPath), environment.userDataPath),
commit: product.commit || 'development',
};
}
public async handleWebSocket(socket: net.Socket, query: Query): Promise<true> {
if (!query.reconnectionToken) {
throw new Error('Reconnection token is missing from query parameters');
}
const protocol = new Protocol(socket, {
reconnectionToken: <string>query.reconnectionToken,
reconnection: query.reconnection === 'true',
skipWebSocketFrames: query.skipWebSocketFrames === 'true',
});
try {
await this.connect(await protocol.handshake(), protocol);
} catch (error) {
protocol.sendMessage({ type: 'error', reason: error.message });
protocol.dispose();
protocol.getSocket().dispose();
}
return true;
}
private async connect(message: ConnectionTypeRequest, protocol: Protocol): Promise<void> {
if (product.commit && message.commit !== product.commit) {
logger.warn(`Version mismatch (${message.commit} instead of ${product.commit})`);
}
switch (message.desiredConnectionType) {
case ConnectionType.ExtensionHost:
case ConnectionType.Management:
if (!this.connections.has(message.desiredConnectionType)) {
this.connections.set(message.desiredConnectionType, new Map());
}
const connections = this.connections.get(message.desiredConnectionType)!;
const ok = async () => {
return message.desiredConnectionType === ConnectionType.ExtensionHost
? { debugPort: await this.getDebugPort() }
: { type: 'ok' };
};
const token = protocol.options.reconnectionToken;
if (protocol.options.reconnection && connections.has(token)) {
protocol.sendMessage(await ok());
const buffer = protocol.readEntireBuffer();
protocol.dispose();
return connections.get(token)!.reconnect(protocol.getSocket(), buffer);
} else if (protocol.options.reconnection || connections.has(token)) {
throw new Error(protocol.options.reconnection
? 'Unrecognized reconnection token'
: 'Duplicate reconnection token'
);
}
logger.debug('New connection', field('token', token));
protocol.sendMessage(await ok());
let connection: Connection;
if (message.desiredConnectionType === ConnectionType.Management) {
connection = new ManagementConnection(protocol, token);
this._onDidClientConnect.fire({
protocol, onDidClientDisconnect: connection.onClose,
});
// TODO: Need a way to match clients with a connection. For now
// dispose everything which only works because no extensions currently
// utilize long-running proxies.
(this.services.get(INodeProxyService) as NodeProxyService)._onUp.fire();
connection.onClose(() => (this.services.get(INodeProxyService) as NodeProxyService)._onDown.fire());
} else {
const buffer = protocol.readEntireBuffer();
connection = new ExtensionHostConnection(
message.args ? message.args.language : 'en',
protocol, buffer, token,
this.services.get(IEnvironmentService) as INativeEnvironmentService,
);
}
connections.set(token, connection);
connection.onClose(() => {
logger.debug('Connection closed', field('token', token));
connections.delete(token);
});
this.disposeOldOfflineConnections(connections);
break;
case ConnectionType.Tunnel: return protocol.tunnel();
default: throw new Error('Unrecognized connection type');
}
}
private disposeOldOfflineConnections(connections: Map<string, Connection>): void {
const offline = Array.from(connections.values())
.filter((connection) => typeof connection.offline !== 'undefined');
for (let i = 0, max = offline.length - this.maxExtraOfflineConnections; i < max; ++i) {
logger.debug('Disposing offline connection', field('token', offline[i].token));
offline[i].dispose();
}
}
private async initializeServices(args: NativeParsedArgs): Promise<void> {
const environmentService = new NativeEnvironmentService(args);
// https://github.com/cdr/code-server/issues/1693
fs.mkdirSync(environmentService.globalStorageHome.fsPath, { recursive: true });
const logService = new SpdLogService(RemoteExtensionLogFileName, environmentService.logsPath, getLogLevel(environmentService));
const fileService = new FileService(logService);
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(logService));
const piiPaths = [
path.join(environmentService.userDataPath, 'clp'), // Language packs.
environmentService.appRoot,
environmentService.extensionsPath,
environmentService.builtinExtensionsPath,
...environmentService.extraExtensionPaths,
...environmentService.extraBuiltinExtensionPaths,
];
this.ipc.registerChannel('logger', new LoggerChannel(logService));
this.ipc.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ExtensionHostDebugBroadcastChannel());
this.services.set(ILogService, logService);
this.services.set(IEnvironmentService, environmentService);
this.services.set(INativeEnvironmentService, environmentService);
this.services.set(ILoggerService, new SyncDescriptor(LoggerService));
const configurationService = new ConfigurationService(environmentService.settingsResource, fileService);
await configurationService.initialize();
this.services.set(IConfigurationService, configurationService);
this.services.set(IRequestService, new SyncDescriptor(RequestService));
this.services.set(IFileService, fileService);
this.services.set(IProductService, { _serviceBrand: undefined, ...product });
const machineId = await getMachineId();
await new Promise((resolve) => {
const instantiationService = new InstantiationService(this.services);
instantiationService.invokeFunction((accessor) => {
instantiationService.createInstance(LogsDataCleaner);
let telemetryService: ITelemetryService;
if (!environmentService.disableTelemetry) {
telemetryService = new TelemetryService({
appender: combinedAppender(
new AppInsightsAppender('code-server', null, () => new TelemetryClient() as any),
new TelemetryLogAppender(accessor.get(ILoggerService), environmentService)
),
sendErrorTelemetry: true,
commonProperties: resolveCommonProperties(
product.commit, product.version, machineId,
[], environmentService.installSourcePath, 'code-server',
),
piiPaths,
}, configurationService);
} else {
telemetryService = NullTelemetryService;
}
this.services.set(ITelemetryService, telemetryService);
this.services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService));
this.services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService));
this.services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService));
this.services.set(INodeProxyService, new SyncDescriptor(NodeProxyService));
this.ipc.registerChannel('extensions', new ExtensionManagementChannel(
accessor.get(IExtensionManagementService),
(context) => getUriTransformer(context.remoteAuthority),
));
this.ipc.registerChannel('remoteextensionsenvironment', new ExtensionEnvironmentChannel(
environmentService, logService, telemetryService, '',
));
this.ipc.registerChannel('request', new RequestChannel(accessor.get(IRequestService)));
this.ipc.registerChannel('telemetry', new TelemetryChannel(telemetryService));
this.ipc.registerChannel('nodeProxy', new NodeProxyChannel(accessor.get(INodeProxyService)));
this.ipc.registerChannel('localizations', <IServerChannel<any>>createChannelReceiver(accessor.get(ILocalizationsService)));
this.ipc.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, new FileProviderChannel(environmentService, logService));
this.ipc.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new TerminalProviderChannel(logService));
resolve(new ErrorTelemetry(telemetryService));
});
});
}
/**
* TODO: implement.
*/
private async getDebugPort(): Promise<number | undefined> {
return undefined;
}
}

View File

@ -0,0 +1,13 @@
import { URITransformer } from 'vs/base/common/uriIpc';
export const getUriTransformer = (remoteAuthority: string): URITransformer => {
return new URITransformer(remoteAuthority);
};
/**
* Encode a path for opening via the folder or workspace query parameter. This
* preserves slashes so it can be edited by hand more easily.
*/
export const encodePath = (path: string): string => {
return path.split('/').map((p) => encodeURIComponent(p)).join('/');
};