Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Cleanup
  • Loading branch information
tmat committed Dec 11, 2025
commit 9c77d23bc40558db50889cc29e2e256d72ae28bc
6 changes: 6 additions & 0 deletions src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,9 @@ Make the profile names distinct.</value>
<data name="LaunchProfileIsNotAJsonObject" xml:space="preserve">
<value>A profile with the specified name isn't a valid JSON object.</value>
</data>
<data name="LaunchProfile0IsMissingProperty1" xml:space="preserve">
<value>Launch profile '{0}' is missing property '{1}'</value>
</data>
<data name="LaunchProfilesCollectionIsNotAJsonObject" xml:space="preserve">
<value>The 'profiles' property of the launch settings document is not a JSON object.</value>
</data>
Expand Down Expand Up @@ -1761,6 +1764,9 @@ Your project targets multiple frameworks. Specify which framework to run using '
<data name="RunCommandSpecifiedFileIsNotAValidProject" xml:space="preserve">
<value>'{0}' is not a valid project file.</value>
</data>
<data name="Path0SpecifiedIn1IsInvalid" xml:space="preserve">
<value>Path '{0}' specified in '{1}' is invalid.</value>
</data>
<data name="RunConfigurationOptionDescription" xml:space="preserve">
<value>The configuration to run for. The default for most projects is 'Debug'.</value>
</data>
Expand Down
5 changes: 2 additions & 3 deletions src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,8 @@ public override RunApiOutput Execute()
environmentVariables: ReadOnlyDictionary<string, string>.Empty,
msbuildRestoreProperties: ReadOnlyDictionary<string, string>.Empty);

runCommand.TryGetLaunchProfileSettingsIfNeeded(out var launchSettings);
var targetCommand = (Utils.Command)runCommand.GetTargetCommand(buildCommand.CreateProjectInstance, cachedRunProperties: null);
runCommand.ApplyLaunchSettingsProfileToCommand(targetCommand, launchSettings);
var result = runCommand.ReadLaunchProfileSettings();
var targetCommand = (Utils.Command)runCommand.GetTargetCommand(result.Model, buildCommand.CreateProjectInstance, cachedRunProperties: null);

return new RunApiOutput.RunCommand
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ internal class ExecutableLaunchProfileJson
[JsonPropertyName("workingDirectory")]
public string? WorkingDirectory { get; set; }

[JsonPropertyName("dotnetRunMessages")]
public bool DotNetRunMessages { get; set; }

