Skip to content

Commit 5e3306a

Browse files
jjonescznagilsonCopilot
authored
Add file-based program API for use by debugger (#48905)
Co-authored-by: Noah Gilson <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 34ae4f2 commit 5e3306a

File tree

6 files changed

+102
-8
lines changed

6 files changed

+102
-8
lines changed

src/Cli/Microsoft.DotNet.Cli.Utils/Command.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77

88
namespace Microsoft.DotNet.Cli.Utils;
99

10-
public class Command(Process? process, bool trimTrailingNewlines = false) : ICommand
10+
public class Command(Process? process, bool trimTrailingNewlines = false, IDictionary<string, string?>? customEnvironmentVariables = null) : ICommand
1111
{
1212
private readonly Process _process = process ?? throw new ArgumentNullException(nameof(process));
1313

14+
private readonly Dictionary<string, string?>? _customEnvironmentVariables =
15+
// copy the dictionary to avoid mutating the original
16+
customEnvironmentVariables == null ? null : new(customEnvironmentVariables);
17+
1418
private StreamForwarder? _stdOut;
1519

1620
private StreamForwarder? _stdErr;
@@ -98,6 +102,7 @@ public ICommand WorkingDirectory(string? projectDirectory)
98102
public ICommand EnvironmentVariable(string name, string? value)
99103
{
100104
_process.StartInfo.Environment[name] = value;
105+
_customEnvironmentVariables?[name] = value;
101106
return this;
102107
}
103108

@@ -185,6 +190,14 @@ public ICommand OnErrorLine(Action<string> handler)
185190

186191
public string CommandArgs => _process.StartInfo.Arguments;
187192

193+
public ProcessStartInfo StartInfo => _process.StartInfo;
194+
195+
/// <summary>
196+
/// If set in the constructor, it's used to keep track of environment variables modified via <see cref="EnvironmentVariable"/>
197+
/// unlike <see cref="ProcessStartInfo.Environment"/> which includes all environment variables of the current process.
198+
/// </summary>
199+
public IReadOnlyDictionary<string, string?>? CustomEnvironmentVariables => _customEnvironmentVariables;
200+
188201
public ICommand SetCommandArgs(string commandArgs)
189202
{
190203
_process.StartInfo.Arguments = commandArgs;

src/Cli/dotnet/CommandFactory/CommandFactoryUsingResolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,6 @@ public static Command Create(CommandSpec commandSpec)
113113
StartInfo = psi
114114
};
115115

116-
return new Command(_process);
116+
return new Command(_process, customEnvironmentVariables: commandSpec.EnvironmentVariables);
117117
}
118118
}

src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Immutable;
5+
using System.Collections.ObjectModel;
56
using System.CommandLine;
67
using System.Text.Json;
78
using System.Text.Json.Serialization;
@@ -46,6 +47,7 @@ static void Respond(RunApiOutput message)
4647
}
4748

4849
[JsonDerivedType(typeof(GetProject), nameof(GetProject))]
50+
[JsonDerivedType(typeof(GetRunCommand), nameof(GetRunCommand))]
4951
internal abstract class RunApiInput
5052
{
5153
private RunApiInput() { }
@@ -74,10 +76,57 @@ public override RunApiOutput Execute()
7476
};
7577
}
7678
}
79+
80+
public sealed class GetRunCommand : RunApiInput
81+
{
82+
public string? ArtifactsPath { get; init; }
83+
public required string EntryPointFileFullPath { get; init; }
84+
85+
public override RunApiOutput Execute()
86+
{
87+
var buildCommand = new VirtualProjectBuildingCommand(
88+
entryPointFileFullPath: EntryPointFileFullPath,
89+
msbuildArgs: [],
90+
verbosity: VerbosityOptions.quiet,
91+
interactive: false)
92+
{
93+
CustomArtifactsPath = ArtifactsPath,
94+
};
95+
96+
buildCommand.PrepareProjectInstance();
97+
98+
var runCommand = new RunCommand(
99+
noBuild: false,
100+
projectFileOrDirectory: null,
101+
launchProfile: null,
102+
noLaunchProfile: false,
103+
noLaunchProfileArguments: false,
104+
noRestore: false,
105+
noCache: false,
106+
interactive: false,
107+
verbosity: VerbosityOptions.quiet,
108+
restoreArgs: [],
109+
args: [EntryPointFileFullPath],
110+
environmentVariables: ReadOnlyDictionary<string, string>.Empty);
111+
112+
runCommand.TryGetLaunchProfileSettingsIfNeeded(out var launchSettings);
113+
var targetCommand = (Utils.Command)runCommand.GetTargetCommand(buildCommand.CreateProjectInstance);
114+
runCommand.ApplyLaunchSettingsProfileToCommand(targetCommand, launchSettings);
115+
116+
return new RunApiOutput.RunCommand
117+
{
118+
ExecutablePath = targetCommand.CommandName,
119+
CommandLineArguments = targetCommand.CommandArgs,
120+
WorkingDirectory = targetCommand.StartInfo.WorkingDirectory,
121+
EnvironmentVariables = targetCommand.CustomEnvironmentVariables ?? ReadOnlyDictionary<string, string?>.Empty,
122+
};
123+
}
124+
}
77125
}
78126

