Skip to content

Commit 3385262

Browse files
authored
Allow live debugging tasks with actual sources (#19978)
This change makes it much easier to troubleshoot problems with tasks or develop new features. It makes it possible to connect to a specific task which is being executed by the agent and debug this in Visual Studio Code.
1 parent 835d7ce commit 3385262

17 files changed

+298
-38
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace BuildConfigGen.Debugging
2+
{
3+
internal interface IDebugConfigGenerator
4+
{
5+
void WriteTypescriptConfig(string taskOutput);
6+
7+
void AddForTask(string taskConfigPath);
8+
9+
void WriteLaunchConfigurations();
10+
}
11+
12+
sealed internal class NoDebugConfigGenerator : IDebugConfigGenerator
13+
{
14+
public void AddForTask(string taskConfigPath)
15+
{
16+
// noop
17+
}
18+
19+
public void WriteLaunchConfigurations()
20+
{
21+
// noop
22+
}
23+
24+
public void WriteTypescriptConfig(string taskOutput)
25+
{
26+
// noop
27+
}
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Nodes;
3+
4+
namespace BuildConfigGen.Debugging
5+
{
6+
internal class VsCodeLaunchConfigGenerator : IDebugConfigGenerator
7+
{
8+
private string GitRootPath { get; }
9+
10+
private string AgentPath { get; }
11+
12+
private string LaunchConfigPath => Path.Combine(GitRootPath, ".vscode", "launch.json");
13+
14+
private VsCodeLaunchConfiguration LaunchConfig { get; }
15+
16+
public VsCodeLaunchConfigGenerator(string gitRootPath, string agentPath)
17+
{
18+
ArgumentException.ThrowIfNullOrEmpty(agentPath, nameof(agentPath));
19+
ArgumentException.ThrowIfNullOrEmpty(gitRootPath, nameof(gitRootPath));
20+
21+
if (!Directory.Exists(agentPath))
22+
{
23+
throw new ArgumentException($"Agent directory used for debugging could not be found at {Path.GetFullPath(agentPath)}!");
24+
}
25+
26+
AgentPath = agentPath;
27+
GitRootPath = gitRootPath;
28+
LaunchConfig = VsCodeLaunchConfiguration.ReadFromFileIfPresentOrDefault(LaunchConfigPath);
29+
}
30+
31+
public void AddForTask(string taskConfigPath)
32+
{
33+
if (!File.Exists(taskConfigPath))
34+
{
35+
throw new ArgumentException($"Task configuration (task.json) does not exist at path {taskConfigPath}!");
36+
}
37+
38+
var taskContent = File.ReadAllText(taskConfigPath);
39+
var taskConfig = JsonNode.Parse(taskContent)!;
40+
41+
JsonNode versionNode = taskConfig["version"]!;
42+
int major = versionNode["Major"]!.GetValue<int>();
43+
int minor = versionNode["Minor"]!.GetValue<int>();
44+
int patch = versionNode["Patch"]!.GetValue<int>();
45+
46+
var version = new TaskVersion(major, minor, patch);
47+
48+
LaunchConfig.AddConfigForTask(
49+
taskId: taskConfig["id"]!.GetValue<string>(),
50+
taskName: taskConfig["name"]!.GetValue<string>(),
51+
taskVersion: version.ToString(),
52+
agentPath: AgentPath
53+
);
54+
}
55+
56+
public void WriteLaunchConfigurations()
57+
{
58+
var launchConfigString = LaunchConfig.ToJsonString();
59+
File.WriteAllText(LaunchConfigPath, launchConfigString);
60+
}
61+
62+
public void WriteTypescriptConfig(string taskOutput)
63+
{
64+
var tsconfigPath = Path.Combine(taskOutput, "tsconfig.json");
65+
if (!File.Exists(tsconfigPath))
66+
{
67+
return;
68+
}
69+
70+
var tsConfigContent = File.ReadAllText(tsconfigPath);
71+
var tsConfigObject = JsonNode.Parse(tsConfigContent)?.AsObject();
72+
73+
if (tsConfigObject == null)
74+
{
75+
return;
76+
}
77+
78+
var compilerOptionsObject = tsConfigObject["compilerOptions"]?.AsObject();
79+
compilerOptionsObject?.Add("inlineSourceMap", true);
80+
compilerOptionsObject?.Add("inlineSources", true);
81+
82+
JsonSerializerOptions options = new() { WriteIndented = true };
83+
var outputTsConfigString = JsonSerializer.Serialize(tsConfigObject, options);
84+
File.WriteAllText(tsconfigPath, outputTsConfigString);
85+
}
86+
}
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Nodes;
3+
using System.Text.RegularExpressions;
4+
5+
namespace BuildConfigGen
6+
{
7+
internal partial class VsCodeLaunchConfiguration
8+
{
9+
private JsonObject LaunchConfiguration { get; }
10+
11+
private JsonArray ConfigurationsList => _configurationsList.Value;
12+
13+
private readonly Lazy<JsonArray> _configurationsList;
14+
15+
public VsCodeLaunchConfiguration(JsonObject launchConfiguration)
16+
{
17+
ArgumentNullException.ThrowIfNull(launchConfiguration);
18+
LaunchConfiguration = launchConfiguration;
19+
20+
_configurationsList = new(() =>
21+
{
22+
if (!LaunchConfiguration.TryGetPropertyValue("configurations", out JsonNode? configurationsNode))
23+
{
24+
configurationsNode = new JsonArray();
25+
LaunchConfiguration["configurations"] = configurationsNode;
26+
}
27+
return configurationsNode!.AsArray();
28+
});
29+
}
30+
31+
public static VsCodeLaunchConfiguration ReadFromFileIfPresentOrDefault(string configPath)
32+
{
33+
ArgumentException.ThrowIfNullOrEmpty(configPath);
34+
35+
JsonObject launchConfiguration;
36+
if (File.Exists(configPath))
37+
{
38+
var rawConfigurationsString = File.ReadAllText(configPath);
39+
var safeConfigurationsString = RemoveJsonComments(rawConfigurationsString);
40+
41+
launchConfiguration = JsonNode.Parse(safeConfigurationsString)?.AsObject() ?? throw new ArgumentException($"Provided configuration file at {Path.GetFullPath(configPath)} is not a valid JSON file!");
42+
} else
43+
{
44+
launchConfiguration = new JsonObject
45+
{
46+
["version"] = "0.2.0",
47+
["configurations"] = new JsonArray()
48+
};
49+
}
50+
51+
return new VsCodeLaunchConfiguration(launchConfiguration);
52+
}
53+
54+
public void AddConfigForTask(
55+
string taskName,
56+
string taskVersion,
57+
string taskId,
58+
string agentPath)
59+
{
60+
ArgumentException.ThrowIfNullOrEmpty(taskName);
61+
ArgumentException.ThrowIfNullOrEmpty(taskVersion);
62+
ArgumentException.ThrowIfNullOrEmpty(taskId);
63+
ArgumentException.ThrowIfNullOrEmpty(agentPath);
64+
65+
var launchConfigName = GetLaunchConfigurationName(taskName, taskVersion);
66+
67+
var existingLaunchConfig = ConfigurationsList.FirstOrDefault(x =>
68+
{
69+
var name = x?[c_taskName]?.GetValue<string>();
70+
71+
return string.Equals(name, launchConfigName, StringComparison.OrdinalIgnoreCase);
72+
});
73+
74+
ConfigurationsList.Remove(existingLaunchConfig);
75+
76+
var launchConfig = new JsonObject
77+
{
78+
[c_taskName] = launchConfigName,
79+
["type"] = "node",
80+
["request"] = "attach",
81+
["address"] = "localhost",
82+
["port"] = 9229,
83+
["autoAttachChildProcesses"] = true,
84+
["skipFiles"] = new JsonArray("<node_internals>/**"),
85+
["sourceMaps"] = true,
86+
["remoteRoot"] = GetRemoteSourcesPath(taskName, taskVersion, taskId, agentPath)
87+
};
88+
89+
ConfigurationsList.Add(launchConfig);
90+
}
91+
92+
public string ToJsonString()
93+
{
94+
var options = new JsonSerializerOptions { WriteIndented = true };
95+
return JsonSerializer.Serialize(LaunchConfiguration, options);
96+
}
97+
98+
private static string GetLaunchConfigurationName(string task, string version) =>
99+
$"Attach to {task} ({version})";
100+
101+
private static string GetRemoteSourcesPath(string taskName, string taskVersion, string taskId, string agentPath) =>
102+
@$"{agentPath}\_work\_tasks\{taskName}_{taskId.ToLower()}\{taskVersion}";
103+
104+
private static string RemoveJsonComments(string jsonString)
105+
{
106+
jsonString = SingleLineCommentsRegex().Replace(jsonString, string.Empty);
107+
jsonString = MultiLineCommentsRegex().Replace(jsonString, string.Empty);
108+
return jsonString;
109+
}
110+
111+
[GeneratedRegex(@"//.*(?=\r?\n|$)")]
112+
private static partial Regex SingleLineCommentsRegex();
113+
114+
[GeneratedRegex(@"/\*.*?\*/", RegexOptions.Singleline)]
115+
private static partial Regex MultiLineCommentsRegex();
116+
117+
private const string c_taskName = "name";
118+
}
119+
}

BuildConfigGen/Program.cs

+33-15
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using System.Diagnostics.CodeAnalysis;
2+
using System.Runtime.InteropServices.JavaScript;
23
using System.Text;
34
using System.Text.Json;
45
using System.Text.Json.Nodes;
56
using System.Text.RegularExpressions;
7+
using BuildConfigGen.Debugging;
68

79
namespace BuildConfigGen
810
{
@@ -63,11 +65,12 @@ public record ConfigRecord(string name, string constMappingKey, bool isDefault,
6365
/// <param name="writeUpdates">Write updates if true, else validate that the output is up-to-date</param>
6466
/// <param name="allTasks"></param>
6567
/// <param name="getTaskVersionTable"></param>
66-
static void Main(string? task = null, string? configs = null, int? currentSprint = null, bool writeUpdates = false, bool allTasks = false, bool getTaskVersionTable = false)
68+
/// <param name="debugAgentDir">When set to the local pipeline agent directory, this tool will produce tasks in debug mode with the corresponding visual studio launch configurations that can be used to attach to built tasks running on this agent</param>
69+
static void Main(string? task = null, string? configs = null, int? currentSprint = null, bool writeUpdates = false, bool allTasks = false, bool getTaskVersionTable = false, string? debugAgentDir = null)
6770
{
6871
try
6972
{
70-
MainInner(task, configs, currentSprint, writeUpdates, allTasks, getTaskVersionTable);
73+
MainInner(task, configs, currentSprint, writeUpdates, allTasks, getTaskVersionTable, debugAgentDir);
7174
}
7275
catch (Exception e2)
7376
{
@@ -85,7 +88,7 @@ static void Main(string? task = null, string? configs = null, int? currentSprint
8588
}
8689
}
8790

88-
private static void MainInner(string? task, string? configs, int? currentSprint, bool writeUpdates, bool allTasks, bool getTaskVersionTable)
91+
private static void MainInner(string? task, string? configs, int? currentSprint, bool writeUpdates, bool allTasks, bool getTaskVersionTable, string? debugAgentDir)
8992
{
9093
if (allTasks)
9194
{
@@ -98,11 +101,10 @@ private static void MainInner(string? task, string? configs, int? currentSprint,
98101
NotNullOrThrow(configs, "Configs is required");
99102
}
100103

104+
string currentDir = Environment.CurrentDirectory;
105+
string gitRootPath = GitUtil.GetGitRootPath(currentDir);
101106
if (getTaskVersionTable)
102107
{
103-
string currentDir = Environment.CurrentDirectory;
104-
string gitRootPath = GitUtil.GetGitRootPath(currentDir);
105-
106108
var tasks = MakeOptionsReader.ReadMakeOptions(gitRootPath);
107109

108110
Console.WriteLine("config\ttask\tversion");
@@ -120,15 +122,16 @@ private static void MainInner(string? task, string? configs, int? currentSprint,
120122
return;
121123
}
122124

125+
IDebugConfigGenerator debugConfGen = string.IsNullOrEmpty(debugAgentDir)
126+
? new NoDebugConfigGenerator()
127+
: new VsCodeLaunchConfigGenerator(gitRootPath, debugAgentDir);
128+
123129
if (allTasks)
124130
{
125-
string currentDir = Environment.CurrentDirectory;
126-
string gitRootPath = GitUtil.GetGitRootPath(currentDir);
127-
128131
var tasks = MakeOptionsReader.ReadMakeOptions(gitRootPath);
129132
foreach (var t in tasks.Values)
130133
{
131-
MainUpdateTask(t.Name, string.Join('|', t.Configs), writeUpdates, currentSprint);
134+
MainUpdateTask(t.Name, string.Join('|', t.Configs), writeUpdates, currentSprint, debugConfGen);
132135
}
133136
}
134137
else
@@ -139,10 +142,12 @@ private static void MainInner(string? task, string? configs, int? currentSprint,
139142
// 3. Ideally default windows exception will occur and errors reported to WER/watson. I'm not sure this is happening, perhaps DragonFruit is handling the exception
140143
foreach (var t in task!.Split(',', '|'))
141144
{
142-
MainUpdateTask(t, configs!, writeUpdates, currentSprint);
145+
MainUpdateTask(t, configs!, writeUpdates, currentSprint, debugConfGen);
143146
}
144147
}
145148

149+
debugConfGen.WriteLaunchConfigurations();
150+
146151
if (notSyncronizedDependencies.Count > 0)
147152
{
148153
notSyncronizedDependencies.Insert(0, $"##vso[task.logissue type=error]There are problems with the dependencies in the buildConfig's package.json files. Please fix the following issues:");
@@ -225,7 +230,12 @@ private static void GetVersions(string task, string configsString, out List<(str
225230
}
226231
}
227232

228-
private static void MainUpdateTask(string task, string configsString, bool writeUpdates, int? currentSprint)
233+
private static void MainUpdateTask(
234+
string task,
235+
string configsString,
236+
bool writeUpdates,
237+
int? currentSprint,
238+
IDebugConfigGenerator debugConfigGen)
229239
{
230240
if (string.IsNullOrEmpty(task))
231241
{
@@ -265,7 +275,7 @@ private static void MainUpdateTask(string task, string configsString, bool write
265275
{
266276
ensureUpdateModeVerifier = new EnsureUpdateModeVerifier(!writeUpdates);
267277

268-
MainUpdateTaskInner(task, currentSprint, targetConfigs);
278+
MainUpdateTaskInner(task, currentSprint, targetConfigs, debugConfigGen);
269279

270280
ThrowWithUserFriendlyErrorToRerunWithWriteUpdatesIfVeriferError(task, skipContentCheck: false);
271281
}
@@ -309,7 +319,11 @@ private static void ThrowWithUserFriendlyErrorToRerunWithWriteUpdatesIfVeriferEr
309319
}
310320
}
311321

312-
private static void MainUpdateTaskInner(string task, int? currentSprint, HashSet<Config.ConfigRecord> targetConfigs)
322+
private static void MainUpdateTaskInner(
323+
string task,
324+
int? currentSprint,
325+
HashSet<Config.ConfigRecord> targetConfigs,
326+
IDebugConfigGenerator debugConfigGen)
313327
{
314328
if (!currentSprint.HasValue)
315329
{
@@ -387,7 +401,8 @@ private static void MainUpdateTaskInner(string task, int? currentSprint, HashSet
387401
EnsureBuildConfigFileOverrides(config, taskTargetPath);
388402
}
389403

390-
var taskConfigExists = File.Exists(Path.Combine(taskOutput, "task.json"));
404+
var taskConfigPath = Path.Combine(taskOutput, "task.json");
405+
var taskConfigExists = File.Exists(taskConfigPath);
391406

392407
// only update task output if a new version was added, the config exists, or the task contains preprocessor instructions
393408
// Note: CheckTaskInputContainsPreprocessorInstructions is expensive, so only call if needed
@@ -423,6 +438,9 @@ private static void MainUpdateTaskInner(string task, int? currentSprint, HashSet
423438
Path.Combine(taskTargetPath, buildConfigs, configTaskPath, "package.json"));
424439
WriteNodePackageJson(taskOutput, config.nodePackageVersion, config.shouldUpdateTypescript);
425440
}
441+
442+
debugConfigGen.WriteTypescriptConfig(taskOutput);
443+
debugConfigGen.AddForTask(taskConfigPath);
426444
}
427445

428446
// delay updating version map file until after buildconfigs generated

BuildConfigGen/TaskVersion.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Globalization;
1+
using System;
2+
using System.Globalization;
23

34
internal class TaskVersion : IComparable<TaskVersion>, IEquatable<TaskVersion>
45
{

Tasks/BashV3/task.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"author": "Microsoft Corporation",
1818
"version": {
1919
"Major": 3,
20-
"Minor": 239,
20+
"Minor": 241,
2121
"Patch": 0
2222
},
2323
"releaseNotes": "Script task consistency. Added support for multiple lines and added support for Windows.",

0 commit comments

Comments
 (0)