[JsonPropertyName("environmentVariables")]
public Dictionary<string, string>? EnvironmentVariables { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

namespace Microsoft.DotNet.Cli.Commands.Run.LaunchSettings;

public class ExecutableLaunchSettingsModel : LaunchSettingsModel
public sealed class ExecutableLaunchSettingsModel : LaunchSettingsModel
{
public string? ExecutablePath { get; set; }
public const string WorkingDirectoryPropertyName = "workingDirectory";
public const string ExecutablePathPropertyName = "executablePath";

public string? WorkingDirectory { get; set; }

public override LaunchProfileKind ProfileKind => LaunchProfileKind.Executable;
public required string ExecutablePath { get; init; }
public string? WorkingDirectory { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Text.Json;

namespace Microsoft.DotNet.Cli.Commands.Run.LaunchSettings;

internal sealed class ExecutableLaunchSettingsParser : LaunchProfileParser
{
public const string CommandName = "Executable";

public static readonly ExecutableLaunchSettingsParser Instance = new();

private ExecutableLaunchSettingsParser()
{
}

public override LaunchProfileSettings ParseProfile(string launchSettingsPath, string? launchProfileName, string json)
{
var profile = JsonSerializer.Deserialize<ExecutableLaunchProfileJson>(json);
if (profile == null)
{
return LaunchProfileSettings.Failure(CliCommandStrings.LaunchProfileIsNotAJsonObject);
}

if (profile.ExecutablePath == null)
{
return LaunchProfileSettings.Failure(
string.Format(
CliCommandStrings.LaunchProfile0IsMissingProperty1,
RunCommand.GetLaunchProfileDisplayName(launchProfileName),
ExecutableLaunchSettingsModel.ExecutablePathPropertyName));
}

if (!TryParseWorkingDirectory(launchSettingsPath, profile.WorkingDirectory, out var workingDirectory, out var error))
{
return LaunchProfileSettings.Failure(error);
}

return LaunchProfileSettings.Success(new ExecutableLaunchSettingsModel
{
LaunchProfileName = launchProfileName,
ExecutablePath = ExpandVariables(profile.ExecutablePath),
CommandLineArgs = ParseCommandLineArgs(profile.CommandLineArgs),
WorkingDirectory = workingDirectory,
DotNetRunMessages = profile.DotNetRunMessages,
EnvironmentVariables = ParseEnvironmentVariables(profile.EnvironmentVariables),
});
}

private static bool TryParseWorkingDirectory(string launchSettingsPath, string? value, out string? workingDirectory, [NotNullWhen(false)] out string? error)
{
if (value == null)
{
workingDirectory = null;
error = null;
return true;
}

var expandedValue = ExpandVariables(value);

try
{
workingDirectory = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(launchSettingsPath)!, expandedValue));
error = null;
return true;
}
catch
{
workingDirectory = null;
error = string.Format(CliCommandStrings.Path0SpecifiedIn1IsInvalid, expandedValue, ExecutableLaunchSettingsModel.WorkingDirectoryPropertyName);
return false;
}
}
}

This file was deleted.

This file was deleted.

36 changes: 36 additions & 0 deletions src/Cli/dotnet/Commands/Run/LaunchSettings/LaunchProfileParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;

namespace Microsoft.DotNet.Cli.Commands.Run.LaunchSettings;

internal abstract class LaunchProfileParser
{
public abstract LaunchProfileSettings ParseProfile(string launchSettingsPath, string? launchProfileName, string json);

protected static string? ParseCommandLineArgs(string? value)
=> value != null ? ExpandVariables(value) : null;

protected static ImmutableDictionary<string, string> ParseEnvironmentVariables(Dictionary<string, string>? values)
{
if (values is null or { Count: 0 })
{
return [];
}

var builder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
foreach (var (key, value) in values)
{
// override previously set variables:
builder[key] = ExpandVariables(value);
}

return builder.ToImmutable();
}

// TODO: Expand MSBuild variables $(...): https://github.com/dotnet/sdk/issues/50157
// See https://github.com/dotnet/project-system/blob/main/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/DebugTokenReplacer.cs#L35-L57
protected static string ExpandVariables(string value)
=> Environment.ExpandEnvironmentVariables(value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Microsoft.DotNet.Cli.Commands.Run.LaunchSettings;

public sealed class LaunchProfileSettings
{
public string? FailureReason { get; }

public LaunchSettingsModel? Model { get; }

private LaunchProfileSettings(string? failureReason, LaunchSettingsModel? launchSettings)
{
FailureReason = failureReason;
Model = launchSettings;
}

[MemberNotNullWhen(false, nameof(FailureReason))]
public bool Successful
=> FailureReason == null;

public static LaunchProfileSettings Failure(string reason)
=> new(reason, launchSettings: null);

public static LaunchProfileSettings Success(LaunchSettingsModel? model)
=> new(failureReason: null, launchSettings: model);
}

This file was deleted.

40 changes: 20 additions & 20 deletions src/Cli/dotnet/Commands/Run/LaunchSettings/LaunchSettingsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,29 @@

namespace Microsoft.DotNet.Cli.Commands.Run.LaunchSettings;

internal class LaunchSettingsManager
internal sealed class LaunchSettingsManager
{
private const string ProfilesKey = "profiles";
private const string CommandNameKey = "commandName";
private const string DefaultProfileCommandName = "Project";
private static readonly IReadOnlyDictionary<string, ILaunchSettingsProvider> _providers;
private static readonly IReadOnlyDictionary<string, LaunchProfileParser> _providers;

public static IEnumerable<string> SupportedProfileTypes => _providers.Keys;

static LaunchSettingsManager()
{
_providers = new Dictionary<string, ILaunchSettingsProvider>
_providers = new Dictionary<string, LaunchProfileParser>
{
{ ProjectLaunchSettingsProvider.CommandNameValue, new ProjectLaunchSettingsProvider() },
{ ExecutableLaunchSettingsProvider.CommandNameValue, new ExecutableLaunchSettingsProvider() }
{ ProjectLaunchSettingsParser.CommandName, ProjectLaunchSettingsParser.Instance },
{ ExecutableLaunchSettingsParser.CommandName, ExecutableLaunchSettingsParser.Instance }
};
}

public static LaunchSettingsApplyResult TryApplyLaunchSettings(string launchSettingsPath, string? profileName = null)
public static LaunchProfileSettings ReadProfileSettingsFromFile(string launchSettingsPath, string? profileName = null)
{
var launchSettingsJsonContents = File.ReadAllText(launchSettingsPath);
try
{
var launchSettingsJsonContents = File.ReadAllText(launchSettingsPath);

var jsonDocumentOptions = new JsonDocumentOptions
{
CommentHandling = JsonCommentHandling.Skip,
Expand All @@ -42,7 +42,7 @@ public static LaunchSettingsApplyResult TryApplyLaunchSettings(string launchSett

if (model.ValueKind != JsonValueKind.Object || !model.TryGetProperty(ProfilesKey, out var profilesObject) || profilesObject.ValueKind != JsonValueKind.Object)
{
return new LaunchSettingsApplyResult(false, CliCommandStrings.LaunchProfilesCollectionIsNotAJsonObject);
return LaunchProfileSettings.Failure(CliCommandStrings.LaunchProfilesCollectionIsNotAJsonObject);
}

var selectedProfileName = profileName;
Expand All @@ -66,7 +66,7 @@ public static LaunchSettingsApplyResult TryApplyLaunchSettings(string launchSett
}
else if (!caseInsensitiveProfileMatches.Any())
{
return new LaunchSettingsApplyResult(false, string.Format(CliCommandStrings.LaunchProfileDoesNotExist, profileName));
return LaunchProfileSettings.Failure(string.Format(CliCommandStrings.LaunchProfileDoesNotExist, profileName));
}
else
{
Expand All @@ -75,7 +75,7 @@ public static LaunchSettingsApplyResult TryApplyLaunchSettings(string launchSett

if (profileObject.ValueKind != JsonValueKind.Object)
{
return new LaunchSettingsApplyResult(false, CliCommandStrings.LaunchProfileIsNotAJsonObject);
return LaunchProfileSettings.Failure(CliCommandStrings.LaunchProfileIsNotAJsonObject);
}
}

Expand All @@ -87,7 +87,7 @@ public static LaunchSettingsApplyResult TryApplyLaunchSettings(string launchSett
{
if (prop.Value.TryGetProperty(CommandNameKey, out var commandNameElement) && commandNameElement.ValueKind == JsonValueKind.String)
{
if (commandNameElement.GetString() is { } commandNameElementKey && _providers.ContainsKey(commandNameElementKey))
if (commandNameElement.GetString() is { } commandNameElementKey && _providers.ContainsKey(commandNameElementKey))
{
profileObject = prop.Value;
break;
Expand All @@ -99,31 +99,31 @@ public static LaunchSettingsApplyResult TryApplyLaunchSettings(string launchSett

if (profileObject.ValueKind == default)
{
return new LaunchSettingsApplyResult(false, CliCommandStrings.UsableLaunchProfileCannotBeLocated);
return LaunchProfileSettings.Failure(CliCommandStrings.UsableLaunchProfileCannotBeLocated);
}

if (!profileObject.TryGetProperty(CommandNameKey, out var finalCommandNameElement)
|| finalCommandNameElement.ValueKind != JsonValueKind.String)
{
return new LaunchSettingsApplyResult(false, CliCommandStrings.UsableLaunchProfileCannotBeLocated);
return LaunchProfileSettings.Failure(CliCommandStrings.UsableLaunchProfileCannotBeLocated);
}

string? commandName = finalCommandNameElement.GetString();
if (!TryLocateHandler(commandName, out ILaunchSettingsProvider? provider))
if (!TryLocateHandler(commandName, out LaunchProfileParser? provider))
{
return new LaunchSettingsApplyResult(false, string.Format(CliCommandStrings.LaunchProfileHandlerCannotBeLocated, commandName));
return LaunchProfileSettings.Failure(string.Format(CliCommandStrings.LaunchProfileHandlerCannotBeLocated, commandName));
}

return provider.TryGetLaunchSettings(selectedProfileName, profileObject);
return provider.ParseProfile(launchSettingsPath, selectedProfileName, profileObject.GetRawText());
}
}
catch (JsonException ex)
catch (Exception ex) when (ex is JsonException or IOException)
{
return new LaunchSettingsApplyResult(false, string.Format(CliCommandStrings.DeserializationExceptionMessage, launchSettingsPath, ex.Message));
return LaunchProfileSettings.Failure(string.Format(CliCommandStrings.DeserializationExceptionMessage, launchSettingsPath, ex.Message));
}
}

private static bool TryLocateHandler(string? commandName, [NotNullWhen(true)]out ILaunchSettingsProvider? provider)
private static bool TryLocateHandler(string? commandName, [NotNullWhen(true)]out LaunchProfileParser? provider)
{
if (commandName == null)
{
Expand Down
Loading