Skip to content

Commit 4ab59c9

Browse files
authored
Use -e to set env variables (#45844)
1 parent 569c39b commit 4ab59c9

File tree

11 files changed

+259
-123
lines changed

11 files changed

+259
-123
lines changed

src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ internal sealed class StartupHook
1212
{
1313
private static readonly bool s_logToStandardOutput = Environment.GetEnvironmentVariable(EnvironmentVariables.Names.HotReloadDeltaClientLogMessages) == "1";
1414
private static readonly string s_namedPipeName = Environment.GetEnvironmentVariable(EnvironmentVariables.Names.DotnetWatchHotReloadNamedPipeName);
15-
private static readonly string s_targetProcessPath = Environment.GetEnvironmentVariable(EnvironmentVariables.Names.DotnetWatchHotReloadTargetProcessPath);
1615

1716
/// <summary>
1817
/// Invoked by the runtime when the containing assembly is listed in DOTNET_STARTUP_HOOKS.
@@ -21,16 +20,6 @@ public static void Initialize()
2120
{
2221
var processPath = Environment.GetCommandLineArgs().FirstOrDefault();
2322

24-
// Workaround for https://github.com/dotnet/sdk/issues/40484
25-
// When launching the application process dotnet-watch sets Hot Reload environment variables via CLI environment directives (dotnet [env:X=Y] run).
26-
// Currently, the CLI parser sets the env variables to the dotnet.exe process itself, rather then to the target process.
27-
// This may cause the dotnet.exe process to connect to the named pipe and break it for the target process.
28-
if (!IsMatchingProcess(processPath, s_targetProcessPath))
29-
{
30-
Log($"Ignoring process '{processPath}', expecting '{s_targetProcessPath}'");
31-
return;
32-
}
33-
3423
Log($"Loaded into process: {processPath}");
3524

3625
ClearHotReloadEnvironmentVariables();

src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace Microsoft.DotNet.Watch;
1212

1313
internal class AspireServiceFactory : IRuntimeProcessLauncherFactory
1414
{
15-
private sealed class SessionManager : IAspireServerEvents, IRuntimeProcessLauncher
15+
internal sealed class SessionManager : IAspireServerEvents, IRuntimeProcessLauncher
1616
{
1717
private readonly struct Session(string dcpId, string sessionId, RunningProject runningProject, Task outputReader)
1818
{
@@ -30,7 +30,7 @@ private readonly struct Session(string dcpId, string sessionId, RunningProject r
3030

3131
private readonly ProjectLauncher _projectLauncher;
3232
private readonly AspireServerService _service;
33-
private readonly IReadOnlyList<string> _buildArguments;
33+
private readonly ProjectOptions _hostProjectOptions;
3434

3535
/// <summary>
3636
/// Lock to access:
@@ -43,10 +43,10 @@ private readonly struct Session(string dcpId, string sessionId, RunningProject r
4343
private int _sessionIdDispenser;
4444
private volatile bool _isDisposed;
4545

46-
public SessionManager(ProjectLauncher projectLauncher, IReadOnlyList<string> buildArguments)
46+
public SessionManager(ProjectLauncher projectLauncher, ProjectOptions hostProjectOptions)
4747
{
4848
_projectLauncher = projectLauncher;
49-
_buildArguments = buildArguments;
49+
_hostProjectOptions = hostProjectOptions;
5050

5151
_service = new AspireServerService(
5252
this,
@@ -204,44 +204,64 @@ private async ValueTask TerminateSessionAsync(Session session, CancellationToken
204204
}
205205

206206
private ProjectOptions GetProjectOptions(ProjectLaunchRequest projectLaunchInfo)
207+
{
208+
var hostLaunchProfile = _hostProjectOptions.NoLaunchProfile ? null : _hostProjectOptions.LaunchProfileName;
209+
210+
return new()
211+
{
212+
IsRootProject = false,
213+
ProjectPath = projectLaunchInfo.ProjectPath,
214+
WorkingDirectory = _projectLauncher.EnvironmentOptions.WorkingDirectory,
215+
BuildArguments = _hostProjectOptions.BuildArguments,
216+
Command = "run",
217+
CommandArguments = GetRunCommandArguments(projectLaunchInfo, hostLaunchProfile),
218+
LaunchEnvironmentVariables = projectLaunchInfo.Environment?.Select(e => (e.Key, e.Value))?.ToArray() ?? [],
219+
LaunchProfileName = projectLaunchInfo.LaunchProfile,
220+
NoLaunchProfile = projectLaunchInfo.DisableLaunchProfile,
221+
TargetFramework = _hostProjectOptions.TargetFramework,
222+
};
223+
}
224+
225+
// internal for testing
226+
internal static IReadOnlyList<string> GetRunCommandArguments(ProjectLaunchRequest projectLaunchInfo, string? hostLaunchProfile)
207227
{
208228
var arguments = new List<string>
209229
{
210230
"--project",
211231
projectLaunchInfo.ProjectPath,
212-
// TODO: https://github.com/dotnet/sdk/issues/43946
213-
// Need to suppress launch profile for now, otherwise it would override the port set via env variable.
214-
"--no-launch-profile",
215232
};
216233

217-
//if (projectLaunchInfo.DisableLaunchProfile)
218-
//{
219-
// arguments.Add("--no-launch-profile");
220-
//}
221-
//else if (!string.IsNullOrEmpty(projectLaunchInfo.LaunchProfile))
222-
//{
223-
// arguments.Add("--launch-profile");
224-
// arguments.Add(projectLaunchInfo.LaunchProfile);
225-
//}
234+
// Implements https://github.com/dotnet/aspire/blob/main/docs/specs/IDE-execution.md#launch-profile-processing-project-launch-configuration
226235

227-
if (projectLaunchInfo.Arguments != null)
236+
if (projectLaunchInfo.DisableLaunchProfile)
237+
{
238+
arguments.Add("--no-launch-profile");
239+
}
240+
else if (!string.IsNullOrEmpty(projectLaunchInfo.LaunchProfile))
228241
{
229-
arguments.AddRange(projectLaunchInfo.Arguments);
242+
arguments.Add("--launch-profile");
243+
arguments.Add(projectLaunchInfo.LaunchProfile);
244+
}
245+
else if (hostLaunchProfile != null)
246+
{
247+
arguments.Add("--launch-profile");
248+
arguments.Add(hostLaunchProfile);
230249
}
231250

232-
return new()
251+
if (projectLaunchInfo.Arguments != null)
233252
{
234-
IsRootProject = false,
235-
ProjectPath = projectLaunchInfo.ProjectPath,
236-
WorkingDirectory = _projectLauncher.EnvironmentOptions.WorkingDirectory, // TODO: Should DCP protocol specify?
237-
BuildArguments = _buildArguments, // TODO: Should DCP protocol specify?
238-
Command = "run",
239-
CommandArguments = arguments,
240-
LaunchEnvironmentVariables = projectLaunchInfo.Environment?.Select(kvp => (kvp.Key, kvp.Value)).ToArray() ?? [],
241-
LaunchProfileName = projectLaunchInfo.LaunchProfile,
242-
NoLaunchProfile = projectLaunchInfo.DisableLaunchProfile,
243-
TargetFramework = null, // TODO: Should DCP protocol specify?
244-
};
253+
if (projectLaunchInfo.Arguments.Any())
254+
{
255+
arguments.AddRange(projectLaunchInfo.Arguments);
256+
}
257+
else
258+
{
259+
// indicate that no arguments should be used even if launch profile specifies some:
260+
arguments.Add("--no-launch-profile-arguments");
261+
}
262+
}
263+
264+
return arguments;
245265
}
246266
}
247267

@@ -250,8 +270,8 @@ private ProjectOptions GetProjectOptions(ProjectLaunchRequest projectLaunchInfo)
250270
public static readonly AspireServiceFactory Instance = new();
251271
public const string AppHostProjectCapability = "Aspire";
252272

253-
public IRuntimeProcessLauncher? TryCreate(ProjectGraphNode projectNode, ProjectLauncher projectLauncher, IReadOnlyList<string> buildArguments)
273+
public IRuntimeProcessLauncher? TryCreate(ProjectGraphNode projectNode, ProjectLauncher projectLauncher, ProjectOptions hostProjectOptions)
254274
=> projectNode.GetCapabilities().Contains(AppHostProjectCapability)
255-
? new SessionManager(projectLauncher, buildArguments)
275+
? new SessionManager(projectLauncher, hostProjectOptions)
256276
: null;
257277
}

src/BuiltInTools/dotnet-watch/DotNetWatcher.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
6565
? await browserConnector.LaunchOrRefreshBrowserAsync(projectRootNode, processSpec, environmentBuilder, Context.RootProjectOptions, shutdownCancellationToken)
6666
: null;
6767

68-
environmentBuilder.ConfigureProcess(processSpec);
68+
environmentBuilder.SetProcessEnvironmentVariables(processSpec);
6969

7070
// Reset for next run
7171
buildEvaluator.RequiresRevaluation = false;

src/BuiltInTools/dotnet-watch/EnvironmentVariablesBuilder.cs

Lines changed: 13 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,6 @@ internal sealed class EnvironmentVariablesBuilder
1818
/// </summary>
1919
private readonly Dictionary<string, string> _variables = [];
2020

21-
/// <summary>
22-
/// Environment variables passed as directives on command line (dotnet [env:name=value] run).
23-
/// Currently, the effect is the same as setting <see cref="_variables"/> due to
24-
/// https://github.com/dotnet/sdk/issues/40484
25-
/// </summary>
26-
private readonly Dictionary<string, string> _directives = [];
27-
2821
public static EnvironmentVariablesBuilder FromCurrentEnvironment()
2922
{
3023
var builder = new EnvironmentVariablesBuilder();
@@ -42,57 +35,39 @@ public static EnvironmentVariablesBuilder FromCurrentEnvironment()
4235
return builder;
4336
}
4437

45-
public void SetDirective(string name, string value)
46-
{
47-
// should use DotNetStartupHookDirective
48-
Debug.Assert(!name.Equals(EnvironmentVariables.Names.DotnetStartupHooks, StringComparison.OrdinalIgnoreCase));
49-
50-
_directives[name] = value;
51-
}
52-
5338
public void SetVariable(string name, string value)
5439
{
55-
// should use AspNetCoreHostingStartupAssembliesVariable
40+
// should use AspNetCoreHostingStartupAssembliesVariable/DotNetStartupHookDirective
5641
Debug.Assert(!name.Equals(EnvironmentVariables.Names.AspNetCoreHostingStartupAssemblies, StringComparison.OrdinalIgnoreCase));
42+
Debug.Assert(!name.Equals(EnvironmentVariables.Names.DotnetStartupHooks, StringComparison.OrdinalIgnoreCase));
5743

5844
_variables[name] = value;
5945
}
6046

61-
public void ConfigureProcess(ProcessSpec processSpec)
47+
public void SetProcessEnvironmentVariables(ProcessSpec processSpec)
6248
{
63-
processSpec.Arguments = [.. GetCommandLineDirectives(), .. processSpec.Arguments ?? []];
64-
AddToEnvironment(processSpec.EnvironmentVariables);
65-
}
66-
67-
// for testing
68-
internal void AddToEnvironment(Dictionary<string, string> variables)
69-
{
70-
foreach (var (name, value) in _variables)
71-
{
72-
variables.Add(name, value);
73-
}
74-
75-
if (AspNetCoreHostingStartupAssembliesVariable is not [])
49+
foreach (var (name, value) in GetEnvironment())
7650
{
77-
variables.Add(EnvironmentVariables.Names.AspNetCoreHostingStartupAssemblies, string.Join(AssembliesSeparator, AspNetCoreHostingStartupAssembliesVariable));
51+
processSpec.EnvironmentVariables.Add(name, value);
7852
}
7953
}
8054

81-
// for testing
82-
internal IEnumerable<string> GetCommandLineDirectives()
55+
public IEnumerable<(string name, string value)> GetEnvironment()
8356
{
84-
foreach (var (name, value) in _directives)
57+
foreach (var (name, value) in _variables)
8558
{
86-
yield return MakeDirective(name, value);
59+
yield return (name, value);
8760
}
8861

8962
if (DotNetStartupHookDirective is not [])
9063
{
91-
yield return MakeDirective(EnvironmentVariables.Names.DotnetStartupHooks, string.Join(s_startupHooksSeparator, DotNetStartupHookDirective));
64+
yield return (EnvironmentVariables.Names.DotnetStartupHooks, string.Join(s_startupHooksSeparator, DotNetStartupHookDirective));
9265
}
9366

94-
static string MakeDirective(string name, string value)
95-
=> $"[env:{name}={value}]";
67+
if (AspNetCoreHostingStartupAssembliesVariable is not [])
68+
{
69+
yield return (EnvironmentVariables.Names.AspNetCoreHostingStartupAssemblies, string.Join(AssembliesSeparator, AspNetCoreHostingStartupAssembliesVariable));
70+
}
9671
}
9772
}
9873
}

src/BuiltInTools/dotnet-watch/EnvironmentVariables_StartupHook.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,6 @@ public static partial class Names
1313
/// </summary>
1414
public const string DotnetWatchHotReloadNamedPipeName = "DOTNET_WATCH_HOTRELOAD_NAMEDPIPE_NAME";
1515

16-
/// <summary>
17-
/// The full path to the process being launched by dotnet run.
18-
/// Workaround for https://github.com/dotnet/sdk/issues/40484
19-
/// </summary>
20-
public const string DotnetWatchHotReloadTargetProcessPath = "DOTNET_WATCH_HOTRELOAD_TARGET_PROCESS_PATH";
21-
2216
/// <summary>
2317
/// Enables logging from the client delta applier agent.
2418
/// </summary>

src/BuiltInTools/dotnet-watch/HotReload/IRuntimeProcessLauncherFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ namespace Microsoft.DotNet.Watch;
1212
/// </summary>
1313
internal interface IRuntimeProcessLauncherFactory
1414
{
15-
public IRuntimeProcessLauncher? TryCreate(ProjectGraphNode projectNode, ProjectLauncher projectLauncher, IReadOnlyList<string> buildArguments);
15+
public IRuntimeProcessLauncher? TryCreate(ProjectGraphNode projectNode, ProjectLauncher projectLauncher, ProjectOptions hostProjectOptions);
1616
}

src/BuiltInTools/dotnet-watch/HotReload/ProjectLauncher.cs

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using System.Globalization;
56
using Microsoft.Build.Graph;
67

@@ -53,17 +54,12 @@ public EnvironmentOptions EnvironmentOptions
5354
{
5455
Executable = EnvironmentOptions.MuxerPath,
5556
WorkingDirectory = projectOptions.WorkingDirectory,
56-
OnOutput = onOutput,
57-
Arguments = [projectOptions.Command, "--no-build", .. projectOptions.CommandArguments]
57+
OnOutput = onOutput
5858
};
5959

6060
var environmentBuilder = EnvironmentVariablesBuilder.FromCurrentEnvironment();
6161
var namedPipeName = Guid.NewGuid().ToString();
6262

63-
// Directives:
64-
65-
// Variables:
66-
6763
foreach (var (name, value) in projectOptions.LaunchEnvironmentVariables)
6864
{
6965
// ignore dotnet-watch reserved variables -- these shouldn't be set by the project
@@ -85,30 +81,36 @@ public EnvironmentOptions EnvironmentOptions
8581
// expect DOTNET_MODIFIABLE_ASSEMBLIES to be set in the blazor-devserver process, even though we are not performing Hot Reload in this process.
8682
// The value is converted to DOTNET-MODIFIABLE-ASSEMBLIES header, which is in turn converted back to environment variable in Mono browser runtime loader:
8783
// https://github.com/dotnet/runtime/blob/342936c5a88653f0f622e9d6cb727a0e59279b31/src/mono/browser/runtime/loader/config.ts#L330
88-
environmentBuilder.SetDirective(EnvironmentVariables.Names.DotnetModifiableAssemblies, "debug");
84+
environmentBuilder.SetVariable(EnvironmentVariables.Names.DotnetModifiableAssemblies, "debug");
8985

9086
if (injectDeltaApplier)
9187
{
9288
environmentBuilder.DotNetStartupHookDirective.Add(DeltaApplier.StartupHookPath);
93-
environmentBuilder.SetDirective(EnvironmentVariables.Names.DotnetWatchHotReloadNamedPipeName, namedPipeName);
94-
95-
// Do not ask agent to log to stdout until https://github.com/dotnet/sdk/issues/40484 is fixed.
96-
// For now we need to set the env variable explicitly when we need to diagnose issue with the agent.
97-
// Build targets might launch a process and read it's stdout. If the agent is loaded into such process and starts logging
98-
// to stdout it might interfere with the expected output.
99-
//if (context.Options.Verbose)
100-
//{
101-
// environmentBuilder.SetVariable(EnvironmentVariables.Names.HotReloadDeltaClientLogMessages, "1");
102-
//}
103-
104-
// TODO: workaround for https://github.com/dotnet/sdk/issues/40484
105-
var targetPath = projectNode.ProjectInstance.GetPropertyValue("RunCommand");
106-
environmentBuilder.SetVariable(EnvironmentVariables.Names.DotnetWatchHotReloadTargetProcessPath, targetPath);
107-
Reporter.Verbose($"Target process is '{targetPath}'");
89+
environmentBuilder.SetVariable(EnvironmentVariables.Names.DotnetWatchHotReloadNamedPipeName, namedPipeName);
90+
91+
if (context.Options.Verbose)
92+
{
93+
environmentBuilder.SetVariable(EnvironmentVariables.Names.HotReloadDeltaClientLogMessages, "1");
94+
}
10895
}
10996

11097
var browserRefreshServer = await browserConnector.LaunchOrRefreshBrowserAsync(projectNode, processSpec, environmentBuilder, projectOptions, cancellationToken);
111-
environmentBuilder.ConfigureProcess(processSpec);
98+
99+
var arguments = new List<string>()
100+
{
101+
projectOptions.Command,
102+
"--no-build"
103+
};
104+
105+
foreach (var (name, value) in environmentBuilder.GetEnvironment())
106+
{
107+
arguments.Add("-e");
108+
arguments.Add($"{name}={value}");
109+
}
110+
111+
arguments.AddRange(projectOptions.CommandArguments);
112+
113+
processSpec.Arguments = arguments;
112114

113115
var processReporter = new ProjectSpecificReporter(projectNode, Reporter);
114116

src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
111111

112112
var rootProjectNode = evaluationResult.ProjectGraph.GraphRoots.Single();
113113

114-
runtimeProcessLauncher = runtimeProcessLauncherFactory?.TryCreate(rootProjectNode, projectLauncher, rootProjectOptions.BuildArguments);
114+
runtimeProcessLauncher = runtimeProcessLauncherFactory?.TryCreate(rootProjectNode, projectLauncher, rootProjectOptions);
115115
if (runtimeProcessLauncher != null)
116116
{
117117
var launcherEnvironment = runtimeProcessLauncher.GetEnvironmentVariables();

0 commit comments

Comments
 (0)