diff --git a/.gitignore b/.gitignore index 79f4ca4258..33889157be 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ project.lock.json .build/ /.vs/ testWorkDir/ - +*.nuget.props +*.nuget.targets \ No newline at end of file diff --git a/DotNetTools.sln b/DotNetTools.sln index 3dbe6b4f4e..8852cbde1a 100644 --- a/DotNetTools.sln +++ b/DotNetTools.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 VisualStudioVersion = 14.0.25420.1 @@ -33,6 +33,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.Secret EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.SecretManager.Tools.Tests", "test\Microsoft.Extensions.SecretManager.Tools.Tests\Microsoft.Extensions.SecretManager.Tools.Tests.xproj", "{7B331122-83B1-4F08-A119-DC846959844C}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.DotNet.Watcher.Tools.Tests", "test\Microsoft.DotNet.Watcher.Tools.Tests\Microsoft.DotNet.Watcher.Tools.Tests.xproj", "{8A2E6961-6B12-4A8E-8215-3E7301D52EAC}" +EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.Caching.SqlConfig.Tools", "src\Microsoft.Extensions.Caching.SqlConfig.Tools\Microsoft.Extensions.Caching.SqlConfig.Tools.xproj", "{53F3B53D-303A-4DAA-9C38-4F55195FA5B9}" EndProject Global @@ -81,6 +83,10 @@ Global {53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Release|Any CPU.Build.0 = Release|Any CPU + {8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -97,5 +103,6 @@ Global {8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E} = {66517987-2A5A-4330-B130-207039378FD4} {7B331122-83B1-4F08-A119-DC846959844C} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134} {53F3B53D-303A-4DAA-9C38-4F55195FA5B9} = {66517987-2A5A-4330-B130-207039378FD4} + {8A2E6961-6B12-4A8E-8215-3E7301D52EAC} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134} EndGlobalSection EndGlobal diff --git a/src/Microsoft.DotNet.Watcher.Core/DotNetWatcher.cs b/src/Microsoft.DotNet.Watcher.Core/DotNetWatcher.cs index 92577d59fe..2fe63625d2 100644 --- a/src/Microsoft.DotNet.Watcher.Core/DotNetWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Core/DotNetWatcher.cs @@ -2,9 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Watcher.Core.Internal; using Microsoft.Extensions.Logging; @@ -33,7 +35,7 @@ namespace Microsoft.DotNet.Watcher.Core _logger = _loggerFactory.CreateLogger(nameof(DotNetWatcher)); } - public async Task WatchAsync(string projectFile, string[] dotnetArguments, CancellationToken cancellationToken) + public async Task WatchAsync(string projectFile, IEnumerable dotnetArguments, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(projectFile)) { @@ -48,24 +50,7 @@ namespace Microsoft.DotNet.Watcher.Core throw new ArgumentNullException(nameof(cancellationToken)); } - // If any argument has spaces then quote it because we're going to convert everything - // to string - for (var i = 0; i < dotnetArguments.Length; i++) - { - var arg = dotnetArguments[i]; - foreach (char c in arg) - { - if (c == ' ' || - c == '\t') - { - arg = $"\"{arg}\""; - break; - } - } - dotnetArguments[i] = arg; - } - - var dotnetArgumentsAsString = string.Join(" ", dotnetArguments); + var dotnetArgumentsAsString = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(dotnetArguments); var workingDir = Path.GetDirectoryName(projectFile); diff --git a/src/Microsoft.DotNet.Watcher.Core/project.json b/src/Microsoft.DotNet.Watcher.Core/project.json index 53730690fc..2f3f041420 100644 --- a/src/Microsoft.DotNet.Watcher.Core/project.json +++ b/src/Microsoft.DotNet.Watcher.Core/project.json @@ -17,6 +17,7 @@ }, "dependencies": { "Microsoft.DotNet.ProjectModel": "1.0.0-*", + "Microsoft.DotNet.Cli.Utils": "1.0.0-*", "Microsoft.Extensions.FileProviders.Abstractions": "1.1.0-*", "Microsoft.Extensions.FileProviders.Physical": "1.1.0-*", "Microsoft.Extensions.Logging.Abstractions": "1.1.0-*", diff --git a/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs b/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs new file mode 100644 index 0000000000..03ab69567c --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs @@ -0,0 +1,54 @@ +// 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.Collections.Generic; +using System.IO; +using Microsoft.Extensions.CommandLineUtils; + +namespace Microsoft.DotNet.Watcher.Tools +{ + internal class CommandLineOptions + { + public bool IsHelp { get; private set; } + public IList RemainingArguments { get; private set; } + public static CommandLineOptions Parse(string[] args, TextWriter consoleOutput) + { + if (args == null) + { + throw new ArgumentNullException(nameof(args)); + } + + var app = new CommandLineApplication(throwOnUnexpectedArg: false) + { + Name = "dotnet watch", + FullName = "Microsoft DotNet File Watcher", + Out = consoleOutput, + AllowArgumentSeparator = true + }; + + app.HelpOption("-?|-h|--help"); + + app.OnExecute(() => + { + if (app.RemainingArguments.Count == 0) + { + app.ShowHelp(); + } + + return 0; + }); + + if (app.Execute(args) != 0) + { + return null; + } + + return new CommandLineOptions + { + RemainingArguments = app.RemainingArguments, + IsHelp = app.IsShowingInformation + }; + } + } +} diff --git a/src/Microsoft.DotNet.Watcher.Tools/Program.cs b/src/Microsoft.DotNet.Watcher.Tools/Program.cs index 5b0cde0b71..d3f0631134 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Program.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Program.cs @@ -6,7 +6,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.DotNet.Watcher.Core; -using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watcher.Tools @@ -14,9 +13,24 @@ namespace Microsoft.DotNet.Watcher.Tools public class Program { private readonly ILoggerFactory _loggerFactory; + private readonly CancellationToken _cancellationToken; + private readonly TextWriter _out; - public Program() + public Program(TextWriter consoleOutput, CancellationToken cancellationToken) { + if (consoleOutput == null) + { + throw new ArgumentNullException(nameof(consoleOutput)); + } + + if (cancellationToken == null) + { + throw new ArgumentNullException(nameof(cancellationToken)); + } + + _cancellationToken = cancellationToken; + _out = consoleOutput; + _loggerFactory = new LoggerFactory(); var logVar = Environment.GetEnvironmentVariable("DOTNET_WATCH_LOG_LEVEL"); @@ -44,49 +58,44 @@ namespace Microsoft.DotNet.Watcher.Tools ev.Cancel = false; }; - return new Program().MainInternal(args, ctrlCTokenSource.Token); + int exitCode; + try + { + exitCode = new Program(Console.Out, ctrlCTokenSource.Token) + .MainInternalAsync(args) + .GetAwaiter() + .GetResult(); + } + catch (TaskCanceledException) + { + // swallow when only exception is the CTRL+C exit cancellation task + exitCode = 0; + } + return exitCode; } } - private int MainInternal(string[] args, CancellationToken cancellationToken) + private async Task MainInternalAsync(string[] args) { - var app = new CommandLineApplication(); - app.Name = "dotnet-watch"; - app.FullName = "Microsoft dotnet File Watcher"; - - app.HelpOption("-?|-h|--help"); - - app.OnExecute(() => + var options = CommandLineOptions.Parse(args, _out); + if (options == null) { - var projectToWatch = Path.Combine(Directory.GetCurrentDirectory(), ProjectModel.Project.FileName); - var watcher = DotNetWatcher.CreateDefault(_loggerFactory); - - try - { - watcher.WatchAsync(projectToWatch, args, cancellationToken).Wait(); - } - catch (AggregateException ex) - { - if (ex.InnerExceptions.Count != 1 || !(ex.InnerException is TaskCanceledException)) - { - throw; - } - } - - return 0; - }); - - if (args == null || - args.Length == 0 || - args[0].Equals("--help", StringComparison.OrdinalIgnoreCase) || - args[0].Equals("-h", StringComparison.OrdinalIgnoreCase) || - args[0].Equals("-?", StringComparison.OrdinalIgnoreCase)) - { - app.ShowHelp(); + // invalid args syntax return 1; } - return app.Execute(); + if (options.IsHelp) + { + return 2; + } + + var projectToWatch = Path.Combine(Directory.GetCurrentDirectory(), ProjectModel.Project.FileName); + + await DotNetWatcher + .CreateDefault(_loggerFactory) + .WatchAsync(projectToWatch, options.RemainingArguments, _cancellationToken); + + return 0; } } } diff --git a/src/Microsoft.DotNet.Watcher.Tools/Properties/AssemblyInfo.cs b/src/Microsoft.DotNet.Watcher.Tools/Properties/AssemblyInfo.cs index 35b4b58d45..df3730569e 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Properties/AssemblyInfo.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Properties/AssemblyInfo.cs @@ -3,12 +3,9 @@ using System.Reflection; using System.Resources; -using System.Runtime.CompilerServices; [assembly: AssemblyMetadata("Serviceable", "True")] [assembly: NeutralResourcesLanguage("en-US")] [assembly: AssemblyCompany("Microsoft Corporation.")] [assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] [assembly: AssemblyProduct("Microsoft .NET")] - -[assembly: InternalsVisibleTo("Microsoft.DotNet.Watcher.Tools.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/src/Microsoft.DotNet.Watcher.Tools/Properties/InternalsVisibleTo.cs b/src/Microsoft.DotNet.Watcher.Tools/Properties/InternalsVisibleTo.cs new file mode 100644 index 0000000000..ea9ec15282 --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Tools/Properties/InternalsVisibleTo.cs @@ -0,0 +1,6 @@ +// 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.DotNet.Watcher.Tools.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/src/Microsoft.DotNet.Watcher.Tools/README.md b/src/Microsoft.DotNet.Watcher.Tools/README.md index b244177440..8c47b9c58c 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/README.md +++ b/src/Microsoft.DotNet.Watcher.Tools/README.md @@ -18,7 +18,9 @@ Add `Microsoft.DotNet.Watcher.Tools` to the `tools` section of your `project.jso ### How To Use - dotnet watch [dotnet arguments] + dotnet watch [-?|-h|--help] + + dotnet watch [[--] ...] Add `watch` after `dotnet` in the command that you want to run: diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/CommandLineOptionsTests.cs b/test/Microsoft.DotNet.Watcher.Tools.Tests/CommandLineOptionsTests.cs new file mode 100644 index 0000000000..306cddaacd --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/CommandLineOptionsTests.cs @@ -0,0 +1,46 @@ +// 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.IO; +using System.Linq; +using System.Text; +using Xunit; + +namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests +{ + public class CommandLineOptionsTests + { + [Theory] + [InlineData(new object[] { new[] { "-h" } })] + [InlineData(new object[] { new[] { "-?" } })] + [InlineData(new object[] { new[] { "--help" } })] + [InlineData(new object[] { new[] { "--help", "--bogus" } })] + [InlineData(new object[] { new[] { "--" } })] + [InlineData(new object[] { new string[0] })] + public void HelpArgs(string[] args) + { + var stdout = new StringBuilder(); + + var options = CommandLineOptions.Parse(args, new StringWriter(stdout)); + + Assert.True(options.IsHelp); + Assert.Contains("Usage: dotnet watch ", stdout.ToString()); + } + + [Theory] + [InlineData(new[] { "run" }, new[] { "run" })] + [InlineData(new[] { "run", "--", "subarg" }, new[] { "run", "--", "subarg" })] + [InlineData(new[] { "--", "run", "--", "subarg" }, new[] { "run", "--", "subarg" })] + [InlineData(new[] { "--unrecognized-arg" }, new[] { "--unrecognized-arg" })] + public void ParsesRemainingArgs(string[] args, string[] expected) + { + var stdout = new StringBuilder(); + + var options = CommandLineOptions.Parse(args, new StringWriter(stdout)); + + Assert.Equal(expected, options.RemainingArguments.ToArray()); + Assert.False(options.IsHelp); + Assert.Empty(stdout.ToString()); + } + } +} diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/Microsoft.DotNet.Watcher.Tools.Tests.xproj b/test/Microsoft.DotNet.Watcher.Tools.Tests/Microsoft.DotNet.Watcher.Tools.Tests.xproj new file mode 100644 index 0000000000..3a1a6e45b7 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/Microsoft.DotNet.Watcher.Tools.Tests.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 8a2e6961-6b12-4a8e-8215-3e7301d52eac + Microsoft.DotNet.Watcher.Tools.Tests + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/project.json b/test/Microsoft.DotNet.Watcher.Tools.Tests/project.json new file mode 100644 index 0000000000..bcdb3e2ae6 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/project.json @@ -0,0 +1,22 @@ +{ + "buildOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk" + }, + "dependencies": { + "Microsoft.DotNet.Watcher.Tools": "1.0.0-*", + "dotnet-test-xunit": "2.2.0-*", + "xunit": "2.2.0-*" + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.0.0", + "type": "platform" + } + } + } + }, + "testRunner": "xunit" +}