79127
[JsonDerivedType(typeof(Error), nameof(Error))]
80128
[JsonDerivedType(typeof(Project), nameof(Project))]
129+
[JsonDerivedType(typeof(RunCommand), nameof(RunCommand))]
81130
internal abstract class RunApiOutput
82131
{
83132
private RunApiOutput() { }
@@ -100,6 +149,14 @@ public sealed class Project : RunApiOutput
100149
public required string Content { get; init; }
101150
public required ImmutableArray<SimpleDiagnostic> Diagnostics { get; init; }
102151
}
152+
153+
public sealed class RunCommand : RunApiOutput
154+
{
155+
public required string ExecutablePath { get; init; }
156+
public required string CommandLineArguments { get; init; }
157+
public required string? WorkingDirectory { get; init; }
158+
public required IReadOnlyDictionary<string, string?> EnvironmentVariables { get; init; }
159+
}
103160
}
104161

105162
[JsonSerializable(typeof(RunApiInput))]

src/Cli/dotnet/Commands/Run/RunCommand.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public class RunCommand
5353

5454
private bool ShouldBuild => !NoBuild;
5555

56-
public string LaunchProfile { get; }
56+
public string? LaunchProfile { get; }
5757
public bool NoLaunchProfile { get; }
5858

5959
/// <summary>
@@ -64,7 +64,7 @@ public class RunCommand
6464
public RunCommand(
6565
bool noBuild,
6666
string? projectFileOrDirectory,
67-
string launchProfile,
67+
string? launchProfile,
6868
bool noLaunchProfile,
6969
bool noLaunchProfileArguments,
7070
bool noRestore,
@@ -145,7 +145,7 @@ public int Execute()
145145
}
146146
}
147147

148-
private void ApplyLaunchSettingsProfileToCommand(ICommand targetCommand, ProjectLaunchSettingsModel? launchSettings)
148+
internal void ApplyLaunchSettingsProfileToCommand(ICommand targetCommand, ProjectLaunchSettingsModel? launchSettings)
149149
{
150150
if (launchSettings == null)
151151
{
@@ -172,7 +172,7 @@ private void ApplyLaunchSettingsProfileToCommand(ICommand targetCommand, Project
172172
}
173173
}
174174

175-
private bool TryGetLaunchProfileSettingsIfNeeded(out ProjectLaunchSettingsModel? launchSettingsModel)
175+
internal bool TryGetLaunchProfileSettingsIfNeeded(out ProjectLaunchSettingsModel? launchSettingsModel)
176176
{
177177
launchSettingsModel = default;
178178
if (NoLaunchProfile)
@@ -310,7 +310,7 @@ internal static VerbosityOptions GetDefaultVerbosity(bool interactive)
310310
return interactive ? VerbosityOptions.minimal : VerbosityOptions.quiet;
311311
}
312312

313-
private ICommand GetTargetCommand(Func<ProjectCollection, ProjectInstance>? projectFactory)
313+
internal ICommand GetTargetCommand(Func<ProjectCollection, ProjectInstance>? projectFactory)
314314
{
315315
FacadeLogger? logger = LoggerUtility.DetermineBinlogger(RestoreArgs, "dotnet-run");
316316
var project = EvaluateProject(ProjectFileFullPath, projectFactory, RestoreArgs, logger);

src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public VirtualProjectBuildingCommand(
7979
public Dictionary<string, string> GlobalProperties { get; }
8080
public string[] BinaryLoggerArgs { get; }
8181
public VerbosityOptions Verbosity { get; }
82+
public string? CustomArtifactsPath { get; init; }
8283
public bool NoRestore { get; init; }
8384
public bool NoCache { get; init; }
8485
public bool NoBuild { get; init; }
@@ -437,7 +438,7 @@ ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection)
437438
}
438439
}
439440

440-
private string GetArtifactsPath() => GetArtifactsPath(EntryPointFileFullPath);
441+
private string GetArtifactsPath() => CustomArtifactsPath ?? GetArtifactsPath(EntryPointFileFullPath);
441442

442443
// internal for testing
443444
internal static string GetArtifactsPath(string entryPointFileFullPath)

test/dotnet.Tests/CommandTests/Run/RunFileTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1517,6 +1517,29 @@ public void Api_Error()
15171517
.And.HaveStdOutContaining("Unknown2");
15181518
}
15191519

1520+
[Fact]
1521+
public void Api_RunCommand()
1522+
{
1523+
var testInstance = _testAssetsManager.CreateTestDirectory();
1524+
var programPath = Path.Join(testInstance.Path, "Program.cs");
1525+
File.WriteAllText(programPath, """
1526+
Console.WriteLine();
1527+
""");
1528+
1529+
string artifactsPath = OperatingSystem.IsWindows() ? @"C:\artifacts" : "/artifacts";
1530+
string executablePath = OperatingSystem.IsWindows() ? @"C:\artifacts\bin\debug\Program.exe" : "/artifacts/bin/debug/Program";
1531+
new DotnetCommand(Log, "run-api")
1532+
.WithStandardInput($$"""
1533+
{"$type":"GetRunCommand","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":{{ToJson(artifactsPath)}}}
1534+
""")
1535+
.Execute()
1536+
.Should().Pass()
1537+
// DOTNET_ROOT environment variable is platform dependent so we don't verify it fully for simplicity
1538+
.And.HaveStdOutContaining($$"""
1539+
{"$type":"RunCommand","Version":1,"ExecutablePath":{{ToJson(executablePath)}},"CommandLineArguments":"","WorkingDirectory":"","EnvironmentVariables":{"DOTNET_ROOT
1540+
""");
1541+
}
1542+
15201543
[Fact]
15211544
public void EntryPointFilePath()
15221545
{

0 commit comments

Comments
 (0)