Implements the debugVisualizer.debugAdapterConfigurations configuration option.

This commit is contained in:
Henning Dieterichs
2020-03-02 10:52:19 +01:00
parent f6a554c897
commit f12ea19369
19 changed files with 338 additions and 118 deletions

View File

@@ -1,28 +1,99 @@
import { workspace } from "vscode";
import { Disposable } from "@hediet/std/disposable";
import { EventEmitter, EventSource } from "@hediet/std/events";
import { observable } from "mobx";
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
export const useChromeKioskModeKey = "debugVisualizer.useChromeKioskMode";
const useChromeKioskModeKey = "debugVisualizer.useChromeKioskMode";
const debugAdapterConfigsKey = "debugVisualizer.debugAdapterConfigurations";
export class Config {
private changeEventEmitter = new EventEmitter();
public readonly onChange: EventSource = this.changeEventEmitter;
public dispose = Disposable.fn();
@observable
private _useChromeKioskMode!: boolean;
@observable
private _debugAdapterConfigs!: DebugAdapterConfigs;
public get useChromeKioskMode(): boolean {
return this._useChromeKioskMode;
}
public get debugAdapterConfigs(): DebugAdapterConfigs {
return this._debugAdapterConfigs;
}
constructor() {
this.updateConfig();
this.dispose.track(
workspace.onDidChangeConfiguration(() => {
this.changeEventEmitter.emit();
this.updateConfig();
})
);
}
public useChromeKioskMode(): boolean {
private updateConfig(): void {
const c = workspace.getConfiguration();
const b = c.get<boolean>(useChromeKioskModeKey);
if (b === undefined) {
return true;
this._useChromeKioskMode = mapUndefined(
c.get<boolean>(useChromeKioskModeKey),
true
);
this._debugAdapterConfigs = mapUndefined(
c.get<DebugAdapterConfigs>(debugAdapterConfigsKey),
{}
);
}
public getDebugAdapterConfig(
debugAdapterType: string
): DebugAdapterConfig | undefined {
const c = this.debugAdapterConfigs[debugAdapterType];
if (!c) {
return undefined;
}
return b;
return {
context: c.context || "watch",
getFinalExpression: ({ expression, preferredExtractorId }) =>
evaluateTemplate(c.expressionTemplate || "${expr}", {
expr: expression,
preferredDataExtractorId: preferredExtractorId || "",
}),
};
}
}
function mapUndefined<T>(val: T | undefined, defaultVal: T) {
if (val === undefined) {
return defaultVal;
}
return val;
}
type DebugAdapterConfigs = {
[debugAdapter: string]: {
context?: "watch" | "repl";
expressionTemplate?: "string";
};
};
export interface DebugAdapterConfig {
context: "watch" | "repl";
getFinalExpression(vars: {
expression: string;
preferredExtractorId: DataExtractorId | undefined;
}): string;
}
function evaluateTemplate(
template: string,
data: Record<string, string>
): string {
let result = template;
for (const [key, val] of Object.entries(data)) {
result = result.split(`\${${key}}`).join(val);
}
return result;
}

View File

@@ -76,7 +76,7 @@ export class ConnectionHandler {
});
let opened = false;
if (config.useChromeKioskMode()) {
if (config.useChromeKioskMode) {
opened = await launchChrome(url);
}
if (!opened) {

View File

@@ -0,0 +1,49 @@
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
import { VsCodeDebugSession } from "../../VsCodeDebugger";
import {
DataExtractionProviderFactory,
DataExtractionProvider,
} from "./DataExtractionProvider";
import { Config, DebugAdapterConfig } from "../../Config";
import { GenericDataExtractionProvider } from "./GenericDataExtractionProviderFactory";
export class ConfiguredDataExtractionProviderFactory
implements DataExtractionProviderFactory {
constructor(private readonly config: Config) {}
createDataExtractionProvider(
session: VsCodeDebugSession
): DataExtractionProvider | undefined {
const config = this.config.getDebugAdapterConfig(session.session.type);
if (!config) {
return undefined;
}
return new ConfiguredDataExtractionProvider(session, config);
}
}
class ConfiguredDataExtractionProvider extends GenericDataExtractionProvider {
constructor(
session: VsCodeDebugSession,
private readonly config: DebugAdapterConfig
) {
super(session);
}
protected getContext() {
return this.config.context;
}
protected getFinalExpression({
expression,
preferredExtractorId,
}: {
expression: string;
preferredExtractorId: DataExtractorId | undefined;
}) {
return this.config.getFinalExpression({
expression,
preferredExtractorId,
});
}
}

View File

@@ -1,4 +1,7 @@
import { DataExtractionResult } from "@hediet/debug-visualizer-data-extraction";
import {
DataExtractionResult,
DataExtractorId,
} from "@hediet/debug-visualizer-data-extraction";
import { VsCodeDebugSession } from "../../VsCodeDebugger";
export interface DataExtractionProviderFactory {
@@ -18,6 +21,6 @@ export interface DataExtractionProvider {
export interface DataExtractionProviderArgs {
expression: string;
preferredExtractor: string | undefined;
preferredExtractorId: DataExtractorId | undefined;
frameId: number | undefined;
}

View File

@@ -1,8 +1,6 @@
import {
DataResult,
DataExtractionResult,
ExtractedData,
isExtractedData,
DataExtractorId,
} from "@hediet/debug-visualizer-data-extraction";
import { VsCodeDebugSession } from "../../VsCodeDebugger";
import {
@@ -10,6 +8,7 @@ import {
DataExtractionProvider,
DataExtractionProviderArgs,
} from "./DataExtractionProvider";
import { parseEvaluationResultFromGenericDebugAdapter } from "./parseEvaluationResultFromGenericDebugAdapter";
export class GenericDataExtractionProviderFactory
implements DataExtractionProviderFactory {
@@ -20,84 +19,46 @@ export class GenericDataExtractionProviderFactory
}
}
class GenericDataExtractionProvider implements DataExtractionProvider {
export class GenericDataExtractionProvider implements DataExtractionProvider {
constructor(private readonly session: VsCodeDebugSession) {}
public async evaluate({
expression,
preferredExtractor,
preferredExtractorId,
frameId,
}: DataExtractionProviderArgs): Promise<
| { kind: "data"; result: DataExtractionResult }
| { kind: "error"; message: string }
> {
const finalExpression = this.getFinalExpression({
expression,
preferredExtractorId,
});
let reply;
try {
const reply = await this.session.evaluate({
expression,
reply = await this.session.evaluate({
expression: finalExpression,
frameId,
context: this.getContext(),
});
function isEnclosedWith(str: string, char: string): boolean {
return str.startsWith(char) && str.endsWith(char);
}
const jsonData = reply.result.trim();
function parseJson(str: string) {
try {
return JSON.parse(str);
} catch (error) {
throw new Error(
`Could not parse \`${str}\` as JSON.\n${error.message}`
);
}
}
let result;
try {
let jsonData2;
// Remove optionally enclosing characters.
if (
isEnclosedWith(jsonData, '"') ||
isEnclosedWith(jsonData, "'")
) {
// In case of JavaScript: `"{ "kind": { ... }, "text": "some\ntext" }"`
jsonData2 = jsonData.substr(1, jsonData.length - 2);
} else {
// Just in case no quoting is done.
jsonData2 = jsonData;
}
result = parseJson(jsonData2);
} catch (e) {
// in case of C++: `"{ \"kind\": { ... }, \"text\": \"some\\ntext\" }"`
const str = parseJson(jsonData);
// str is now `{ "kind": { ... }, "text": "some\ntext" }"`
result = parseJson(str);
// result is now { kind: { ... }, text: "some\ntext" }
}
if (!isExtractedData(result)) {
throw new Error("Data does not match ExtractedData interface.");
}
return {
kind: "data",
result: {
availableExtractors: [],
usedExtractor: {
id: "generic" as any,
name: "Generic",
priority: 1,
},
data: result,
},
};
} catch (error) {
return {
kind: "error",
message: error.message,
};
}
return parseEvaluationResultFromGenericDebugAdapter(reply.result);
}
protected getFinalExpression(args: {
expression: string;
preferredExtractorId: DataExtractorId | undefined;
}): string {
return args.expression;
}
protected getContext(): "watch" | "repl" {
return "watch";
}
}

View File

@@ -35,7 +35,7 @@ class JsDataEvaluator implements DataExtractionProvider {
public async evaluate({
expression,
preferredExtractor,
preferredExtractorId,
frameId,
}: DataExtractionProviderArgs): Promise<
| { kind: "data"; result: DataExtractionResult }
@@ -43,8 +43,8 @@ class JsDataEvaluator implements DataExtractionProvider {
> {
while (true) {
try {
const preferredExtractorExpr = preferredExtractor
? `"${preferredExtractor}"`
const preferredExtractorExpr = preferredExtractorId
? `"${preferredExtractorId}"`
: "undefined";
const body = `${getExpressionForDataExtractorApi()}.getData(

View File

@@ -1,12 +1,6 @@
export * from "./DataExtractionProvider";
import { ComposedDataExtractionProviderFactory } from "./ComposedDataExtractionProviderFactory";
import { JsDataExtractionProviderFactory } from "./JsDataExtractionProviderFactory";
import { GenericDataExtractionProviderFactory } from "./GenericDataExtractionProviderFactory";
export const defaultDataExtractionProviderFactory = new ComposedDataExtractionProviderFactory(
[
new JsDataExtractionProviderFactory(),
new GenericDataExtractionProviderFactory(),
]
);
export { ComposedDataExtractionProviderFactory } from "./ComposedDataExtractionProviderFactory";
export { JsDataExtractionProviderFactory } from "./JsDataExtractionProviderFactory";
export { GenericDataExtractionProviderFactory } from "./GenericDataExtractionProviderFactory";
export { ConfiguredDataExtractionProviderFactory } from "./ConfiguredDataExtractionProviderFactory";

View File

@@ -0,0 +1,76 @@
import {
DataExtractionResult,
isExtractedData,
} from "@hediet/debug-visualizer-data-extraction";
export function parseEvaluationResultFromGenericDebugAdapter(
resultText: string
):
| { kind: "data"; result: DataExtractionResult }
| { kind: "error"; message: string } {
const jsonData = resultText.trim();
let resultObj;
try {
try {
let jsonData2;
// Remove optionally enclosing characters.
if (
isEnclosedWith(jsonData, '"') ||
isEnclosedWith(jsonData, "'")
) {
// In case of JavaScript: `"{ "kind": { ... }, "text": "some\ntext" }"`
jsonData2 = jsonData.substr(1, jsonData.length - 2);
} else {
// Just in case no quoting is done.
jsonData2 = jsonData;
}
resultObj = parseJson(jsonData2);
} catch (e) {
// in case of C++: `"{ \"kind\": { ... }, \"text\": \"some\\ntext\" }"`
const str = parseJson(jsonData);
// str is now `{ "kind": { ... }, "text": "some\ntext" }"`
resultObj = parseJson(str);
// result is now { kind: { ... }, text: "some\ntext" }
}
if (!isExtractedData(resultObj)) {
return {
kind: "error",
message: "Data does not match ExtractedData interface.",
};
}
} catch (e) {
return {
kind: "error",
message: e.message,
};
}
return {
kind: "data",
result: {
availableExtractors: [],
usedExtractor: {
id: "generic" as any,
name: "Generic",
priority: 1,
},
data: resultObj,
},
};
}
function isEnclosedWith(str: string, char: string): boolean {
return str.startsWith(char) && str.endsWith(char);
}
function parseJson(str: string) {
try {
return JSON.parse(str);
} catch (error) {
throw new Error(
`Could not parse \`${str}\` as JSON.\n${error.message}`
);
}
}

View File

@@ -73,7 +73,7 @@ export class DataSourceImpl implements DataSource {
const result = await extractionProvider.evaluate({
expression: w.expression,
frameId,
preferredExtractor: w.preferredDataExtractor,
preferredExtractorId: w.preferredDataExtractor,
});
w._state = result;

View File

@@ -1,17 +0,0 @@
import { Disposable } from "@hediet/std/disposable";
import { VsCodeDebugger, VsCodeDebuggerView } from "../VsCodeDebugger";
import { DataSourceImpl } from "./DataSourceImpl";
import { defaultDataExtractionProviderFactory } from "./DataExtractionProvider";
export class Sources {
public readonly dispose = Disposable.fn();
private readonly debugger = this.dispose.track(new VsCodeDebugger());
private readonly debuggerView = this.dispose.track(
new VsCodeDebuggerView(this.debugger)
);
public readonly dataSource = new DataSourceImpl(
this.debuggerView,
defaultDataExtractionProviderFactory
);
}

View File

@@ -1 +1 @@
export * from "./Sources";
export * from "./DataSourceImpl";

View File

@@ -2,19 +2,19 @@ import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
import { AddressInfo } from "net";
import WebSocket = require("ws");
import { ConnectionHandler } from "./ConnectionHandler";
import { Sources } from "./DataSource";
import * as express from "express";
import * as http from "http";
import * as serveStatic from "serve-static";
import { Config } from "./Config";
import cryptoRandomString = require("crypto-random-string");
import { distPath } from "debug-visualizer-webview";
import { DataSource } from "./DataSource/DataSource";
export class Server {
private server: http.Server;
public readonly secret = cryptoRandomString({ length: 20 });
constructor(sources: Sources, config: Config) {
constructor(dataSource: DataSource, config: Config) {
const app = express();
app.use(serveStatic(distPath));
@@ -31,7 +31,7 @@ export class Server {
wss.on("connection", ws => {
const stream = new WebSocketStream(ws);
new ConnectionHandler(
sources.dataSource,
dataSource,
stream,
this,
config,

View File

@@ -162,11 +162,12 @@ export class VsCodeDebugSession {
public async evaluate(args: {
expression: string;
frameId: number | undefined;
context?: "watch" | "repl";
}): Promise<{ result: string }> {
const reply = await this.session.customRequest("evaluate", {
expression: args.expression,
frameId: args.frameId,
context: "watch",
context: args.context || "watch",
});
return { result: reply.result };
}

View File

@@ -15,14 +15,35 @@ registerUpdateReconciler(module);
import { WebViews } from "./WebViews";
import { Server } from "./Server";
import { Config } from "./Config";
import { Sources } from "./DataSource";
import { VsCodeDebugger, VsCodeDebuggerView } from "./VsCodeDebugger";
import { DataSourceImpl } from "./DataSource";
import {
ComposedDataExtractionProviderFactory,
JsDataExtractionProviderFactory,
GenericDataExtractionProviderFactory,
ConfiguredDataExtractionProviderFactory,
} from "./DataSource/DataExtractionProvider";
export class Extension {
public dispose = Disposable.fn();
public readonly dispose = Disposable.fn();
private readonly config = new Config();
private readonly sources = this.dispose.track(new Sources());
private readonly server = new Server(this.sources, this.config);
private readonly debugger = this.dispose.track(new VsCodeDebugger());
private readonly debuggerView = this.dispose.track(
new VsCodeDebuggerView(this.debugger)
);
public readonly dataSource = new DataSourceImpl(
this.debuggerView,
new ComposedDataExtractionProviderFactory([
new ConfiguredDataExtractionProviderFactory(this.config),
new JsDataExtractionProviderFactory(),
new GenericDataExtractionProviderFactory(),
])
);
private readonly server = new Server(this.dataSource, this.config);
private readonly views = this.dispose.track(new WebViews(this.server));
constructor() {