Implements GenericDataExtractionProviderFactory. With this, the debug visualizer can be used with every language that can be debugged in VS Code.

This commit is contained in:
Henning Dieterichs
2020-02-18 03:04:59 +01:00
parent 5a4678b876
commit 5bc4347f3c
15 changed files with 466 additions and 253 deletions

View File

@@ -4,7 +4,9 @@ export type DataExtractionResult = {
availableExtractors: DataExtractorInfo[];
};
// Instances must be valid json values.
/**
* Instances must be valid json values.
*/
export type ExtractedData = {
kind: Record<string, true>;
};
@@ -18,3 +20,16 @@ export type DataExtractorInfo = {
export type DataExtractorId = {
__brand: "DataExtractorId";
} & string;
export function isExtractedData(val: unknown): val is ExtractedData {
if (typeof val !== "object" || !val || !("kind" in val)) {
return false;
}
const obj = val as any;
if (typeof obj.kind !== "object" || !obj.kind) {
return false;
}
return Object.values(obj.kind).every(val => val === true);
}

View File

@@ -1,4 +1,4 @@
import { ExtractedData } from "../../../DataExtractionResult";
import { ExtractedData, isExtractedData } from "../../../DataExtractionResult";
import {
DataExtractor,
ExtractionCollector,
@@ -12,17 +12,7 @@ export class AsIsDataExtractor implements DataExtractor<ExtractedData> {
extractionCollector: ExtractionCollector<ExtractedData>,
context: DataExtractorContext
): void {
if (typeof data !== "object" || !data || !("kind" in data)) {
return;
}
const obj = data as any;
if (typeof obj.kind !== "object" || !obj.kind) {
return;
}
const areAllTrue = Object.values(obj.kind).every(val => val === true);
if (!areAllTrue) {
if (!isExtractedData(data)) {
return;
}
@@ -31,7 +21,7 @@ export class AsIsDataExtractor implements DataExtractor<ExtractedData> {
name: "As Is",
priority: 500,
extractData() {
return obj;
return data;
},
});
}

22
example-project/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,22 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9000
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 9000
}
]
}

View File

@@ -0,0 +1,18 @@
<?php
for ($i = 0; $i < 100; $i++) {
// { kind: { graph: true }, nodes: [ { id: "1", label: "1" }, { id: "2", label: "2" } ], edges: [{ from: "1", to: "2", label: "edge" }]}
$data = json_encode(array(
"kind" => array("graph" => true),
"nodes" => [
array("id" => "1", "label" => "1"),
array("id" => "2", "label" => "2"),
],
"edges" => [
array("from" => "1", "to" => "2", "label" => "edge")
]
));
echo "test" . $i;
}

View File

