Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: unable to hit most inflight breakpoints while debugging #6217

Merged
merged 8 commits into from
Apr 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion apps/vscode-wing/.projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,49 @@ const contributes: VSCodeExtensionContributions = {
},
},
],
debuggers: [
{
type: "wing",
label: "Wing Debug",
program: "",
configurationAttributes: {
launch: {
entrypoint: {
type: "string",
description: "The entrypoint to run",
default: "${file}",
},
arguments: {
type: "string",
description: "Wing CLI arguments",
default: "test",
},
},
},
initialConfigurations: [
{
label: "Wing Debug: Launch",
description: "Launch a Wing program",
body: {
type: "wing",
request: "launch",
name: "Launch",
},
},
],
configurationSnippets: [
{
label: "Wing Debug: Launch",
description: "Launch a Wing program",
body: {
type: "wing",
request: "launch",
name: "Launch",
},
},
],
},
],
grammars: [
{
language: "wing",
Expand Down Expand Up @@ -167,7 +210,7 @@ project.addFields({
vscode: `^${VSCODE_BASE_VERSION}`,
},
categories: ["Programming Languages"],
activationEvents: ["onLanguage:wing"],
activationEvents: ["onLanguage:wing", "onDebug"],
contributes,
});

Expand Down
46 changes: 45 additions & 1 deletion apps/vscode-wing/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 66 additions & 1 deletion apps/vscode-wing/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
languages,
workspace,
window,
debug,
DebugConfiguration,
} from "vscode";
import { getWingBin, updateStatusBar } from "./bin-helper";
import { CFG_WING, CFG_WING_BIN, COMMAND_OPEN_CONSOLE } from "./constants";
Expand All @@ -17,7 +19,6 @@ let languageServerManager: LanguageServerManager | undefined;
export async function deactivate() {
wingBinWatcher?.close();
await languageServerManager?.stop();
await wingConsoleContext?.stop();
}

