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

Append command line arguments to project specified RunArguments #37285

Merged
merged 1 commit into from
Dec 6, 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
35 changes: 35 additions & 0 deletions src/Assets/TestProjects/WatchHotReloadAppCustomHost/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.Versioning;
using System.Threading;

var assembly = typeof(C).Assembly;

Console.WriteLine("Started");

// Process ID is insufficient because PID's may be reused.
Console.WriteLine($"Process identifier = {Process.GetCurrentProcess().Id}, {Process.GetCurrentProcess().StartTime:hh:mm:ss.FF}");
Console.WriteLine($"DOTNET_WATCH = {Environment.GetEnvironmentVariable("DOTNET_WATCH")}");
Console.WriteLine($"DOTNET_WATCH_ITERATION = {Environment.GetEnvironmentVariable("DOTNET_WATCH_ITERATION")}");
Console.WriteLine($"Arguments = {string.Join(",", args)}");
Console.WriteLine($"Version = {assembly.GetCustomAttributes<AssemblyVersionAttribute>().FirstOrDefault()?.Version ?? "<unspecified>"}");
Console.WriteLine($"TFM = {assembly.GetCustomAttributes<TargetFrameworkAttribute>().FirstOrDefault()?.FrameworkName ?? "<unspecified>"}");
Console.WriteLine($"Configuration = {assembly.GetCustomAttributes<AssemblyConfigurationAttribute>().FirstOrDefault()?.Configuration ?? "<unspecified>"}");

Loop();

static void Loop()
{
while (true)
{
Console.WriteLine(".");
Thread.Sleep(1000);
}
}

