mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-31 17:20:28 -04:00
Vscode merge (#4582)
* Merge from vscode 37cb23d3dd4f9433d56d4ba5ea3203580719a0bd * fix issues with merges * bump node version in azpipe * replace license headers * remove duplicate launch task * fix build errors * fix build errors * fix tslint issues * working through package and linux build issues * more work * wip * fix packaged builds * working through linux build errors * wip * wip * wip * fix mac and linux file limits * iterate linux pipeline * disable editor typing * revert series to parallel * remove optimize vscode from linux * fix linting issues * revert testing change * add work round for new node * readd packaging for extensions * fix issue with angular not resolving decorator dependencies
This commit is contained in:
545
src/vs/workbench/contrib/debug/node/debugAdapter.ts
Normal file
545
src/vs/workbench/contrib/debug/node/debugAdapter.ts
Normal file
@@ -0,0 +1,545 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { exists } from 'vs/base/node/pfs';
|
||||
import * as cp from 'child_process';
|
||||
import * as stream from 'stream';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as net from 'net';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IOutputService } from 'vs/workbench/contrib/output/common/output';
|
||||
import { IDebugAdapter, IDebugAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution, IDebugAdapterServer } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
/**
|
||||
* Abstract implementation of the low level API for a debug adapter.
|
||||
* Missing is how this API communicates with the debug adapter.
|
||||
*/
|
||||
export abstract class AbstractDebugAdapter implements IDebugAdapter {
|
||||
|
||||
private sequence: number;
|
||||
private pendingRequests: Map<number, (e: DebugProtocol.Response) => void>;
|
||||
private requestCallback: (request: DebugProtocol.Request) => void;
|
||||
private eventCallback: (request: DebugProtocol.Event) => void;
|
||||
private messageCallback: (message: DebugProtocol.ProtocolMessage) => void;
|
||||
|
||||
protected readonly _onError: Emitter<Error>;
|
||||
protected readonly _onExit: Emitter<number | null>;
|
||||
|
||||
constructor() {
|
||||
this.sequence = 1;
|
||||
this.pendingRequests = new Map();
|
||||
|
||||
this._onError = new Emitter<Error>();
|
||||
this._onExit = new Emitter<number>();
|
||||
}
|
||||
|
||||
abstract startSession(): Promise<void>;
|
||||
abstract stopSession(): Promise<void>;
|
||||
|
||||
abstract sendMessage(message: DebugProtocol.ProtocolMessage): void;
|
||||
|
||||
get onError(): Event<Error> {
|
||||
return this._onError.event;
|
||||
}
|
||||
|
||||
get onExit(): Event<number | null> {
|
||||
return this._onExit.event;
|
||||
}
|
||||
|
||||
onMessage(callback: (message: DebugProtocol.ProtocolMessage) => void): void {
|
||||
if (this.eventCallback) {
|
||||
this._onError.fire(new Error(`attempt to set more than one 'Message' callback`));
|
||||
}
|
||||
this.messageCallback = callback;
|
||||
}
|
||||
|
||||
onEvent(callback: (event: DebugProtocol.Event) => void): void {
|
||||
if (this.eventCallback) {
|
||||
this._onError.fire(new Error(`attempt to set more than one 'Event' callback`));
|
||||
}
|
||||
this.eventCallback = callback;
|
||||
}
|
||||
|
||||
onRequest(callback: (request: DebugProtocol.Request) => void): void {
|
||||
if (this.requestCallback) {
|
||||
this._onError.fire(new Error(`attempt to set more than one 'Request' callback`));
|
||||
}
|
||||
this.requestCallback = callback;
|
||||
}
|
||||
|
||||
sendResponse(response: DebugProtocol.Response): void {
|
||||
if (response.seq > 0) {
|
||||
this._onError.fire(new Error(`attempt to send more than one response for command ${response.command}`));
|
||||
} else {
|
||||
this.internalSend('response', response);
|
||||
}
|
||||
}
|
||||
|
||||
sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void, timeout?: number): void {
|
||||
|
||||
const request: any = {
|
||||
command: command
|
||||
};
|
||||
if (args && Object.keys(args).length > 0) {
|
||||
request.arguments = args;
|
||||
}
|
||||
|
||||
this.internalSend('request', request);
|
||||
|
||||
if (typeof timeout === 'number') {
|
||||
const timer = setTimeout(() => {
|
||||
clearTimeout(timer);
|
||||
const clb = this.pendingRequests.get(request.seq);
|
||||
if (clb) {
|
||||
this.pendingRequests.delete(request.seq);
|
||||
const err: DebugProtocol.Response = {
|
||||
type: 'response',
|
||||
seq: 0,
|
||||
request_seq: request.seq,
|
||||
success: false,
|
||||
command,
|
||||
message: `timeout after ${timeout} ms`
|
||||
};
|
||||
clb(err);
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
if (clb) {
|
||||
// store callback for this request
|
||||
this.pendingRequests.set(request.seq, clb);
|
||||
}
|
||||
}
|
||||
|
||||
acceptMessage(message: DebugProtocol.ProtocolMessage): void {
|
||||
if (this.messageCallback) {
|
||||
this.messageCallback(message);
|
||||
} else {
|
||||
switch (message.type) {
|
||||
case 'event':
|
||||
if (this.eventCallback) {
|
||||
this.eventCallback(<DebugProtocol.Event>message);
|
||||
}
|
||||
break;
|
||||
case 'request':
|
||||
if (this.requestCallback) {
|
||||
this.requestCallback(<DebugProtocol.Request>message);
|
||||
}
|
||||
break;
|
||||
case 'response':
|
||||
const response = <DebugProtocol.Response>message;
|
||||
const clb = this.pendingRequests.get(response.request_seq);
|
||||
if (clb) {
|
||||
this.pendingRequests.delete(response.request_seq);
|
||||
clb(response);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private internalSend(typ: 'request' | 'response' | 'event', message: DebugProtocol.ProtocolMessage): void {
|
||||
|
||||
message.type = typ;
|
||||
message.seq = this.sequence++;
|
||||
|
||||
this.sendMessage(message);
|
||||
}
|
||||
|
||||
protected cancelPending() {
|
||||
const pending = this.pendingRequests;
|
||||
this.pendingRequests = new Map();
|
||||
setTimeout(_ => {
|
||||
pending.forEach((callback, request_seq) => {
|
||||
const err: DebugProtocol.Response = {
|
||||
type: 'response',
|
||||
seq: 0,
|
||||
request_seq,
|
||||
success: false,
|
||||
command: 'canceled',
|
||||
message: 'canceled'
|
||||
};
|
||||
callback(err);
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.cancelPending();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation that communicates via two streams with the debug adapter.
|
||||
*/
|
||||
export abstract class StreamDebugAdapter extends AbstractDebugAdapter {
|
||||
|
||||
private static readonly TWO_CRLF = '\r\n\r\n';
|
||||
private static readonly HEADER_LINESEPARATOR = /\r?\n/; // allow for non-RFC 2822 conforming line separators
|
||||
private static readonly HEADER_FIELDSEPARATOR = /: */;
|
||||
|
||||
private outputStream: stream.Writable;
|
||||
private rawData: Buffer;
|
||||
private contentLength: number;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
protected connect(readable: stream.Readable, writable: stream.Writable): void {
|
||||
|
||||
this.outputStream = writable;
|
||||
this.rawData = Buffer.allocUnsafe(0);
|
||||
this.contentLength = -1;
|
||||
|
||||
readable.on('data', (data: Buffer) => this.handleData(data));
|
||||
}
|
||||
|
||||
sendMessage(message: DebugProtocol.ProtocolMessage): void {
|
||||
|
||||
if (this.outputStream) {
|
||||
const json = JSON.stringify(message);
|
||||
this.outputStream.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}${StreamDebugAdapter.TWO_CRLF}${json}`, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
private handleData(data: Buffer): void {
|
||||
|
||||
this.rawData = Buffer.concat([this.rawData, data]);
|
||||
|
||||
while (true) {
|
||||
if (this.contentLength >= 0) {
|
||||
if (this.rawData.length >= this.contentLength) {
|
||||
const message = this.rawData.toString('utf8', 0, this.contentLength);
|
||||
this.rawData = this.rawData.slice(this.contentLength);
|
||||
this.contentLength = -1;
|
||||
if (message.length > 0) {
|
||||
try {
|
||||
this.acceptMessage(<DebugProtocol.ProtocolMessage>JSON.parse(message));
|
||||
} catch (e) {
|
||||
this._onError.fire(new Error((e.message || e) + '\n' + message));
|
||||
}
|
||||
}
|
||||
continue; // there may be more complete messages to process
|
||||
}
|
||||
} else {
|
||||
const idx = this.rawData.indexOf(StreamDebugAdapter.TWO_CRLF);
|
||||
if (idx !== -1) {
|
||||
const header = this.rawData.toString('utf8', 0, idx);
|
||||
const lines = header.split(StreamDebugAdapter.HEADER_LINESEPARATOR);
|
||||
for (const h of lines) {
|
||||
const kvPair = h.split(StreamDebugAdapter.HEADER_FIELDSEPARATOR);
|
||||
if (kvPair[0] === 'Content-Length') {
|
||||
this.contentLength = Number(kvPair[1]);
|
||||
}
|
||||
}
|
||||
this.rawData = this.rawData.slice(idx + StreamDebugAdapter.TWO_CRLF.length);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation that connects to a debug adapter via a socket.
|
||||
*/
|
||||
export class SocketDebugAdapter extends StreamDebugAdapter {
|
||||
|
||||
private socket?: net.Socket;
|
||||
|
||||
constructor(private adapterServer: IDebugAdapterServer) {
|
||||
super();
|
||||
}
|
||||
|
||||
startSession(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let connected = false;
|
||||
this.socket = net.createConnection(this.adapterServer.port, this.adapterServer.host || '127.0.0.1', () => {
|
||||
this.connect(this.socket!, this.socket!);
|
||||
resolve();
|
||||
connected = true;
|
||||
});
|
||||
this.socket.on('close', () => {
|
||||
if (connected) {
|
||||
this._onError.fire(new Error('connection closed'));
|
||||
} else {
|
||||
reject(new Error('connection closed'));
|
||||
}
|
||||
});
|
||||
this.socket.on('error', error => {
|
||||
if (connected) {
|
||||
this._onError.fire(error);
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
stopSession(): Promise<void> {
|
||||
|
||||
// Cancel all sent promises on disconnect so debug trees are not left in a broken state #3666.
|
||||
this.cancelPending();
|
||||
|
||||
if (this.socket) {
|
||||
this.socket.end();
|
||||
this.socket = undefined;
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation that launches the debug adapter as a separate process and communicates via stdin/stdout.
|
||||
*/
|
||||
export class ExecutableDebugAdapter extends StreamDebugAdapter {
|
||||
|
||||
private serverProcess: cp.ChildProcess;
|
||||
|
||||
constructor(private adapterExecutable: IDebugAdapterExecutable, private debugType: string, private readonly outputService?: IOutputService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async startSession(): Promise<void> {
|
||||
|
||||
const command = this.adapterExecutable.command;
|
||||
const args = this.adapterExecutable.args;
|
||||
const options = this.adapterExecutable.options || {};
|
||||
|
||||
try {
|
||||
// verify executables asynchronously
|
||||
if (command) {
|
||||
if (path.isAbsolute(command)) {
|
||||
const commandExists = await exists(command);
|
||||
if (!commandExists) {
|
||||
throw new Error(nls.localize('debugAdapterBinNotFound', "Debug adapter executable '{0}' does not exist.", command));
|
||||
}
|
||||
} else {
|
||||
// relative path
|
||||
if (command.indexOf('/') < 0 && command.indexOf('\\') < 0) {
|
||||
// no separators: command looks like a runtime name like 'node' or 'mono'
|
||||
// TODO: check that the runtime is available on PATH
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(nls.localize({ key: 'debugAdapterCannotDetermineExecutable', comment: ['Adapter executable file not found'] },
|
||||
"Cannot determine executable for debug adapter '{0}'.", this.debugType));
|
||||
}
|
||||
|
||||
let env = objects.mixin({}, process.env);
|
||||
if (options.env) {
|
||||
env = objects.mixin(env, options.env);
|
||||
}
|
||||
delete env.VSCODE_PREVENT_FOREIGN_INSPECT;
|
||||
|
||||
if (command === 'node') {
|
||||
if (Array.isArray(args) && args.length > 0) {
|
||||
const isElectron = !!process.env['ELECTRON_RUN_AS_NODE'] || !!process.versions['electron'];
|
||||
const forkOptions: cp.ForkOptions = {
|
||||
env: env,
|
||||
execArgv: isElectron ? ['-e', 'delete process.env.ELECTRON_RUN_AS_NODE;require(process.argv[1])'] : [],
|
||||
silent: true
|
||||
};
|
||||
if (options.cwd) {
|
||||
forkOptions.cwd = options.cwd;
|
||||
}
|
||||
const child = cp.fork(args[0], args.slice(1), forkOptions);
|
||||
if (!child.pid) {
|
||||
throw new Error(nls.localize('unableToLaunchDebugAdapter', "Unable to launch debug adapter from '{0}'.", args[0]));
|
||||
}
|
||||
this.serverProcess = child;
|
||||
} else {
|
||||
throw new Error(nls.localize('unableToLaunchDebugAdapterNoArgs', "Unable to launch debug adapter."));
|
||||
}
|
||||
} else {
|
||||
const spawnOptions: cp.SpawnOptions = {
|
||||
env: env
|
||||
};
|
||||
if (options.cwd) {
|
||||
spawnOptions.cwd = options.cwd;
|
||||
}
|
||||
this.serverProcess = cp.spawn(command, args, spawnOptions);
|
||||
}
|
||||
|
||||
this.serverProcess.on('error', err => {
|
||||
this._onError.fire(err);
|
||||
});
|
||||
this.serverProcess.on('exit', (code, signal) => {
|
||||
this._onExit.fire(code);
|
||||
});
|
||||
|
||||
this.serverProcess.stdout.on('close', () => {
|
||||
this._onError.fire(new Error('read error'));
|
||||
});
|
||||
this.serverProcess.stdout.on('error', error => {
|
||||
this._onError.fire(error);
|
||||
});
|
||||
|
||||
this.serverProcess.stdin.on('error', error => {
|
||||
this._onError.fire(error);
|
||||
});
|
||||
|
||||
const outputService = this.outputService;
|
||||
if (outputService) {
|
||||
const sanitize = (s: string) => s.toString().replace(/\r?\n$/mg, '');
|
||||
// this.serverProcess.stdout.on('data', (data: string) => {
|
||||
// console.log('%c' + sanitize(data), 'background: #ddd; font-style: italic;');
|
||||
// });
|
||||
this.serverProcess.stderr.on('data', (data: string) => {
|
||||
const channel = outputService.getChannel(ExtensionsChannelId);
|
||||
if (channel) {
|
||||
channel.append(sanitize(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// finally connect to the DA
|
||||
this.connect(this.serverProcess.stdout, this.serverProcess.stdin);
|
||||
|
||||
} catch (err) {
|
||||
this._onError.fire(err);
|
||||
}
|
||||
}
|
||||
|
||||
stopSession(): Promise<void> {
|
||||
|
||||
// Cancel all sent promises on disconnect so debug trees are not left in a broken state #3666.
|
||||
this.cancelPending();
|
||||
|
||||
if (!this.serverProcess) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// when killing a process in windows its child
|
||||
// processes are *not* killed but become root
|
||||
// processes. Therefore we use TASKKILL.EXE
|
||||
if (platform.isWindows) {
|
||||
return new Promise<void>((c, e) => {
|
||||
const killer = cp.exec(`taskkill /F /T /PID ${this.serverProcess.pid}`, function (err, stdout, stderr) {
|
||||
if (err) {
|
||||
return e(err);
|
||||
}
|
||||
});
|
||||
killer.on('exit', c);
|
||||
killer.on('error', e);
|
||||
});
|
||||
} else {
|
||||
this.serverProcess.kill('SIGTERM');
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private static extract(platformContribution: IPlatformSpecificAdapterContribution, extensionFolderPath: string): IDebuggerContribution | undefined {
|
||||
if (!platformContribution) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result: IDebuggerContribution = Object.create(null);
|
||||
if (platformContribution.runtime) {
|
||||
if (platformContribution.runtime.indexOf('./') === 0) { // TODO
|
||||
result.runtime = path.join(extensionFolderPath, platformContribution.runtime);
|
||||
} else {
|
||||
result.runtime = platformContribution.runtime;
|
||||
}
|
||||
}
|
||||
if (platformContribution.runtimeArgs) {
|
||||
result.runtimeArgs = platformContribution.runtimeArgs;
|
||||
}
|
||||
if (platformContribution.program) {
|
||||
if (!path.isAbsolute(platformContribution.program)) {
|
||||
result.program = path.join(extensionFolderPath, platformContribution.program);
|
||||
} else {
|
||||
result.program = platformContribution.program;
|
||||
}
|
||||
}
|
||||
if (platformContribution.args) {
|
||||
result.args = platformContribution.args;
|
||||
}
|
||||
|
||||
const contribution = platformContribution as IDebuggerContribution;
|
||||
|
||||
if (contribution.win) {
|
||||
result.win = ExecutableDebugAdapter.extract(contribution.win, extensionFolderPath);
|
||||
}
|
||||
if (contribution.winx86) {
|
||||
result.winx86 = ExecutableDebugAdapter.extract(contribution.winx86, extensionFolderPath);
|
||||
}
|
||||
if (contribution.windows) {
|
||||
result.windows = ExecutableDebugAdapter.extract(contribution.windows, extensionFolderPath);
|
||||
}
|
||||
if (contribution.osx) {
|
||||
result.osx = ExecutableDebugAdapter.extract(contribution.osx, extensionFolderPath);
|
||||
}
|
||||
if (contribution.linux) {
|
||||
result.linux = ExecutableDebugAdapter.extract(contribution.linux, extensionFolderPath);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static platformAdapterExecutable(extensionDescriptions: IExtensionDescription[], debugType: string): IDebugAdapterExecutable | undefined {
|
||||
let result: IDebuggerContribution = Object.create(null);
|
||||
debugType = debugType.toLowerCase();
|
||||
|
||||
// merge all contributions into one
|
||||
for (const ed of extensionDescriptions) {
|
||||
if (ed.contributes) {
|
||||
const debuggers = <IDebuggerContribution[]>ed.contributes['debuggers'];
|
||||
if (debuggers && debuggers.length > 0) {
|
||||
debuggers.filter(dbg => typeof dbg.type === 'string' && strings.equalsIgnoreCase(dbg.type, debugType)).forEach(dbg => {
|
||||
// extract relevant attributes and make them absolute where needed
|
||||
const extractedDbg = ExecutableDebugAdapter.extract(dbg, ed.extensionLocation.fsPath);
|
||||
|
||||
// merge
|
||||
result = objects.mixin(result, extractedDbg, ed.isBuiltin);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// select the right platform
|
||||
let platformInfo: IPlatformSpecificAdapterContribution | undefined;
|
||||
if (platform.isWindows && !process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) {
|
||||
platformInfo = result.winx86 || result.win || result.windows;
|
||||
} else if (platform.isWindows) {
|
||||
platformInfo = result.win || result.windows;
|
||||
} else if (platform.isMacintosh) {
|
||||
platformInfo = result.osx;
|
||||
} else if (platform.isLinux) {
|
||||
platformInfo = result.linux;
|
||||
}
|
||||
platformInfo = platformInfo || result;
|
||||
|
||||
// these are the relevant attributes
|
||||
let program = platformInfo.program || result.program;
|
||||
const args = platformInfo.args || result.args;
|
||||
let runtime = platformInfo.runtime || result.runtime;
|
||||
const runtimeArgs = platformInfo.runtimeArgs || result.runtimeArgs;
|
||||
|
||||
if (runtime) {
|
||||
return {
|
||||
type: 'executable',
|
||||
command: runtime,
|
||||
args: (runtimeArgs || []).concat(typeof program === 'string' ? [program] : []).concat(args || [])
|
||||
};
|
||||
} else if (program) {
|
||||
return {
|
||||
type: 'executable',
|
||||
command: program,
|
||||
args: args || []
|
||||
};
|
||||
}
|
||||
|
||||
// nothing found
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
381
src/vs/workbench/contrib/debug/node/debugger.ts
Normal file
381
src/vs/workbench/contrib/debug/node/debugger.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { Client as TelemetryClient } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { isObject } from 'vs/base/common/types';
|
||||
import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc';
|
||||
import { IJSONSchema, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema';
|
||||
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { IConfig, IDebuggerContribution, IDebugAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager, IDebugAdapter, ITerminalSettings, IDebugger, IDebugSession, IAdapterDescriptor, IDebugAdapterServer } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IOutputService } from 'vs/workbench/contrib/output/common/output';
|
||||
import { ExecutableDebugAdapter, SocketDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter';
|
||||
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
|
||||
import * as ConfigurationResolverUtils from 'vs/workbench/services/configurationResolver/common/configurationResolverUtils';
|
||||
import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { TaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDefinitionRegistry';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { isDebuggerMainContribution } from 'vs/workbench/contrib/debug/common/debugUtils';
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
export class Debugger implements IDebugger {
|
||||
|
||||
private debuggerContribution: IDebuggerContribution;
|
||||
private mergedExtensionDescriptions: IExtensionDescription[] = [];
|
||||
private mainExtensionDescription: IExtensionDescription | undefined;
|
||||
|
||||
constructor(private configurationManager: IConfigurationManager, dbgContribution: IDebuggerContribution, extensionDescription: IExtensionDescription,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@ITextResourcePropertiesService private readonly resourcePropertiesService: ITextResourcePropertiesService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IConfigurationResolverService private readonly configurationResolverService: IConfigurationResolverService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
) {
|
||||
this.debuggerContribution = { type: dbgContribution.type };
|
||||
this.merge(dbgContribution, extensionDescription);
|
||||
}
|
||||
|
||||
public merge(otherDebuggerContribution: IDebuggerContribution, extensionDescription: IExtensionDescription): void {
|
||||
|
||||
/**
|
||||
* Copies all properties of source into destination. The optional parameter "overwrite" allows to control
|
||||
* if existing non-structured properties on the destination should be overwritten or not. Defaults to true (overwrite).
|
||||
*/
|
||||
function mixin(destination: any, source: any, overwrite: boolean, level = 0): any {
|
||||
|
||||
if (!isObject(destination)) {
|
||||
return source;
|
||||
}
|
||||
|
||||
if (isObject(source)) {
|
||||
Object.keys(source).forEach(key => {
|
||||
if (isObject(destination[key]) && isObject(source[key])) {
|
||||
mixin(destination[key], source[key], overwrite, level + 1);
|
||||
} else {
|
||||
if (key in destination) {
|
||||
if (overwrite) {
|
||||
if (level === 0 && key === 'type') {
|
||||
// don't merge the 'type' property
|
||||
} else {
|
||||
destination[key] = source[key];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
destination[key] = source[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
// only if not already merged
|
||||
if (this.mergedExtensionDescriptions.indexOf(extensionDescription) < 0) {
|
||||
|
||||
// remember all extensions that have been merged for this debugger
|
||||
this.mergedExtensionDescriptions.push(extensionDescription);
|
||||
|
||||
// merge new debugger contribution into existing contributions (and don't overwrite values in built-in extensions)
|
||||
mixin(this.debuggerContribution, otherDebuggerContribution, extensionDescription.isBuiltin);
|
||||
|
||||
// remember the extension that is considered the "main" debugger contribution
|
||||
if (isDebuggerMainContribution(otherDebuggerContribution)) {
|
||||
this.mainExtensionDescription = extensionDescription;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public createDebugAdapter(session: IDebugSession, outputService: IOutputService): Promise<IDebugAdapter> {
|
||||
return this.configurationManager.activateDebuggers('onDebugAdapterProtocolTracker', this.type).then(_ => {
|
||||
if (this.inExtHost()) {
|
||||
const da = this.configurationManager.createDebugAdapter(session);
|
||||
if (da) {
|
||||
return Promise.resolve(da);
|
||||
}
|
||||
throw new Error(nls.localize('cannot.find.da', "Cannot find debug adapter for type '{0}'.", this.type));
|
||||
} else {
|
||||
return this.getAdapterDescriptor(session).then(adapterDescriptor => {
|
||||
switch (adapterDescriptor.type) {
|
||||
case 'executable':
|
||||
return new ExecutableDebugAdapter(adapterDescriptor, this.type, outputService);
|
||||
case 'server':
|
||||
return new SocketDebugAdapter(adapterDescriptor);
|
||||
case 'implementation':
|
||||
// TODO@AW: this.inExtHost() should now return true
|
||||
return Promise.resolve(this.configurationManager.createDebugAdapter(session));
|
||||
default:
|
||||
throw new Error('unknown descriptor type');
|
||||
}
|
||||
}).catch(err => {
|
||||
if (err && err.message) {
|
||||
throw new Error(nls.localize('cannot.create.da.with.err', "Cannot create debug adapter ({0}).", err.message));
|
||||
} else {
|
||||
throw new Error(nls.localize('cannot.create.da', "Cannot create debug adapter."));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getAdapterDescriptor(session: IDebugSession): Promise<IAdapterDescriptor> {
|
||||
|
||||
// a "debugServer" attribute in the launch config takes precedence
|
||||
if (typeof session.configuration.debugServer === 'number') {
|
||||
return Promise.resolve(<IDebugAdapterServer>{
|
||||
type: 'server',
|
||||
port: session.configuration.debugServer
|
||||
});
|
||||
}
|
||||
|
||||
// try the new "createDebugAdapterDescriptor" and the deprecated "provideDebugAdapter" API
|
||||
return this.configurationManager.getDebugAdapterDescriptor(session).then(adapter => {
|
||||
|
||||
if (adapter) {
|
||||
return adapter;
|
||||
}
|
||||
|
||||
// try deprecated command based extension API "adapterExecutableCommand" to determine the executable
|
||||
if (this.debuggerContribution.adapterExecutableCommand) {
|
||||
console.info('debugAdapterExecutable attribute in package.json is deprecated and support for it will be removed soon; please use DebugAdapterDescriptorFactory.createDebugAdapterDescriptor instead.');
|
||||
const rootFolder = session.root ? session.root.uri.toString() : undefined;
|
||||
return this.commandService.executeCommand<IDebugAdapterExecutable>(this.debuggerContribution.adapterExecutableCommand, rootFolder).then(ae => {
|
||||
if (ae) {
|
||||
return <IAdapterDescriptor>{
|
||||
type: 'executable',
|
||||
command: ae.command,
|
||||
args: ae.args || []
|
||||
};
|
||||
}
|
||||
throw new Error('command adapterExecutableCommand did not return proper command.');
|
||||
});
|
||||
}
|
||||
|
||||
// fallback: use executable information from package.json
|
||||
const ae = ExecutableDebugAdapter.platformAdapterExecutable(this.mergedExtensionDescriptions, this.type);
|
||||
if (ae === undefined) {
|
||||
throw new Error('no executable specified in package.json');
|
||||
}
|
||||
return ae;
|
||||
});
|
||||
}
|
||||
|
||||
substituteVariables(folder: IWorkspaceFolder | undefined, config: IConfig): Promise<IConfig> {
|
||||
if (this.inExtHost()) {
|
||||
return this.configurationManager.substituteVariables(this.type, folder, config).then(config => {
|
||||
return this.configurationResolverService.resolveWithInteractionReplace(folder, config, 'launch', this.variables);
|
||||
});
|
||||
} else {
|
||||
return this.configurationResolverService.resolveWithInteractionReplace(folder, config, 'launch', this.variables);
|
||||
}
|
||||
}
|
||||
|
||||
runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): Promise<number | undefined> {
|
||||
const config = this.configurationService.getValue<ITerminalSettings>('terminal');
|
||||
return this.configurationManager.runInTerminal(this.inExtHost() ? this.type : '*', args, config);
|
||||
}
|
||||
|
||||
private inExtHost(): boolean {
|
||||
/*
|
||||
const debugConfigs = this.configurationService.getValue<IDebugConfiguration>('debug');
|
||||
return !!debugConfigs.extensionHostDebugAdapter
|
||||
|| this.configurationManager.needsToRunInExtHost(this.type)
|
||||
|| (!!this.mainExtensionDescription && this.mainExtensionDescription.extensionLocation.scheme !== 'file');
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
|
||||
get label(): string {
|
||||
return this.debuggerContribution.label || this.debuggerContribution.type;
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return this.debuggerContribution.type;
|
||||
}
|
||||
|
||||
get variables(): { [key: string]: string } | undefined {
|
||||
return this.debuggerContribution.variables;
|
||||
}
|
||||
|
||||
get configurationSnippets(): IJSONSchemaSnippet[] | undefined {
|
||||
return this.debuggerContribution.configurationSnippets;
|
||||
}
|
||||
|
||||
get languages(): string[] | undefined {
|
||||
return this.debuggerContribution.languages;
|
||||
}
|
||||
|
||||
hasInitialConfiguration(): boolean {
|
||||
return !!this.debuggerContribution.initialConfigurations;
|
||||
}
|
||||
|
||||
hasConfigurationProvider(): boolean {
|
||||
return this.configurationManager.hasDebugConfigurationProvider(this.type);
|
||||
}
|
||||
|
||||
getInitialConfigurationContent(initialConfigs?: IConfig[]): Promise<string> {
|
||||
// at this point we got some configs from the package.json and/or from registered DebugConfigurationProviders
|
||||
let initialConfigurations = this.debuggerContribution.initialConfigurations || [];
|
||||
if (initialConfigs) {
|
||||
initialConfigurations = initialConfigurations.concat(initialConfigs);
|
||||
}
|
||||
|
||||
const eol = this.resourcePropertiesService.getEOL(URI.from({ scheme: Schemas.untitled, path: '1' })) === '\r\n' ? '\r\n' : '\n';
|
||||
const configs = JSON.stringify(initialConfigurations, null, '\t').split('\n').map(line => '\t' + line).join(eol).trim();
|
||||
const comment1 = nls.localize('launch.config.comment1', "Use IntelliSense to learn about possible attributes.");
|
||||
const comment2 = nls.localize('launch.config.comment2', "Hover to view descriptions of existing attributes.");
|
||||
const comment3 = nls.localize('launch.config.comment3', "For more information, visit: {0}", 'https://go.microsoft.com/fwlink/?linkid=830387');
|
||||
|
||||
let content = [
|
||||
'{',
|
||||
`\t// ${comment1}`,
|
||||
`\t// ${comment2}`,
|
||||
`\t// ${comment3}`,
|
||||
`\t"version": "0.2.0",`,
|
||||
`\t"configurations": ${configs}`,
|
||||
'}'
|
||||
].join(eol);
|
||||
|
||||
// fix formatting
|
||||
const editorConfig = this.configurationService.getValue<any>();
|
||||
if (editorConfig.editor && editorConfig.editor.insertSpaces) {
|
||||
content = content.replace(new RegExp('\t', 'g'), strings.repeat(' ', editorConfig.editor.tabSize));
|
||||
}
|
||||
|
||||
return Promise.resolve(content);
|
||||
}
|
||||
|
||||
public getMainExtensionDescriptor(): IExtensionDescription {
|
||||
return this.mainExtensionDescription || this.mergedExtensionDescriptions[0];
|
||||
}
|
||||
|
||||
@memoize
|
||||
getCustomTelemetryService(): Promise<TelemetryService | undefined> {
|
||||
|
||||
const aiKey = this.debuggerContribution.aiKey;
|
||||
|
||||
if (!aiKey) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return this.telemetryService.getTelemetryInfo().then(info => {
|
||||
const telemetryInfo: { [key: string]: string } = Object.create(null);
|
||||
telemetryInfo['common.vscodemachineid'] = info.machineId;
|
||||
telemetryInfo['common.vscodesessionid'] = info.sessionId;
|
||||
return telemetryInfo;
|
||||
}).then(data => {
|
||||
const client = new TelemetryClient(
|
||||
getPathFromAmdModule(require, 'bootstrap-fork'),
|
||||
{
|
||||
serverName: 'Debug Telemetry',
|
||||
timeout: 1000 * 60 * 5,
|
||||
args: [`${this.getMainExtensionDescriptor().publisher}.${this.type}`, JSON.stringify(data), aiKey],
|
||||
env: {
|
||||
ELECTRON_RUN_AS_NODE: 1,
|
||||
PIPE_LOGGING: 'true',
|
||||
AMD_ENTRYPOINT: 'vs/workbench/contrib/debug/node/telemetryApp'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const channel = client.getChannel('telemetryAppender');
|
||||
const appender = new TelemetryAppenderClient(channel);
|
||||
|
||||
return new TelemetryService({ appender }, this.configurationService);
|
||||
});
|
||||
}
|
||||
|
||||
getSchemaAttributes(): IJSONSchema[] | null {
|
||||
|
||||
if (!this.debuggerContribution.configurationAttributes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// fill in the default configuration attributes shared by all adapters.
|
||||
const taskSchema = TaskDefinitionRegistry.getJsonSchema();
|
||||
return Object.keys(this.debuggerContribution.configurationAttributes).map(request => {
|
||||
const attributes: IJSONSchema = this.debuggerContribution.configurationAttributes[request];
|
||||
const defaultRequired = ['name', 'type', 'request'];
|
||||
attributes.required = attributes.required && attributes.required.length ? defaultRequired.concat(attributes.required) : defaultRequired;
|
||||
attributes.additionalProperties = false;
|
||||
attributes.type = 'object';
|
||||
if (!attributes.properties) {
|
||||
attributes.properties = {};
|
||||
}
|
||||
const properties = attributes.properties;
|
||||
properties['type'] = {
|
||||
enum: [this.type],
|
||||
description: nls.localize('debugType', "Type of configuration."),
|
||||
pattern: '^(?!node2)',
|
||||
errorMessage: nls.localize('debugTypeNotRecognised', "The debug type is not recognized. Make sure that you have a corresponding debug extension installed and that it is enabled."),
|
||||
patternErrorMessage: nls.localize('node2NotSupported', "\"node2\" is no longer supported, use \"node\" instead and set the \"protocol\" attribute to \"inspector\".")
|
||||
};
|
||||
properties['name'] = {
|
||||
type: 'string',
|
||||
description: nls.localize('debugName', "Name of configuration; appears in the launch configuration drop down menu."),
|
||||
default: 'Launch'
|
||||
};
|
||||
properties['request'] = {
|
||||
enum: [request],
|
||||
description: nls.localize('debugRequest', "Request type of configuration. Can be \"launch\" or \"attach\"."),
|
||||
};
|
||||
properties['debugServer'] = {
|
||||
type: 'number',
|
||||
description: nls.localize('debugServer', "For debug extension development only: if a port is specified VS Code tries to connect to a debug adapter running in server mode"),
|
||||
default: 4711
|
||||
};
|
||||
properties['preLaunchTask'] = {
|
||||
anyOf: [taskSchema, {
|
||||
type: ['string', 'null'],
|
||||
}],
|
||||
default: '',
|
||||
description: nls.localize('debugPrelaunchTask', "Task to run before debug session starts.")
|
||||
};
|
||||
properties['postDebugTask'] = {
|
||||
anyOf: [taskSchema, {
|
||||
type: ['string', 'null'],
|
||||
}],
|
||||
default: '',
|
||||
description: nls.localize('debugPostDebugTask', "Task to run after debug session ends.")
|
||||
};
|
||||
properties['internalConsoleOptions'] = INTERNAL_CONSOLE_OPTIONS_SCHEMA;
|
||||
// Clear out windows, linux and osx fields to not have cycles inside the properties object
|
||||
delete properties['windows'];
|
||||
delete properties['osx'];
|
||||
delete properties['linux'];
|
||||
|
||||
const osProperties = objects.deepClone(properties);
|
||||
properties['windows'] = {
|
||||
type: 'object',
|
||||
description: nls.localize('debugWindowsConfiguration', "Windows specific launch configuration attributes."),
|
||||
properties: osProperties
|
||||
};
|
||||
properties['osx'] = {
|
||||
type: 'object',
|
||||
description: nls.localize('debugOSXConfiguration', "OS X specific launch configuration attributes."),
|
||||
properties: osProperties
|
||||
};
|
||||
properties['linux'] = {
|
||||
type: 'object',
|
||||
description: nls.localize('debugLinuxConfiguration', "Linux specific launch configuration attributes."),
|
||||
properties: osProperties
|
||||
};
|
||||
Object.keys(properties).forEach(name => {
|
||||
// Use schema allOf property to get independent error reporting #21113
|
||||
ConfigurationResolverUtils.applyDeprecatedVariableMessage(properties[name]);
|
||||
});
|
||||
return attributes;
|
||||
});
|
||||
}
|
||||
}
|
||||
15
src/vs/workbench/contrib/debug/node/telemetryApp.ts
Normal file
15
src/vs/workbench/contrib/debug/node/telemetryApp.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender';
|
||||
import { TelemetryAppenderChannel } from 'vs/platform/telemetry/node/telemetryIpc';
|
||||
|
||||
const appender = new AppInsightsAppender(process.argv[2], JSON.parse(process.argv[3]), process.argv[4]);
|
||||
process.once('exit', () => appender.dispose());
|
||||
|
||||
const channel = new TelemetryAppenderChannel(appender);
|
||||
const server = new Server('telemetry');
|
||||
server.registerChannel('telemetryAppender', channel);
|
||||
421
src/vs/workbench/contrib/debug/node/terminals.ts
Normal file
421
src/vs/workbench/contrib/debug/node/terminals.ts
Normal file
@@ -0,0 +1,421 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as cp from 'child_process';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as env from 'vs/base/common/platform';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
import { ITerminalLauncher, ITerminalSettings } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
|
||||
const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console");
|
||||
|
||||
let terminalLauncher: ITerminalLauncher | undefined = undefined;
|
||||
|
||||
export function getTerminalLauncher() {
|
||||
if (!terminalLauncher) {
|
||||
if (env.isWindows) {
|
||||
terminalLauncher = new WinTerminalService();
|
||||
} else if (env.isMacintosh) {
|
||||
terminalLauncher = new MacTerminalService();
|
||||
} else if (env.isLinux) {
|
||||
terminalLauncher = new LinuxTerminalService();
|
||||
}
|
||||
}
|
||||
return terminalLauncher;
|
||||
}
|
||||
|
||||
let _DEFAULT_TERMINAL_LINUX_READY: Promise<string> | null = null;
|
||||
export function getDefaultTerminalLinuxReady(): Promise<string> {
|
||||
if (!_DEFAULT_TERMINAL_LINUX_READY) {
|
||||
_DEFAULT_TERMINAL_LINUX_READY = new Promise<string>(c => {
|
||||
if (env.isLinux) {
|
||||
Promise.all<any>([pfs.exists('/etc/debian_version'), process.lazyEnv]).then(([isDebian]) => {
|
||||
if (isDebian) {
|
||||
c('x-terminal-emulator');
|
||||
} else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') {
|
||||
c('gnome-terminal');
|
||||
} else if (process.env.DESKTOP_SESSION === 'kde-plasma') {
|
||||
c('konsole');
|
||||
} else if (process.env.COLORTERM) {
|
||||
c(process.env.COLORTERM);
|
||||
} else if (process.env.TERM) {
|
||||
c(process.env.TERM);
|
||||
} else {
|
||||
c('xterm');
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
c('xterm');
|
||||
});
|
||||
}
|
||||
return _DEFAULT_TERMINAL_LINUX_READY;
|
||||
}
|
||||
|
||||
let _DEFAULT_TERMINAL_WINDOWS: string | null = null;
|
||||
export function getDefaultTerminalWindows(): string {
|
||||
if (!_DEFAULT_TERMINAL_WINDOWS) {
|
||||
const isWoW64 = !!process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
|
||||
_DEFAULT_TERMINAL_WINDOWS = `${process.env.windir ? process.env.windir : 'C:\\Windows'}\\${isWoW64 ? 'Sysnative' : 'System32'}\\cmd.exe`;
|
||||
}
|
||||
return _DEFAULT_TERMINAL_WINDOWS;
|
||||
}
|
||||
|
||||
abstract class TerminalLauncher implements ITerminalLauncher {
|
||||
runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): Promise<number | undefined> {
|
||||
return this.runInTerminal0(args.title!, args.cwd, args.args, args.env || {}, config);
|
||||
}
|
||||
|
||||
abstract runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment | {}, config): Promise<number | undefined>;
|
||||
}
|
||||
|
||||
class WinTerminalService extends TerminalLauncher {
|
||||
|
||||
private static readonly CMD = 'cmd.exe';
|
||||
|
||||
runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, configuration: ITerminalSettings): Promise<number | undefined> {
|
||||
|
||||
const exec = configuration.external.windowsExec || getDefaultTerminalWindows();
|
||||
|
||||
return new Promise<number | undefined>((c, e) => {
|
||||
|
||||
const title = `"${dir} - ${TERMINAL_TITLE}"`;
|
||||
const command = `""${args.join('" "')}" & pause"`; // use '|' to only pause on non-zero exit code
|
||||
|
||||
const cmdArgs = [
|
||||
'/c', 'start', title, '/wait', exec, '/c', command
|
||||
];
|
||||
|
||||
// merge environment variables into a copy of the process.env
|
||||
const env = assign({}, process.env, envVars);
|
||||
|
||||
// delete environment variables that have a null value
|
||||
Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]);
|
||||
|
||||
const options: any = {
|
||||
cwd: dir,
|
||||
env: env,
|
||||
windowsVerbatimArguments: true
|
||||
};
|
||||
|
||||
const cmd = cp.spawn(WinTerminalService.CMD, cmdArgs, options);
|
||||
cmd.on('error', e);
|
||||
|
||||
c(undefined);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class MacTerminalService extends TerminalLauncher {
|
||||
|
||||
private static readonly DEFAULT_TERMINAL_OSX = 'Terminal.app';
|
||||
private static readonly OSASCRIPT = '/usr/bin/osascript'; // osascript is the AppleScript interpreter on OS X
|
||||
|
||||
runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, configuration: ITerminalSettings): Promise<number | undefined> {
|
||||
|
||||
const terminalApp = configuration.external.osxExec || MacTerminalService.DEFAULT_TERMINAL_OSX;
|
||||
|
||||
return new Promise<number | undefined>((c, e) => {
|
||||
|
||||
if (terminalApp === MacTerminalService.DEFAULT_TERMINAL_OSX || terminalApp === 'iTerm.app') {
|
||||
|
||||
// On OS X we launch an AppleScript that creates (or reuses) a Terminal window
|
||||
// and then launches the program inside that window.
|
||||
|
||||
const script = terminalApp === MacTerminalService.DEFAULT_TERMINAL_OSX ? 'TerminalHelper' : 'iTermHelper';
|
||||
const scriptpath = getPathFromAmdModule(require, `vs/workbench/contrib/externalTerminal/electron-browser/${script}.scpt`);
|
||||
|
||||
const osaArgs = [
|
||||
scriptpath,
|
||||
'-t', title || TERMINAL_TITLE,
|
||||
'-w', dir,
|
||||
];
|
||||
|
||||
for (let a of args) {
|
||||
osaArgs.push('-a');
|
||||
osaArgs.push(a);
|
||||
}
|
||||
|
||||
if (envVars) {
|
||||
for (let key in envVars) {
|
||||
const value = envVars[key];
|
||||
if (value === null) {
|
||||
osaArgs.push('-u');
|
||||
osaArgs.push(key);
|
||||
} else {
|
||||
osaArgs.push('-e');
|
||||
osaArgs.push(`${key}=${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let stderr = '';
|
||||
const osa = cp.spawn(MacTerminalService.OSASCRIPT, osaArgs);
|
||||
osa.on('error', e);
|
||||
osa.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
osa.on('exit', (code: number) => {
|
||||
if (code === 0) { // OK
|
||||
c(undefined);
|
||||
} else {
|
||||
if (stderr) {
|
||||
const lines = stderr.split('\n', 1);
|
||||
e(new Error(lines[0]));
|
||||
} else {
|
||||
e(new Error(nls.localize('mac.terminal.script.failed', "Script '{0}' failed with exit code {1}", script, code)));
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
e(new Error(nls.localize('mac.terminal.type.not.supported', "'{0}' not supported", terminalApp)));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class LinuxTerminalService extends TerminalLauncher {
|
||||
|
||||
private static readonly WAIT_MESSAGE = nls.localize('press.any.key', "Press any key to continue...");
|
||||
|
||||
runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, configuration: ITerminalSettings): Promise<number | undefined> {
|
||||
|
||||
const terminalConfig = configuration.external;
|
||||
const execThenable: Promise<string> = terminalConfig.linuxExec ? Promise.resolve(terminalConfig.linuxExec) : getDefaultTerminalLinuxReady();
|
||||
|
||||
return new Promise<number | undefined>((c, e) => {
|
||||
|
||||
let termArgs: string[] = [];
|
||||
//termArgs.push('--title');
|
||||
//termArgs.push(`"${TERMINAL_TITLE}"`);
|
||||
execThenable.then(exec => {
|
||||
if (exec.indexOf('gnome-terminal') >= 0) {
|
||||
termArgs.push('-x');
|
||||
} else {
|
||||
termArgs.push('-e');
|
||||
}
|
||||
termArgs.push('bash');
|
||||
termArgs.push('-c');
|
||||
|
||||
const bashCommand = `${quote(args)}; echo; read -p "${LinuxTerminalService.WAIT_MESSAGE}" -n1;`;
|
||||
termArgs.push(`''${bashCommand}''`); // wrapping argument in two sets of ' because node is so "friendly" that it removes one set...
|
||||
|
||||
// merge environment variables into a copy of the process.env
|
||||
const env = assign({}, process.env, envVars);
|
||||
|
||||
// delete environment variables that have a null value
|
||||
Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]);
|
||||
|
||||
const options: any = {
|
||||
cwd: dir,
|
||||
env: env
|
||||
};
|
||||
|
||||
let stderr = '';
|
||||
const cmd = cp.spawn(exec, termArgs, options);
|
||||
cmd.on('error', e);
|
||||
cmd.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
cmd.on('exit', (code: number) => {
|
||||
if (code === 0) { // OK
|
||||
c(undefined);
|
||||
} else {
|
||||
if (stderr) {
|
||||
const lines = stderr.split('\n', 1);
|
||||
e(new Error(lines[0]));
|
||||
} else {
|
||||
e(new Error(nls.localize('linux.term.failed', "'{0}' failed with exit code {1}", exec, code)));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote args if necessary and combine into a space separated string.
|
||||
*/
|
||||
function quote(args: string[]): string {
|
||||
let r = '';
|
||||
for (let a of args) {
|
||||
if (a.indexOf(' ') >= 0) {
|
||||
r += '"' + a + '"';
|
||||
} else {
|
||||
r += a;
|
||||
}
|
||||
r += ' ';
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
export function hasChildProcesses(processId: number): boolean {
|
||||
if (processId) {
|
||||
try {
|
||||
// if shell has at least one child process, assume that shell is busy
|
||||
if (env.isWindows) {
|
||||
const result = cp.spawnSync('wmic', ['process', 'get', 'ParentProcessId']);
|
||||
if (result.stdout) {
|
||||
const pids = result.stdout.toString().split('\r\n');
|
||||
if (!pids.some(p => parseInt(p) === processId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const result = cp.spawnSync('/usr/bin/pgrep', ['-lP', String(processId)]);
|
||||
if (result.stdout) {
|
||||
const r = result.stdout.toString().trim();
|
||||
if (r.length === 0 || r.indexOf(' tmux') >= 0) { // ignore 'tmux'; see #43683
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
// silently ignore
|
||||
}
|
||||
}
|
||||
// fall back to safe side
|
||||
return true;
|
||||
}
|
||||
|
||||
const enum ShellType { cmd, powershell, bash }
|
||||
|
||||
export function prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): string {
|
||||
|
||||
let shellType: ShellType;
|
||||
|
||||
// get the shell configuration for the current platform
|
||||
let shell: string;
|
||||
const shell_config = config.integrated.shell;
|
||||
if (env.isWindows) {
|
||||
shell = shell_config.windows;
|
||||
shellType = ShellType.cmd;
|
||||
} else if (env.isLinux) {
|
||||
shell = shell_config.linux;
|
||||
shellType = ShellType.bash;
|
||||
} else if (env.isMacintosh) {
|
||||
shell = shell_config.osx;
|
||||
shellType = ShellType.bash;
|
||||
} else {
|
||||
throw new Error('Unknown platform');
|
||||
}
|
||||
|
||||
// try to determine the shell type
|
||||
shell = shell.trim().toLowerCase();
|
||||
if (shell.indexOf('powershell') >= 0 || shell.indexOf('pwsh') >= 0) {
|
||||
shellType = ShellType.powershell;
|
||||
} else if (shell.indexOf('cmd.exe') >= 0) {
|
||||
shellType = ShellType.cmd;
|
||||
} else if (shell.indexOf('bash') >= 0) {
|
||||
shellType = ShellType.bash;
|
||||
} else if (shell.indexOf('git\\bin\\bash.exe') >= 0) {
|
||||
shellType = ShellType.bash;
|
||||
}
|
||||
|
||||
let quote: (s: string) => string;
|
||||
let command = '';
|
||||
|
||||
switch (shellType) {
|
||||
|
||||
case ShellType.powershell:
|
||||
|
||||
quote = (s: string) => {
|
||||
s = s.replace(/\'/g, '\'\'');
|
||||
return `'${s}'`;
|
||||
//return s.indexOf(' ') >= 0 || s.indexOf('\'') >= 0 || s.indexOf('"') >= 0 ? `'${s}'` : s;
|
||||
};
|
||||
|
||||
if (args.cwd) {
|
||||
command += `cd '${args.cwd}'; `;
|
||||
}
|
||||
if (args.env) {
|
||||
for (let key in args.env) {
|
||||
const value = args.env[key];
|
||||
if (value === null) {
|
||||
command += `Remove-Item env:${key}; `;
|
||||
} else {
|
||||
command += `\${env:${key}}='${value}'; `;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (args.args && args.args.length > 0) {
|
||||
const cmd = quote(args.args.shift()!);
|
||||
command += (cmd[0] === '\'') ? `& ${cmd} ` : `${cmd} `;
|
||||
for (let a of args.args) {
|
||||
command += `${quote(a)} `;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ShellType.cmd:
|
||||
|
||||
quote = (s: string) => {
|
||||
s = s.replace(/\"/g, '""');
|
||||
return (s.indexOf(' ') >= 0 || s.indexOf('"') >= 0) ? `"${s}"` : s;
|
||||
};
|
||||
|
||||
if (args.cwd) {
|
||||
command += `cd ${quote(args.cwd)} && `;
|
||||
}
|
||||
if (args.env) {
|
||||
command += 'cmd /C "';
|
||||
for (let key in args.env) {
|
||||
let value = args.env[key];
|
||||
if (value === null) {
|
||||
command += `set "${key}=" && `;
|
||||
} else {
|
||||
value = value.replace(/[\^\&]/g, s => `^${s}`);
|
||||
command += `set "${key}=${value}" && `;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let a of args.args) {
|
||||
command += `${quote(a)} `;
|
||||
}
|
||||
if (args.env) {
|
||||
command += '"';
|
||||
}
|
||||
break;
|
||||
|
||||
case ShellType.bash:
|
||||
|
||||
quote = (s: string) => {
|
||||
s = s.replace(/\"/g, '\\"');
|
||||
return (s.indexOf(' ') >= 0 || s.indexOf('\\') >= 0) ? `"${s}"` : s;
|
||||
};
|
||||
|
||||
const hardQuote = (s: string) => {
|
||||
return /[^\w@%\/+=,.:^-]/.test(s) ? `'${s.replace(/'/g, '\'\\\'\'')}'` : s;
|
||||
};
|
||||
|
||||
if (args.cwd) {
|
||||
command += `cd ${quote(args.cwd)} ; `;
|
||||
}
|
||||
if (args.env) {
|
||||
command += 'env';
|
||||
for (let key in args.env) {
|
||||
const value = args.env[key];
|
||||
if (value === null) {
|
||||
command += ` -u ${hardQuote(key)}`;
|
||||
} else {
|
||||
command += ` ${hardQuote(`${key}=${value}`)}`;
|
||||
}
|
||||
}
|
||||
command += ' ';
|
||||
}
|
||||
for (let a of args.args) {
|
||||
command += `${quote(a)} `;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
Reference in New Issue
Block a user