@@ -1,10 +1,9 @@
import { Disposable } from "@hediet/std/disposable";
import { debugVisualizerUIContract } from "@hediet/debug-visualizer-vscode-shared";
import { ConsoleRpcLogger, RequestHandlingError } from "@hediet/typed-json-rpc";
import { EvaluationWatcher } from "./DataSource/DataSource";
import { EvaluationWatcher, DataSource } from "./DataSource/DataSource";
import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
import { observable, autorun } from "mobx";
import { Sources } from "./DataSource";
import { Server } from "./Server";
import * as open from "open";
import chromeLauncher = require("chrome-launcher");
@@ -16,7 +15,7 @@ export class ConnectionHandler {
private watcher: EvaluationWatcher | undefined = undefined;
constructor(
sources: Sources,
dataSource: DataSource,
stream: WebSocketStream,
server: Server,
config: Config,
@@ -61,12 +60,9 @@ export class ConnectionHandler {
this.dispose.untrack(this.watcher).dispose();
}
this.watcher = this.dispose.track(
sources.jsSource.createEvaluationWatcher(
newExpression,
{
preferredDataExtractor: oldPreferredDataExtractor,
}
)
dataSource.createEvaluationWatcher(newExpression, {
preferredDataExtractor: oldPreferredDataExtractor,
})
);
},
openInBrowser: async ({}) => {
@@ -96,7 +92,7 @@ export class ConnectionHandler {
getCompletions: async ({ text, column }) => {
throwIfNotAuthenticated();
const completions = await sources.jsSource.getCompletions(
const completions = await dataSource.getCompletions(
text,
column
);

View File

@@ -0,0 +1,22 @@
import {
DataExtractionProviderFactory,
DataExtractionProvider,
} from "./DataExtractionProvider";
import { VsCodeDebugSession } from "../../VsCodeDebugger";
export class ComposedDataExtractionProviderFactory
implements DataExtractionProviderFactory {
constructor(public readonly factories: DataExtractionProviderFactory[]) {}
createDataExtractionProvider(
session: VsCodeDebugSession
): DataExtractionProvider | undefined {
for (const f of this.factories) {
const provider = f.createDataExtractionProvider(session);
if (provider) {
return provider;
}
}
return undefined;
}
}

View File

@@ -0,0 +1,23 @@
import { DataExtractionResult } from "@hediet/debug-visualizer-data-extraction";
import { VsCodeDebugSession } from "../../VsCodeDebugger";
export interface DataExtractionProviderFactory {
createDataExtractionProvider(
session: VsCodeDebugSession
): DataExtractionProvider | undefined;
}
export interface DataExtractionProvider {
evaluate(
args: DataExtractionProviderArgs
): Promise<
| { kind: "data"; result: DataExtractionResult }
| { kind: "error"; message: string }
>;
}
export interface DataExtractionProviderArgs {
expression: string;
preferredExtractor: string | undefined;
frameId: number | undefined;
}

View File

@@ -0,0 +1,66 @@
import {
DataResult,
DataExtractionResult,
ExtractedData,
isExtractedData,
} from "@hediet/debug-visualizer-data-extraction";
import { VsCodeDebugSession } from "../../VsCodeDebugger";
import {
DataExtractionProviderFactory,
DataExtractionProvider,
DataExtractionProviderArgs,
} from "./DataExtractionProvider";
export class GenericDataExtractionProviderFactory
implements DataExtractionProviderFactory {
createDataExtractionProvider(
session: VsCodeDebugSession
): DataExtractionProvider | undefined {
return new GenericDataExtractionProvider(session);
}
}
class GenericDataExtractionProvider implements DataExtractionProvider {
constructor(private readonly session: VsCodeDebugSession) {}
public async evaluate({
expression,
preferredExtractor,
frameId,
}: DataExtractionProviderArgs): Promise<
| { kind: "data"; result: DataExtractionResult }
| { kind: "error"; message: string }
> {
try {
const reply = await this.session.evaluate({
expression,
frameId,
});
const resultStr = reply.result;
const jsonData = resultStr.substr(1, resultStr.length - 2);
const result = JSON.parse(jsonData) as ExtractedData;
if (!isExtractedData(result)) {
throw new Error("Invalid Data");
}
return {
kind: "data",
result: {
availableExtractors: [],
usedExtractor: {
id: "generic" as any,
name: "Generic",
priority: 1,
},
data: result,
},
};
} catch (error) {
return {
kind: "error",
message: error.message,
};
}
}
}

View File

@@ -0,0 +1,121 @@
import {
getExpressionForDataExtractorApi,
DataResult,
ApiHasNotBeenInitializedCode,
getExpressionToInitializeDataExtractorApi,
DataExtractionResult,
} from "@hediet/debug-visualizer-data-extraction";
import { VsCodeDebugSession } from "../../VsCodeDebugger";
import {
DataExtractionProviderFactory,
DataExtractionProvider,
DataExtractionProviderArgs,
} from "./DataExtractionProvider";
export class JsDataExtractionProviderFactory
implements DataExtractionProviderFactory {
createDataExtractionProvider(
session: VsCodeDebugSession
): DataExtractionProvider | undefined {
const supportedDebugAdapters = [
"node",
"node2",
"extensionHost",
"chrome",
];
if (supportedDebugAdapters.indexOf(session.session.type) !== -1) {
return new JsDataEvaluator(session);
}
return undefined;
}
}
class JsDataEvaluator implements DataExtractionProvider {
constructor(private readonly session: VsCodeDebugSession) {}
public async evaluate({
expression,
preferredExtractor,
frameId,
}: DataExtractionProviderArgs): Promise<
| { kind: "data"; result: DataExtractionResult }
| { kind: "error"; message: string }
> {
while (true) {
try {
const preferredExtractorExpr = preferredExtractor
? `"${preferredExtractor}"`
: "undefined";
const body = `${getExpressionForDataExtractorApi()}.getData(
e => (${expression}),
expr => eval(expr),
${preferredExtractorExpr}
)`;
const wrappedExpr = `
(() => {
try {
return ${body};
} catch (e) {
return JSON.stringify({
kind: "Error",
message: e.message,
stack: e.stack
});
}
})()
`;
const reply = await this.session.evaluate({
expression: wrappedExpr,
frameId,
});
const resultStr = reply.result;
const jsonData = resultStr.substr(1, resultStr.length - 2);
const result = JSON.parse(jsonData) as DataResult;
if (result.kind === "NoExtractors") {
throw new Error("No extractors");
} else if (result.kind === "Error") {
throw new Error(result.message);
} else if (result.kind === "Data") {
return {
kind: "data",
result: result.extractionResult,
};
} else {
throw new Error("Invalid Data");
}
} catch (error) {
const msg = error.message as string | undefined;
if (msg && msg.includes(ApiHasNotBeenInitializedCode)) {
if (await this.initializeApi(frameId)) {
continue;
}
}
return {
kind: "error",
message: error.message,
};
}
}
}
private async initializeApi(frameId: number | undefined): Promise<boolean> {
try {
// prefer existing is true, so that manually registered (possibly newer) extractors are not overwritten.
const expression = `${getExpressionToInitializeDataExtractorApi()}.registerDefaultExtractors(true);`;
await this.session.evaluate({
expression,
frameId,
});
return true;
} catch (error) {
return false;
}
}
}

View File

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

View File

@@ -4,30 +4,27 @@ import {
CompletionItem,
} from "@hediet/debug-visualizer-vscode-shared";
export interface EvaluationWatcherOptions {
preferredDataExtractor?: DataExtractorId | undefined;
}
export interface DataSource {
createEvaluationWatcher(
expression: string,
options: EvaluationWatcherOptions
): EvaluationWatcher;
getCompletions(text: string, column: number): Promise<CompletionItem[]>;
}
export interface EvaluationWatcherOptions {
preferredDataExtractor?: DataExtractorId | undefined;
}
export interface EvaluationWatcher {
readonly expression: string;
// is observable
/** This field is observable */
readonly state: DataExtractionState;
/** This field is observable */
readonly preferredDataExtractor: DataExtractorId | undefined;
setPreferredDataExtractor(id: DataExtractorId | undefined): void;
refresh(): void;
// is observable
state: DataExtractionState;
dispose(): void;
}

View File

@@ -0,0 +1,136 @@
import {
DataSource,
EvaluationWatcher,
EvaluationWatcherOptions,
} from "./DataSource";
import { observable, autorun, action } from "mobx";
import { Disposable } from "@hediet/std/disposable";
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
import {
DataExtractionState,
CompletionItem,
} from "@hediet/debug-visualizer-vscode-shared";
import { hotClass } from "@hediet/node-reload";
import { VsCodeDebuggerView } from "../VsCodeDebugger";
import { DataExtractionProviderFactory } from "./DataExtractionProvider/DataExtractionProvider";
@hotClass(module)
export class DataSourceImpl implements DataSource {
public readonly dispose = Disposable.fn();
private readonly watchers = new Set<ObservableEvaluationWatcher>();
constructor(
private readonly vsCodeDebuggerView: VsCodeDebuggerView,
private readonly dataExtractionProviderFactory: DataExtractionProviderFactory
) {
this.dispose.track({
dispose: autorun(() => {
if (
vsCodeDebuggerView.activeDebugSession &&
vsCodeDebuggerView.activeFrameId !== undefined
) {
for (const w of this.watchers) {
w.refresh();
}
}
}),
});
}
public createEvaluationWatcher(
expression: string,
options: EvaluationWatcherOptions
): EvaluationWatcher {
const w = new ObservableEvaluationWatcher(expression, this, options);
this.watchers.add(w);
this.refresh(w);
return w;
}
public removeWatcher(w: ObservableEvaluationWatcher) {
this.watchers.delete(w);
}
public async refresh(w: ObservableEvaluationWatcher): Promise<void> {
const session = this.vsCodeDebuggerView.activeDebugSession;
if (!session) {
w._state = { kind: "noDebugSession" };
return;
}
const frameId = this.vsCodeDebuggerView.activeFrameId;
w._state = { kind: "loading" };
const extractionProvider = this.dataExtractionProviderFactory.createDataExtractionProvider(
session
);
if (!extractionProvider) {
w._state = {
kind: "error",
message: `The debug adapter "${session.session.type}" is not supported.`,
};
return;
}
const result = await extractionProvider.evaluate({
expression: w.expression,
frameId,
preferredExtractor: w.preferredDataExtractor,
});
w._state = result;
}
public async getCompletions(
text: string,
column: number
): Promise<CompletionItem[]> {
const session = this.vsCodeDebuggerView.activeDebugSession;
if (!session) {
return [];
}
return await session.getCompletions({
text,
frameId: this.vsCodeDebuggerView.activeFrameId,
column,
});
}
}
class ObservableEvaluationWatcher implements EvaluationWatcher {
constructor(
public readonly expression: string,
private readonly source: DataSourceImpl,
options: EvaluationWatcherOptions
) {
this._preferredDataExtractor = options.preferredDataExtractor;
}
@observable
private _preferredDataExtractor: DataExtractorId | undefined = undefined;
public get preferredDataExtractor(): DataExtractorId | undefined {
return this._preferredDataExtractor;
}
@action
public setPreferredDataExtractor(id: DataExtractorId | undefined): void {
this._preferredDataExtractor = id;
this.refresh();
}
public refresh(): void {
this.source.refresh(this);
}
@observable
public _state: DataExtractionState = { kind: "loading" };
public get state(): DataExtractionState {
return this._state;
}
public dispose(): void {
this.source.removeWatcher(this);
}
}

View File

@@ -1,213 +0,0 @@
import {
DataSource,
EvaluationWatcher,
EvaluationWatcherOptions,
} from "./DataSource";
import { observable, autorun, action } from "mobx";
import { Disposable } from "@hediet/std/disposable";
import {
getExpressionForDataExtractorApi,
DataResult,
DataExtractorId,
ApiHasNotBeenInitializedCode,
getExpressionToInitializeDataExtractorApi,
} from "@hediet/debug-visualizer-data-extraction";
import {
DataExtractionState,
CompletionItem,
} from "@hediet/debug-visualizer-vscode-shared";
import { hotClass } from "@hediet/node-reload";
import { VsCodeDebuggerView, VsCodeDebugSession } from "../VsCodeDebugger";
export function createJsDebuggerSource(args: {
vsCodeDebuggerView: VsCodeDebuggerView;
}): JsDataSource {
return new JsDebuggerSourceImplementation(args.vsCodeDebuggerView);
}
export interface JsCode extends String {
__brand: "JsCode";
}
export interface JsDataSource extends DataSource {
registerDataExtractor(classExpression: JsCode): void;
}
@hotClass(module)
export class JsDebuggerSourceImplementation implements JsDataSource {
public readonly dispose = Disposable.fn();
private readonly watchers = new Set<ObservableEvaluationWatcher>();
constructor(private readonly vsCodeDebuggerView: VsCodeDebuggerView) {
this.dispose.track({
dispose: autorun(() => {
if (
vsCodeDebuggerView.activeDebugSession &&
vsCodeDebuggerView.activeFrameId !== undefined
) {
for (const w of this.watchers) {
w.refresh();
}
}
}),
});
}
registerDataExtractor(classExpression: JsCode): void {
// TODO implement
}
public async getCompletions(
text: string,
column: number
): Promise<CompletionItem[]> {
const session = this.vsCodeDebuggerView.activeDebugSession;
if (!session) {
return [];
}
return await session.getCompletions({
text,
frameId: this.vsCodeDebuggerView.activeFrameId,
column,
});
}
public createEvaluationWatcher(
expression: string,
options: EvaluationWatcherOptions
): EvaluationWatcher {
const w = new ObservableEvaluationWatcher(expression, this, options);
this.watchers.add(w);
this.refresh(w);
return w;
}
public removeWatcher(w: ObservableEvaluationWatcher) {
this.watchers.delete(w);
}
public async refresh(w: ObservableEvaluationWatcher): Promise<void> {
const session = this.vsCodeDebuggerView.activeDebugSession;
if (!session) {
w._state = { kind: "noDebugSession" };
return;
}
const frameId = this.vsCodeDebuggerView.activeFrameId;
try {
w._state = { kind: "loading" };
const preferredExtractor = w.preferredDataExtractor
? `"${w.preferredDataExtractor}"`
: "undefined";
const body = `${getExpressionForDataExtractorApi()}.getData(
e => (${w.expression}),
expr => eval(expr),
${preferredExtractor}
)`;
const expression = `
(() => {
try {
return ${body};
} catch (e) {
return JSON.stringify({
kind: "Error",
message: e.message,
stack: e.stack
});
}
})()
`;
const reply = await session.evaluate({
expression,
frameId,
});
const resultStr = reply.result;
const jsonData = resultStr.substr(1, resultStr.length - 2);
const result = JSON.parse(jsonData) as DataResult;
if (result.kind === "NoExtractors") {
throw new Error("No extractors");
} else if (result.kind === "Error") {
throw new Error(result.message);
}
w._state = {
kind: "data",
result: result.extractionResult,
};
} catch (error) {
const msg = error.message as string | undefined;
if (msg && msg.includes(ApiHasNotBeenInitializedCode)) {
if (await this.initializeApi(session, frameId)) {
await this.refresh(w);
return;
}
}
w._state = {
kind: "error",
message: error.message,
};
}
}
private async initializeApi(
session: VsCodeDebugSession,
frameId: number | undefined
): Promise<boolean> {
try {
// prefer existing is true, so that manually registered (possibly newer) extractors are not overwritten.
const expression = `${getExpressionToInitializeDataExtractorApi()}.registerDefaultExtractors(true);`;
await session.evaluate({
expression,
frameId,
});
return true;
} catch (error) {
return false;
}
}
}
class ObservableEvaluationWatcher implements EvaluationWatcher {
constructor(
public readonly expression: string,
private readonly source: JsDebuggerSourceImplementation,
options: EvaluationWatcherOptions
) {
this._preferredDataExtractor = options.preferredDataExtractor;
}
@observable
private _preferredDataExtractor: DataExtractorId | undefined = undefined;
public get preferredDataExtractor(): DataExtractorId | undefined {
return this._preferredDataExtractor;
}
@action
public setPreferredDataExtractor(id: DataExtractorId | undefined): void {
this._preferredDataExtractor = id;
this.refresh();
}
public refresh(): void {
this.source.refresh(this);
}
@observable
public _state: DataExtractionState = { kind: "loading" };
public get state(): DataExtractionState {
return this._state;
}
public dispose(): void {
this.source.removeWatcher(this);
}
}

View File

@@ -1,6 +1,7 @@
import { Disposable } from "@hediet/std/disposable";
import { VsCodeDebugger, VsCodeDebuggerView } from "../VsCodeDebugger";
import { createJsDebuggerSource } from "./JsDebuggerSource";
import { DataSourceImpl } from "./DataSourceImpl";
import { defaultDataExtractionProviderFactory } from "./DataExtractionProvider";
export class Sources {
public readonly dispose = Disposable.fn();
@@ -9,7 +10,8 @@ export class Sources {
new VsCodeDebuggerView(this.debugger)
);
public readonly jsSource = createJsDebuggerSource({
vsCodeDebuggerView: this.debuggerView,
});
public readonly dataSource = new DataSourceImpl(
this.debuggerView,
defaultDataExtractionProviderFactory
);
}

View File

@@ -24,7 +24,13 @@ export class Server {
const wss = new WebSocket.Server({ server: this.server });
wss.on("connection", ws => {
const stream = new WebSocketStream(ws);
new ConnectionHandler(sources, stream, this, config, this.secret);
new ConnectionHandler(
sources.dataSource,
stream,
this,
config,
this.secret
);
});
}