class C { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"profiles": {
"app": {
"commandName": "Project",
"environmentVariables": {
"EnvironmentFromProfile": "Development"
}
},
"P1": {
"commandName": "Project",
"commandLineArgs": "\"Arg1 from launch profile\" \"Arg2 from launch profile\""
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(CurrentTargetFramework)</TargetFramework>
<OutputType>Exe</OutputType>
<RunArguments>&quot;Argument Specified in Props&quot;</RunArguments>
</PropertyGroup>
</Project>
2 changes: 1 addition & 1 deletion src/BuiltInTools/dotnet-watch/DotNetWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public async Task WatchAsync(DotNetWatchContext context, CancellationToken cance
currentRunCancellationSource.Token))
using (var fileSetWatcher = new FileSetWatcher(fileSet, _reporter))
{
_reporter.Verbose($"Running {processSpec.ShortDisplayName()} with the following arguments: '{string.Join(" ", processSpec.Arguments)}'");
_reporter.Verbose($"Running {processSpec.ShortDisplayName()} with the following arguments: '{processSpec.GetArgumentsDisplay()}'");
var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token);

_reporter.Output("Started", emoji: "🚀");
Expand Down
22 changes: 20 additions & 2 deletions src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public async Task WatchAsync(DotNetWatchContext context, CancellationToken cance
// when the solution captures state of the file after the changes has already been made.
await hotReload.InitializeAsync(context, cancellationToken);

_reporter.Verbose($"Running {processSpec.ShortDisplayName()} with the following arguments: '{string.Join(" ", processSpec.Arguments ?? Array.Empty<string>())}'");
_reporter.Verbose($"Running {processSpec.ShortDisplayName()} with the following arguments: '{processSpec.GetArgumentsDisplay()}'");
var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token);

_reporter.Output("Started", emoji: "🚀");
Expand Down Expand Up @@ -298,10 +298,28 @@ private void ConfigureExecutable(DotNetWatchContext context, ProcessSpec process
var project = context.FileSet?.Project;
Debug.Assert(project != null);

// RunCommand property specifies the host to use to run the project.
// RunArguments then specifies the arguments to the host.
// Arguments to the executable should follow the host arguments.

processSpec.Executable = project.RunCommand;

if (!string.IsNullOrEmpty(project.RunArguments))
{
processSpec.EscapedArguments = project.RunArguments;
var escapedArguments = project.RunArguments;

if (processSpec.EscapedArguments != null)
{
escapedArguments += " " + processSpec.EscapedArguments;
}

if (processSpec.Arguments != null)
{
escapedArguments += " " + CommandLineUtilities.JoinArguments(processSpec.Arguments);
}

processSpec.EscapedArguments = escapedArguments;
processSpec.Arguments = null;
}

if (!string.IsNullOrEmpty(project.RunWorkingDirectory))
Expand Down
111 changes: 111 additions & 0 deletions src/BuiltInTools/dotnet-watch/Internal/CommandLineUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// Copied from dotnet/runtime/src/libraries/System.Private.CoreLib/src/System/PasteArguments.cs
namespace Microsoft.DotNet.Watcher;

internal static class CommandLineUtilities
{
public static string JoinArguments(IEnumerable<string> arguments)
{
var builder = new StringBuilder();
AppendArguments(builder, arguments);
return builder.ToString();
}

public static void AppendArguments(StringBuilder builder, IEnumerable<string> arguments)
{
foreach (var arg in arguments)
{
AppendArgument(builder, arg);
}
}

private static void AppendArgument(StringBuilder stringBuilder, string argument)
{
if (stringBuilder.Length != 0)
{
stringBuilder.Append(' ');
}

// Parsing rules for non-argv[0] arguments:
// - Backslash is a normal character except followed by a quote.
// - 2N backslashes followed by a quote ==> N literal backslashes followed by unescaped quote
// - 2N+1 backslashes followed by a quote ==> N literal backslashes followed by a literal quote
// - Parsing stops at first whitespace outside of quoted region.
// - (post 2008 rule): A closing quote followed by another quote ==> literal quote, and parsing remains in quoting mode.
if (argument.Length != 0 && ContainsNoWhitespaceOrQuotes(argument))
{
// Simple case - no quoting or changes needed.
stringBuilder.Append(argument);
}
else
{
stringBuilder.Append(Quote);
int idx = 0;
while (idx < argument.Length)
{
char c = argument[idx++];
if (c == Backslash)
{
int numBackSlash = 1;
while (idx < argument.Length && argument[idx] == Backslash)
{
idx++;
numBackSlash++;
}

if (idx == argument.Length)
{
// We'll emit an end quote after this so must double the number of backslashes.
stringBuilder.Append(Backslash, numBackSlash * 2);
}
else if (argument[idx] == Quote)
{
// Backslashes will be followed by a quote. Must double the number of backslashes.
stringBuilder.Append(Backslash, numBackSlash * 2 + 1);
stringBuilder.Append(Quote);
idx++;
}
else
{
// Backslash will not be followed by a quote, so emit as normal characters.
stringBuilder.Append(Backslash, numBackSlash);
}

continue;
}

if (c == Quote)
{
// Escape the quote so it appears as a literal. This also guarantees that we won't end up generating a closing quote followed
// by another quote (which parses differently pre-2008 vs. post-2008.)
stringBuilder.Append(Backslash);
stringBuilder.Append(Quote);
continue;
}

stringBuilder.Append(c);
}

stringBuilder.Append(Quote);
}
}

private static bool ContainsNoWhitespaceOrQuotes(string s)
{
for (int i = 0; i < s.Length; i++)
{
char c = s[i];
if (char.IsWhiteSpace(c) || c == Quote)
{
return false;
}
}

return true;
}

private const char Quote = '\"';
private const char Backslash = '\\';
}
3 changes: 1 addition & 2 deletions src/BuiltInTools/dotnet-watch/Internal/ProcessRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@ public async Task<int> RunAsync(ProcessSpec processSpec, CancellationToken cance
stopwatch.Start();
process.Start();

var args = processSpec.EscapedArguments ?? string.Join(" ", processSpec.Arguments ?? Array.Empty<string>());
_reporter.Verbose($"Started '{processSpec.Executable}' '{args}' with process id {process.Id}", emoji: "🚀");
_reporter.Verbose($"Started '{processSpec.Executable}' with arguments '{processSpec.GetArgumentsDisplay()}': process id {process.Id}", emoji: "🚀");

if (readOutput)
{
Expand Down
3 changes: 3 additions & 0 deletions src/BuiltInTools/dotnet-watch/ProcessSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,8 @@ internal sealed class ProcessSpecEnvironmentVariables : Dictionary<string, strin

public string? ShortDisplayName()
=> Path.GetFileNameWithoutExtension(Executable);

public string GetArgumentsDisplay()
=> EscapedArguments ?? CommandLineUtilities.JoinArguments(Arguments ?? []);
}
}
18 changes: 18 additions & 0 deletions src/Tests/dotnet-watch.Tests/Watch/ProgramTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@ public async Task Arguments(string[] arguments, string expectedApplicationArgs)
Assert.Equal(expectedApplicationArgs, await App.AssertOutputLineStartsWith("Arguments = "));
}

[Theory]
[InlineData(new[] { "--no-hot-reload", "--", "run", "args" }, "Argument Specified in Props,run,args")]
[InlineData(new[] { "--", "run", "args" }, "Argument Specified in Props,run,args")]
// if arguments specified on command line the ones from launch profile are ignored
[InlineData(new[] { "-lp", "P1", "--", "run", "args" },"Argument Specified in Props,run,args")]
// if no arguments specified on command line the ones from launch profile are added
[InlineData(new[] { "-lp", "P1" }, "Argument Specified in Props,Arg1 from launch profile,Arg2 from launch profile")]
public async Task Arguments_HostArguments(string[] arguments, string expectedApplicationArgs)
{
var testAsset = TestAssets.CopyTestAsset("WatchHotReloadAppCustomHost", identifier: string.Join(",", arguments))
.WithSource()
.Path;

App.Start(testAsset, arguments);

Assert.Equal(expectedApplicationArgs, await App.AssertOutputLineStartsWith("Arguments = "));
}

[Fact]
public async Task RunArguments_NoHotReload()
{
Expand Down
Loading