export async function activate(context: ExtensionContext) {
Expand All @@ -29,6 +30,70 @@ export async function activate(context: ExtensionContext) {

languageServerManager = new LanguageServerManager();

debug.registerDebugConfigurationProvider("wing", {
async resolveDebugConfiguration(_, _config: DebugConfiguration) {
Loggers.default.appendLine(
`Resolving debug configuration... ${JSON.stringify(_config)}`
);
const editor = window.activeTextEditor;

const currentFilename = editor?.document.fileName;
let chosenFile;
if (
currentFilename?.endsWith("main.w") ||
currentFilename?.endsWith(".test.w")
) {
chosenFile = currentFilename;
} else {
let uriOptions = await workspace.findFiles(
`**/*.{main,test}.w`,
"**/{node_modules,target}/**"
);
uriOptions.concat(
await workspace.findFiles(`**/main.w`, "**/{node_modules,target}/**")
);

const entrypoint = await window.showQuickPick(
uriOptions.map((f) => f.fsPath),
{
placeHolder: "Choose entrypoint to debug",
}
);

if (!entrypoint) {
return;
}

chosenFile = entrypoint;
}

const command = await window.showInputBox({
title: `Debugging ${chosenFile}`,
prompt: "Wing CLI arguments",
value: "test",
});

if (!command) {
return;
}

const currentWingBin = await getWingBin();

// Use builtin node debugger
return {
name: `Debug ${chosenFile}`,
request: "launch",
type: "node",
args: [currentWingBin, command, chosenFile],
runtimeSourcemapPausePatterns: [
"${workspaceFolder}/**/target/**/*.cjs",
],
autoAttachChildProcesses: true,
pauseForSourceMap: true,
};
},
});

const wingBinChanged = async () => {
Loggers.default.appendLine(`Setting up wing bin...`);
const currentWingBin = await getWingBin();
Expand Down
6 changes: 6 additions & 0 deletions apps/vscode-wing/src/project/vscode_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ export interface VSCodeDebugger {
readonly label?: string;
readonly type: string;
readonly runtime?: string;
readonly program?: string;
readonly request?: string;
readonly variables?: string;
readonly configurationAttributes?: any;
readonly initialConfigurations?: any[];
readonly configurationSnippets?: any[];
}

export interface VSCodeGrammar {
Expand Down
10 changes: 3 additions & 7 deletions docs/docs/06-tools/03-debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@ Internally Wing uses JavaScript to execute preflight and inflight code, so stand

### Local/Simulator Debugging

To start, open your .w file in VS Code and set a breakpoint by clicking in the gutter to the left of the line number. Breakpoints can also be set in extern files. There are several ways to start the debugger, but let's use the "JavaScript Debug Terminal".
Open the command palette and type "Debug: Open JavaScript Debug Terminal". This works for any wing commands like `wing test` and `wing it`, although keep in mind that `wing compile` will only debug preflight code.

### Limitations

- ([Issue](https://github.com/winglang/wing/issues/5988)) When using the Wing Console (`wing it`) and attempting to debug inflight code in a `test` or Function, the first execution of the test will not hit a breakpoint and will need to be run again
- ([Issue](https://github.com/winglang/wing/issues/5986)) inflight code by default has a timeout that continues during debugging, so if execution is paused for too long the program is terminate
To start, open your .w file in VS Code and set breakpoints by clicking in the gutter to the left of the line number. Breakpoints can also be set in extern files.
Once set, press F5 or use the "Run and Debug" button in the sidebar to start the debugger. This will use the current file if it's an entrypoint or it will prompt you to select one. Different CLI arguments can be provided as well.
By default, `wing test` will be run with an attached debugger.

#### Non-VSCode Support

Expand Down
4 changes: 4 additions & 0 deletions libs/wingsdk/.projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions libs/wingsdk/.projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const project = new cdk.JsiiProject({
// enhanced diagnostics
"stacktracey",
"ulid",
"vlq",
// tunnels
"@winglang/wingtunnels@workspace:^",
"glob",
Expand Down
2 changes: 2 additions & 0 deletions libs/wingsdk/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

108 changes: 99 additions & 9 deletions libs/wingsdk/src/shared/bundling.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as crypto from "crypto";
import { mkdirSync, realpathSync, writeFileSync } from "fs";
import { posix, resolve } from "path";
import { decode, encode } from "vlq";
import { normalPath } from "./misc";

const SDK_PATH = normalPath(resolve(__dirname, "..", ".."));
Expand Down Expand Up @@ -74,15 +75,7 @@ export function createBundle(
const sourcemapData = JSON.parse(
new TextDecoder().decode(esbuild.outputFiles[0].contents)
);
if (sourcemapData.sourceRoot) {
sourcemapData.sourceRoot = normalPath(sourcemapData.sourceRoot);
}

for (const [idx, source] of Object.entries(
sourcemapData.sources as string[]
)) {
sourcemapData.sources[idx] = normalPath(source);
}
fixSourcemaps(sourcemapData);

writeFileSync(outfile, bundleOutput.contents);
writeFileSync(outfileMap, JSON.stringify(sourcemapData));
Expand All @@ -101,3 +94,100 @@ export function createBundle(
sourcemapPath: outfileMap,
};
}

export interface SourceMap {
sourceRoot?: string;
sources: string[];
sourcesContent: string[];
mappings: string;
}

/**
* Takes a bundled sourcemap and does the following fixes:
* - Normalizes paths in sources and sourceRoot
* - Removes duplicate sources and sourcesContent
* - Updates mappings to reflect the new source indices
*
* The duplicate sources come from esbuild's strange handling of multiple files being bundled that point to the same source (e.g. inflights that point to one .w file)
* See https://github.com/evanw/esbuild/issues/933
*/
export function fixSourcemaps(sourcemapData: SourceMap): void {
// normalize sourceRoot
if (sourcemapData.sourceRoot) {
sourcemapData.sourceRoot = normalPath(sourcemapData.sourceRoot);
}

// normalize sources and remove duplicates
const sourceSet: string[] = [];
const newSourceContents: string[] = [];
const sourceIndexMap: Record<number, number> = {};
let hasSourceDupes = false;
sourcemapData.sources.forEach((source, idx) => {
const newPath = normalPath(source);
sourcemapData.sources[idx] = newPath;

const existingIndex = sourceSet.indexOf(newPath);
if (existingIndex === -1) {
sourceSet.push(newPath);
newSourceContents.push(sourcemapData.sourcesContent[idx]);
sourceIndexMap[idx] = sourceSet.length - 1;
} else {
hasSourceDupes = true;
sourceIndexMap[idx] = existingIndex;
}
});

sourcemapData.sources = sourceSet;
sourcemapData.sourcesContent = newSourceContents;

// fast path: No source duplicates so no need to update mappings
if (!hasSourceDupes) {
return;
}

// update mappings
let newMapping = "";
let characterIndex = 0;
let lastFile = 0;
let lastTrueFile = 0;
while (characterIndex < sourcemapData.mappings.length) {
const char = sourcemapData.mappings[characterIndex];
// `;` and `,` are separators between the segments of interest
if (char === ";" || char === ",") {
newMapping += char;
characterIndex++;
continue;
}

// get next slice of segment data
let segment = "";
let nextChar = char;
while (nextChar !== undefined && nextChar !== "," && nextChar !== ";") {
segment += nextChar;
nextChar = sourcemapData.mappings[++characterIndex];
}
const decoded = decode(segment);
if (decoded.length === 1) {
newMapping += segment;
continue;
}

const sourceRelative = decoded[1];
const originalSource = lastTrueFile + sourceRelative;
const newSourceIndex = sourceIndexMap[originalSource];
lastTrueFile = originalSource;

const newRelativeValue = newSourceIndex - lastFile;
lastFile = newSourceIndex;

if (newRelativeValue === decoded[1]) {
// no change was made, avoid re-encoding
newMapping += segment;
} else {
decoded[1] = newRelativeValue;
newMapping += encode(decoded);
}
}

sourcemapData.mappings = newMapping;
}
Loading
Loading