Skip to content

Commit

Permalink
feat: Debugger for Azure Pipelines (#249)
Browse files Browse the repository at this point in the history
* feat: Debugger for Azure Pipelines

* Preview the expanded yaml
* Specify the root yaml file
* Watch for yaml changes
* custom parameters per launch config
* custom variables per launch config
* custom repositories per launch config
  • Loading branch information
ChristopherHX authored Oct 29, 2023
1 parent 72ec97c commit 9c0f1bc
Show file tree
Hide file tree
Showing 8 changed files with 914 additions and 342 deletions.
153 changes: 153 additions & 0 deletions src/azure-pipelines-vscode-ext/azure-pipelines-debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import {
Logger, logger,
LoggingDebugSession,
InitializedEvent, TerminatedEvent
} from '@vscode/debugadapter';
import { DebugProtocol } from '@vscode/debugprotocol';

import * as vscode from 'vscode'

interface ILaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
program: string;
trace?: boolean;
watch?: boolean;
}

interface IAttachRequestArguments extends ILaunchRequestArguments { }


export class AzurePipelinesDebugSession extends LoggingDebugSession {
watcher: vscode.FileSystemWatcher
virtualFiles: any
name: string
expandAzurePipeline: any
changed: any

public constructor(virtualFiles: any, name: string, expandAzurePipeline: any, changed: any) {
super("azure-pipelines-debug.yml");
this.setDebuggerLinesStartAt1(false);
this.setDebuggerColumnsStartAt1(false);
this.virtualFiles = virtualFiles;
this.name = name;
this.expandAzurePipeline = expandAzurePipeline;
this.changed = changed;
}

protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
response.body = response.body || {};
response.body.supportsConfigurationDoneRequest = true;
response.body.supportsEvaluateForHovers = false;
response.body.supportsStepBack = false;
response.body.supportsDataBreakpoints = false;
response.body.supportsCompletionsRequest = false;
response.body.completionTriggerCharacters = [ ".", "[" ];
response.body.supportsCancelRequest = false;
response.body.supportsBreakpointLocationsRequest = false;
response.body.supportsStepInTargetsRequest = false;
response.body.supportsExceptionFilterOptions = false;
response.body.exceptionBreakpointFilters = [];
response.body.supportsExceptionInfoRequest = false;
response.body.supportsSetVariable = false;
response.body.supportsSetExpression = false;
response.body.supportsDisassembleRequest = false;
response.body.supportsSteppingGranularity = false;
response.body.supportsInstructionBreakpoints = false;
response.body.supportsReadMemoryRequest = false;
response.body.supportsWriteMemoryRequest = false;
response.body.supportSuspendDebuggee = false;
response.body.supportTerminateDebuggee = true;
response.body.supportsFunctionBreakpoints = false;
response.body.supportsDelayedStackTraceLoading = false;

this.sendResponse(response);
this.sendEvent(new InitializedEvent());
}

protected async attachRequest(response: DebugProtocol.AttachResponse, args: IAttachRequestArguments) {
return this.launchRequest(response, args);
}

protected async launchRequest(response: DebugProtocol.LaunchResponse, args: any) {
logger.setup(args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Stop, false);

var self = this;
var message = null;
if(args.preview) {
self.virtualFiles[self.name] = "";
var uri = vscode.Uri.from({
scheme: "azure-pipelines-vscode-ext",
path: this.name
});
var doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc, { preview: true, viewColumn: vscode.ViewColumn.Beside, preserveFocus: true });
vscode.workspace.onDidCloseTextDocument(adoc => {
if(doc === adoc) {
this.sendEvent(new TerminatedEvent());
}
});
self.changed(uri);
}
var run = async() => {
await this.expandAzurePipeline(false, args.repositories, args.variables, args.parameters, result => {
if(args.preview) {
self.virtualFiles[self.name] = result;
self.changed(uri);
} else {
vscode.window.showInformationMessage("No Issues found");
}
}, args.program, async errmsg => {
if(args.preview) {
self.virtualFiles[self.name] = errmsg;
self.changed(uri);
} else if(args.watch) {
vscode.window.showErrorMessage(errmsg);
} else {
message = errmsg;
}
});
};
try {
await run();
} catch(ex) {
console.log(ex?.toString() ?? "<??? error>");
}
if(args.watch) {
this.watcher = vscode.workspace.createFileSystemWatcher("**/*.{yml,yaml}");
this.watcher.onDidCreate(e => {
console.log(`created: ${e.toString()}`);
run();
});
this.watcher.onDidChange(e => {
console.log(`changed: ${e.toString()}`);
run();
});
this.watcher.onDidDelete(e => {
console.log(`deleted: ${e.toString()}`);
run();
});
} else {
if(message) {
this.sendErrorResponse(response, {
id: 1001,
format: message,
showUser: true
});
} else {
this.sendResponse(response);
this.sendEvent(new TerminatedEvent());
}
}
}

protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments, request?: DebugProtocol.Request): void {
this.sendResponse(response);
}

protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments, request?: DebugProtocol.Request): void {
console.log(`disconnectRequest suspend: ${args.suspendDebuggee}, terminate: ${args.terminateDebuggee}`);
if (this.watcher) {
this.watcher.dispose();
}
this.sendResponse(response);
}
}
2 changes: 2 additions & 0 deletions src/azure-pipelines-vscode-ext/ext-core/Interop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public static partial class Interop {
internal static partial void Log(int type, string message);
[JSImport("requestRequiredParameter", "extension.js")]
internal static partial Task<string> RequestRequiredParameter(JSObject handle, string name);
[JSImport("error", "extension.js")]
internal static partial Task Error(JSObject handle, string message);
}
8 changes: 6 additions & 2 deletions src/azure-pipelines-vscode-ext/ext-core/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public IDictionary<string, string> GetVariablesForEnvironment(string name = null


[MethodImpl(MethodImplOptions.NoInlining)]
public static async Task<string> ExpandCurrentPipeline(JSObject handle, string currentFileName, string variables, string parameters) {
public static async Task<string> ExpandCurrentPipeline(JSObject handle, string currentFileName, string variables, string parameters, bool returnErrorContent) {
try {
var context = new Runner.Server.Azure.Devops.Context {
FileProvider = new MyFileProvider(handle),
Expand All @@ -84,7 +84,11 @@ public static async Task<string> ExpandCurrentPipeline(JSObject handle, string c
var pipeline = await new Runner.Server.Azure.Devops.Pipeline().Parse(context.ChildContext(template, currentFileName), template);
return pipeline.ToYaml();
} catch(Exception ex) {
await Interop.Message(2, ex.ToString());
if(returnErrorContent) {
await Interop.Error(handle, ex.ToString());
} else {
await Interop.Message(2, ex.ToString());
}
return null;
}
}
Expand Down
112 changes: 89 additions & 23 deletions src/azure-pipelines-vscode-ext/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const vscode = require('vscode');
import { basePaths, customImports } from "./config.js"
import { basePaths, customImports } from "./config.js"
import { AzurePipelinesDebugSession } from "./azure-pipelines-debug";

/**
* @param {vscode.ExtensionContext} context
Expand All @@ -12,6 +13,16 @@ function activate(context) {

var logchannel = vscode.window.createOutputChannel("Azure Pipeline Evalation Log", { log: true });

var virtualFiles = {};
var myScheme = "azure-pipelines-vscode-ext";
var changeDoc = new vscode.EventEmitter();
vscode.workspace.registerTextDocumentContentProvider(myScheme, {
onDidChange: changeDoc.event,
provideTextDocumentContent(uri) {
return virtualFiles[uri.path];
}
});

var runtimePromise = vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Updating Runtime",
Expand Down Expand Up @@ -64,8 +75,8 @@ function activate(context) {
}
} else {
// Get current textEditor content for the entrypoint
var doc = handle.textEditor.document;
if(handle.filename === filename && doc) {
var doc = handle.textEditor ? handle.textEditor.document : null;
if(handle.filename === filename && doc && !handle.skipCurrentEditor) {
return doc.getText();
}
uri = handle.base.with({ path: handle.base.path + "/" + filename });
Expand Down Expand Up @@ -122,6 +133,9 @@ function activate(context) {
prompt: name,
title: "Provide required Variables in yaml notation"
})
},
error: async (handle, message) => {
await handle.error(message);
}
});
logchannel.appendLine("Starting extension main to keep dotnet alive");
Expand All @@ -130,9 +144,9 @@ function activate(context) {
return runtime;
});

var expandAzurePipeline = async validate => {
var expandAzurePipeline = async (validate, repos, vars, params, callback, fspathname, error) => {
var textEditor = vscode.window.activeTextEditor;
if(!textEditor) {
if(!textEditor && !fspathname) {
await vscode.window.showErrorMessage("No active TextEditor");
return;
}
Expand All @@ -143,40 +157,85 @@ function activate(context) {
var name = line.shift();
repositories[name] = line.join("=");
}
if(repos) {
for(var name in repos) {
repositories[name] = repos[name];
}
}
var variables = {};
for(var repo of conf.variables ?? []) {
var line = repo.split("=");
var name = line.shift();
variables[name] = line.join("=");
}
if(vars) {
for(var name in vars) {
variables[name] = vars[name];
}
}
var parameters = {};
for(var repo of conf.parameters ?? []) {
var line = repo.split("=");
var name = line.shift();
parameters[name] = line.join("=");
if(params) {
for(var name in params) {
parameters[name] = JSON.stringify(params[name]);
}
} else {
for(var repo of conf.parameters ?? []) {
var line = repo.split("=");
var name = line.shift();
parameters[name] = line.join("=");
}
}

var runtime = await runtimePromise;
var base = null;
var filename = null;
var current = textEditor.document.uri;
for(var workspace of vscode.workspace.workspaceFolders) {
var workspacePath = workspace.uri.path.replace(/\/*$/, "/");
if(workspace.uri.scheme === current.scheme && workspace.uri.authority === current.authority && current.path.startsWith(workspacePath)) {
base = workspace.uri;
filename = current.path.substring(workspacePath.length);
break;

var skipCurrentEditor = false;
var filename = null
if(fspathname) {
skipCurrentEditor = true;
var uris = [vscode.Uri.parse(fspathname), vscode.Uri.file(fspathname)];
for(var current of uris) {
var rbase = vscode.workspace.getWorkspaceFolder(current);
var name = vscode.workspace.asRelativePath(current);
if(rbase && name) {
base = rbase.uri;
filename = name;
break;
}
}
}
var li = current.path.lastIndexOf("/");
base ??= current.with({ path: current.path.substring(0, li)});
filename ??= current.path.substring(li + 1);
var result = await runtime.BINDING.bind_static_method("[ext-core] MyClass:ExpandCurrentPipeline")({ base: base, textEditor: textEditor, filename: filename, repositories: repositories }, filename, JSON.stringify(variables), JSON.stringify(parameters));

if(filename == null) {
for(var workspace of vscode.workspace.workspaceFolders) {
if(fspathname.startsWith(workspace.uri.fsPath)) {
base = workspace.uri;
filename = vscode.workspace.asRelativePath(workspace.uri.with({path: workspace.uri.path + "/" + fspathname.substring(workspace.uri.fsPath.length).replace(/[\\\/]+/g, "/")
}));
break;
}
}
}
} else {
filename = null;
var current = textEditor.document.uri;
for(var workspace of vscode.workspace.workspaceFolders) {
var workspacePath = workspace.uri.path.replace(/\/*$/, "/");
if(workspace.uri.scheme === current.scheme && workspace.uri.authority === current.authority && current.path.startsWith(workspacePath)) {
base = workspace.uri;
filename = current.path.substring(workspacePath.length);
break;
}
}
var li = current.path.lastIndexOf("/");
base ??= current.with({ path: current.path.substring(0, li)});
filename ??= current.path.substring(li + 1);
}
var result = await runtime.BINDING.bind_static_method("[ext-core] MyClass:ExpandCurrentPipeline")({ base: base, skipCurrentEditor: skipCurrentEditor, textEditor: textEditor, filename: filename, repositories: repositories, error: error }, filename, JSON.stringify(variables), JSON.stringify(parameters), (error && true) == true);

if(result) {
logchannel.debug(result);
if(validate) {
await vscode.window.showInformationMessage("No issues found");
} else if(callback) {
callback(result);
} else {
await vscode.workspace.openTextDocument({ language: "yaml", content: result });
}
Expand All @@ -200,6 +259,13 @@ function activate(context) {
statusbar.hide();
}
};
var z = 0;
vscode.debug.registerDebugAdapterDescriptorFactory("azure-pipelines-vscode-ext", {
createDebugAdapterDescriptor: (session, executable) => {
return new vscode.DebugAdapterInlineImplementation(new AzurePipelinesDebugSession(virtualFiles, `azure-pipelines-preview-${z++}.yml`, expandAzurePipeline, arg => changeDoc.fire(arg)));
}
});

var onTextEditChanged = texteditor => onLanguageChanged(texteditor && texteditor.document && texteditor.document.languageId ? texteditor.document.languageId : null);
context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(onTextEditChanged))
context.subscriptions.push(vscode.workspace.onDidCloseTextDocument(document => onLanguageChanged(document && document.languageId ? document.languageId : null)));
Expand Down
Loading

0 comments on commit 9c0f1bc

Please sign in to comment.