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

feat: Debugger for Azure Pipelines #249

Merged
merged 2 commits into from
Oct 29, 2023
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
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