feat: apply patch after setting up subtree
This commit is contained in:
241
lib/vscode/src/vs/server/browser/client.ts
Normal file
241
lib/vscode/src/vs/server/browser/client.ts
Normal 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);
|
||||
};
|
51
lib/vscode/src/vs/server/browser/extHostNodeProxy.ts
Normal file
51
lib/vscode/src/vs/server/browser/extHostNodeProxy.ts
Normal 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');
|
55
lib/vscode/src/vs/server/browser/mainThreadNodeProxy.ts
Normal file
55
lib/vscode/src/vs/server/browser/mainThreadNodeProxy.ts
Normal 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;
|
||||
}
|
||||
}
|
48
lib/vscode/src/vs/server/browser/worker.ts
Normal file
48
lib/vscode/src/vs/server/browser/worker.ts
Normal 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();
|
||||
}
|
||||
};
|
47
lib/vscode/src/vs/server/common/nodeProxy.ts
Normal file
47
lib/vscode/src/vs/server/common/nodeProxy.ts
Normal 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]);
|
||||
}
|
||||
}
|
65
lib/vscode/src/vs/server/common/telemetry.ts
Normal file
65
lib/vscode/src/vs/server/common/telemetry.ts
Normal 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]);
|
||||
}
|
||||
}
|
81
lib/vscode/src/vs/server/entry.ts
Normal file
81
lib/vscode/src/vs/server/entry.ts
Normal 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' });
|
||||
}
|
3
lib/vscode/src/vs/server/fork.js
Normal file
3
lib/vscode/src/vs/server/fork.js
Normal 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
140
lib/vscode/src/vs/server/ipc.d.ts
vendored
Normal 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;
|
||||
}
|
906
lib/vscode/src/vs/server/node/channel.ts
Normal file
906
lib/vscode/src/vs/server/node/channel.ts
Normal 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;
|
||||
}
|
192
lib/vscode/src/vs/server/node/connection.ts
Normal file
192
lib/vscode/src/vs/server/node/connection.ts
Normal 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;
|
||||
}
|
||||
}
|
124
lib/vscode/src/vs/server/node/insights.ts
Normal file
124
lib/vscode/src/vs/server/node/insights.ts
Normal 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('');
|
||||
}
|
||||
}
|
||||
}
|
61
lib/vscode/src/vs/server/node/ipc.ts
Normal file
61
lib/vscode/src/vs/server/node/ipc.ts
Normal 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();
|
2
lib/vscode/src/vs/server/node/logger.ts
Normal file
2
lib/vscode/src/vs/server/node/logger.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import { logger as baseLogger } from '@coder/logger';
|
||||
export const logger = baseLogger.named('vscode');
|
174
lib/vscode/src/vs/server/node/marketplace.ts
Normal file
174
lib/vscode/src/vs/server/node/marketplace.ts
Normal 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;
|
||||
};
|
88
lib/vscode/src/vs/server/node/nls.ts
Normal file
88
lib/vscode/src/vs/server/node/nls.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
};
|
91
lib/vscode/src/vs/server/node/protocol.ts
Normal file
91
lib/vscode/src/vs/server/node/protocol.ts
Normal 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)));
|
||||
}
|
||||
}
|
308
lib/vscode/src/vs/server/node/server.ts
Normal file
308
lib/vscode/src/vs/server/node/server.ts
Normal 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;
|
||||
}
|
||||
}
|
13
lib/vscode/src/vs/server/node/util.ts
Normal file
13
lib/vscode/src/vs/server/node/util.ts
Normal 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('/');
|
||||
};
|
Reference in New Issue
Block a user