241 lines
7.5 KiB
TypeScript
241 lines
7.5 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as vscode from 'vscode';
|
|
import * as util from 'util';
|
|
import * as nls from 'vscode-nls';
|
|
|
|
const localize = nls.loadMessageBundle();
|
|
|
|
const PATTERN = 'listening on.* (https?://\\S+|[0-9]+)'; // matches "listening on port 3000" or "Now listening on: https://localhost:5001"
|
|
const URI_PORT_FORMAT = 'http://localhost:%s';
|
|
const URI_FORMAT = '%s';
|
|
const WEB_ROOT = '${workspaceFolder}';
|
|
|
|
interface ServerReadyAction {
|
|
pattern: string;
|
|
action?: 'openExternally' | 'debugWithChrome' | 'startDebugging';
|
|
uriFormat?: string;
|
|
webRoot?: string;
|
|
name?: string;
|
|
}
|
|
|
|
class ServerReadyDetector extends vscode.Disposable {
|
|
|
|
private static detectors = new Map<vscode.DebugSession, ServerReadyDetector>();
|
|
private static terminalDataListener: vscode.Disposable | undefined;
|
|
|
|
private hasFired = false;
|
|
private shellPid?: number;
|
|
private regexp: RegExp;
|
|
private disposables: vscode.Disposable[] = [];
|
|
|
|
static start(session: vscode.DebugSession): ServerReadyDetector | undefined {
|
|
if (session.configuration.serverReadyAction) {
|
|
let detector = ServerReadyDetector.detectors.get(session);
|
|
if (!detector) {
|
|
detector = new ServerReadyDetector(session);
|
|
ServerReadyDetector.detectors.set(session, detector);
|
|
}
|
|
return detector;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
static stop(session: vscode.DebugSession): void {
|
|
let detector = ServerReadyDetector.detectors.get(session);
|
|
if (detector) {
|
|
ServerReadyDetector.detectors.delete(session);
|
|
detector.dispose();
|
|
}
|
|
}
|
|
|
|
static rememberShellPid(session: vscode.DebugSession, pid: number) {
|
|
let detector = ServerReadyDetector.detectors.get(session);
|
|
if (detector) {
|
|
detector.shellPid = pid;
|
|
}
|
|
}
|
|
|
|
static async startListeningTerminalData() {
|
|
if (!this.terminalDataListener) {
|
|
this.terminalDataListener = vscode.window.onDidWriteTerminalData(async e => {
|
|
|
|
// first find the detector with a matching pid
|
|
const pid = await e.terminal.processId;
|
|
for (let [, detector] of this.detectors) {
|
|
if (detector.shellPid === pid) {
|
|
detector.detectPattern(e.data);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// if none found, try all detectors until one matches
|
|
for (let [, detector] of this.detectors) {
|
|
if (detector.detectPattern(e.data)) {
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private constructor(private session: vscode.DebugSession) {
|
|
super(() => this.internalDispose());
|
|
|
|
this.regexp = new RegExp(session.configuration.serverReadyAction.pattern || PATTERN, 'i');
|
|
}
|
|
|
|
private internalDispose() {
|
|
this.disposables.forEach(d => d.dispose());
|
|
this.disposables = [];
|
|
}
|
|
|
|
detectPattern(s: string): boolean {
|
|
if (!this.hasFired) {
|
|
const matches = this.regexp.exec(s);
|
|
if (matches && matches.length >= 1) {
|
|
this.openExternalWithString(this.session, matches.length > 1 ? matches[1] : '');
|
|
this.hasFired = true;
|
|
this.internalDispose();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private openExternalWithString(session: vscode.DebugSession, captureString: string) {
|
|
|
|
const args: ServerReadyAction = session.configuration.serverReadyAction;
|
|
|
|
let uri;
|
|
if (captureString === '') {
|
|
// nothing captured by reg exp -> use the uriFormat as the target uri without substitution
|
|
// verify that format does not contain '%s'
|
|
const format = args.uriFormat || '';
|
|
if (format.indexOf('%s') >= 0) {
|
|
const errMsg = localize('server.ready.nocapture.error', "Format uri ('{0}') uses a substitution placeholder but pattern did not capture anything.", format);
|
|
vscode.window.showErrorMessage(errMsg, { modal: true }).then(_ => undefined);
|
|
return;
|
|
}
|
|
uri = format;
|
|
} else {
|
|
// if no uriFormat is specified guess the appropriate format based on the captureString
|
|
const format = args.uriFormat || (/^[0-9]+$/.test(captureString) ? URI_PORT_FORMAT : URI_FORMAT);
|
|
// verify that format only contains a single '%s'
|
|
const s = format.split('%s');
|
|
if (s.length !== 2) {
|
|
const errMsg = localize('server.ready.placeholder.error', "Format uri ('{0}') must contain exactly one substitution placeholder.", format);
|
|
vscode.window.showErrorMessage(errMsg, { modal: true }).then(_ => undefined);
|
|
return;
|
|
}
|
|
uri = util.format(format, captureString);
|
|
}
|
|
|
|
this.openExternalWithUri(session, uri);
|
|
}
|
|
|
|
private openExternalWithUri(session: vscode.DebugSession, uri: string) {
|
|
|
|
const args: ServerReadyAction = session.configuration.serverReadyAction;
|
|
switch (args.action || 'openExternally') {
|
|
|
|
case 'openExternally':
|
|
vscode.env.openExternal(vscode.Uri.parse(uri));
|
|
break;
|
|
|
|
case 'debugWithChrome':
|
|
vscode.debug.startDebugging(session.workspaceFolder, {
|
|
type: 'pwa-chrome',
|
|
name: 'Chrome Debug',
|
|
request: 'launch',
|
|
url: uri,
|
|
webRoot: args.webRoot || WEB_ROOT
|
|
});
|
|
break;
|
|
|
|
case 'startDebugging':
|
|
vscode.debug.startDebugging(session.workspaceFolder, args.name || 'unspecified');
|
|
break;
|
|
|
|
default:
|
|
// not supported
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function activate(context: vscode.ExtensionContext) {
|
|
|
|
context.subscriptions.push(vscode.debug.onDidChangeActiveDebugSession(session => {
|
|
if (session && session.configuration.serverReadyAction) {
|
|
const detector = ServerReadyDetector.start(session);
|
|
if (detector) {
|
|
ServerReadyDetector.startListeningTerminalData();
|
|
}
|
|
}
|
|
}));
|
|
|
|
context.subscriptions.push(vscode.debug.onDidTerminateDebugSession(session => {
|
|
ServerReadyDetector.stop(session);
|
|
}));
|
|
|
|
const trackers = new Set<string>();
|
|
|
|
context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('*', {
|
|
resolveDebugConfigurationWithSubstitutedVariables(_folder: vscode.WorkspaceFolder | undefined, debugConfiguration: vscode.DebugConfiguration) {
|
|
if (debugConfiguration.type && debugConfiguration.serverReadyAction) {
|
|
if (!trackers.has(debugConfiguration.type)) {
|
|
trackers.add(debugConfiguration.type);
|
|
startTrackerForType(context, debugConfiguration.type);
|
|
}
|
|
}
|
|
return debugConfiguration;
|
|
}
|
|
}));
|
|
}
|
|
|
|
function startTrackerForType(context: vscode.ExtensionContext, type: string) {
|
|
|
|
// scan debug console output for a PORT message
|
|
context.subscriptions.push(vscode.debug.registerDebugAdapterTrackerFactory(type, {
|
|
createDebugAdapterTracker(session: vscode.DebugSession) {
|
|
const detector = ServerReadyDetector.start(session);
|
|
if (detector) {
|
|
let runInTerminalRequestSeq: number | undefined;
|
|
return {
|
|
onDidSendMessage: m => {
|
|
if (m.type === 'event' && m.event === 'output' && m.body) {
|
|
switch (m.body.category) {
|
|
case 'console':
|
|
case 'stderr':
|
|
case 'stdout':
|
|
if (m.body.output) {
|
|
detector.detectPattern(m.body.output);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
if (m.type === 'request' && m.command === 'runInTerminal' && m.arguments) {
|
|
if (m.arguments.kind === 'integrated') {
|
|
runInTerminalRequestSeq = m.seq; // remember this to find matching response
|
|
}
|
|
}
|
|
},
|
|
onWillReceiveMessage: m => {
|
|
if (runInTerminalRequestSeq && m.type === 'response' && m.command === 'runInTerminal' && m.body && runInTerminalRequestSeq === m.request_seq) {
|
|
runInTerminalRequestSeq = undefined;
|
|
ServerReadyDetector.rememberShellPid(session, m.body.shellProcessId);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
return undefined;
|
|
}
|
|
}));
|
|
}
|