Add better errors, fix help output text, and add 'dotnet watch --list' to help us diagnose issues

Fixes #252 - help output shown twice in dotnet-watch
Fixes #250 - add dotnet-watch --list. Prints a list of all files discovered
Fixes #249 - better error message when GenerateWatchList fails
This commit is contained in:
Nate McMaster 2017-01-13 09:52:32 -08:00
parent 7ee45afed4
commit e481df3d49
14 changed files with 207 additions and 61 deletions

28
.editorconfig Normal file
View File

@ -0,0 +1,28 @@
; EditorConfig to support per-solution formatting.
; Use the EditorConfig VS add-in to make this work.
; http://editorconfig.org/
; This is the default for the codeline.
root = true
[*]
indent_style = space
charset = utf-8
trim_trailing_whitespace = true
[*.{cs}]
indent_size = 4
insert_final_newline = true
; All XML-based file formats
[*.{config,csproj,nuspec,props,resx,targets,xml}]
indent_size = 2
[*.{json}]
indent_size = 2
[*.{ps1}]
indent_size = 4
[*.{sh}]
indent_size = 4

View File

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.25807.0
VisualStudioVersion = 15.0.26020.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{66517987-2A5A-4330-B130-207039378FD4}"
EndProject
@ -8,6 +8,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Watcher.To
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8321E0D1-9A47-4D2F-AED8-3AE636D44E35}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
NuGet.Config = NuGet.Config
EndProjectSection
EndProject
@ -43,10 +44,6 @@ Global
{8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}.Release|Any CPU.Build.0 = Debug|Any CPU
{9295E811-FF0F-E40A-2F9A-0B2C4EBC22AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9295E811-FF0F-E40A-2F9A-0B2C4EBC22AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9295E811-FF0F-E40A-2F9A-0B2C4EBC22AD}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{9295E811-FF0F-E40A-2F9A-0B2C4EBC22AD}.Release|Any CPU.Build.0 = Debug|Any CPU
{7B331122-83B1-4F08-A119-DC846959844C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7B331122-83B1-4F08-A119-DC846959844C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7B331122-83B1-4F08-A119-DC846959844C}.Release|Any CPU.ActiveCfg = Debug|Any CPU
@ -59,6 +56,10 @@ Global
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Release|Any CPU.Build.0 = Debug|Any CPU
{9295E811-FF0F-E40A-2F9A-0B2C4EBC22AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9295E811-FF0F-E40A-2F9A-0B2C4EBC22AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9295E811-FF0F-E40A-2F9A-0B2C4EBC22AD}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{9295E811-FF0F-E40A-2F9A-0B2C4EBC22AD}.Release|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -67,9 +68,9 @@ Global
{8A8CEABC-AC47-43FF-A5DF-69224F7E1F46} = {66517987-2A5A-4330-B130-207039378FD4}
{16BADE2F-1184-4518-8A70-B68A19D0805B} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
{8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E} = {66517987-2A5A-4330-B130-207039378FD4}
{9295E811-FF0F-E40A-2F9A-0B2C4EBC22AD} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
{7B331122-83B1-4F08-A119-DC846959844C} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
{8A2E6961-6B12-4A8E-8215-3E7301D52EAC} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9} = {66517987-2A5A-4330-B130-207039378FD4}
{9295E811-FF0F-E40A-2F9A-0B2C4EBC22AD} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
EndGlobalSection
EndGlobal

View File

@ -16,6 +16,7 @@ namespace Microsoft.DotNet.Watcher
public bool IsQuiet { get; private set; }
public bool IsVerbose { get; private set; }
public IList<string> RemainingArguments { get; private set; }
public bool ListFiles { get; private set; }
public static CommandLineOptions Parse(string[] args, IConsole console)
{
@ -63,6 +64,9 @@ Examples:
CommandOptionType.NoValue);
var optVerbose = app.VerboseOption();
var optList = app.Option("--list", "Lists all discovered files without starting the watcher",
CommandOptionType.NoValue);
app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly);
if (app.Execute(args) != 0)
@ -75,7 +79,9 @@ Examples:
throw new CommandParsingException(app, Resources.Error_QuietAndVerboseSpecified);
}
if (app.RemainingArguments.Count == 0)
if (app.RemainingArguments.Count == 0
&& !app.IsShowingInformation
&& !optList.HasValue())
{
app.ShowHelp();
}
@ -86,8 +92,9 @@ Examples:
IsQuiet = optQuiet.HasValue(),
IsVerbose = optVerbose.HasValue(),
RemainingArguments = app.RemainingArguments,
IsHelp = app.IsShowingInformation
IsHelp = app.IsShowingInformation,
ListFiles = optList.HasValue(),
};
}
}
}
}

View File

@ -34,6 +34,13 @@ namespace Microsoft.DotNet.Watcher
while (true)
{
var fileSet = await fileSetFactory.CreateAsync(cancellationToken);
if (fileSet == null)
{
_reporter.Error("Failed to find a list of files to watch");
return;
}
if (cancellationToken.IsCancellationRequested)
{
return;
@ -91,4 +98,4 @@ namespace Microsoft.DotNet.Watcher
}
}
}
}
}

View File

@ -16,17 +16,19 @@ namespace Microsoft.DotNet.Watcher.Internal
public class MsBuildFileSetFactory : IFileSetFactory
{
private const string TargetName = "GenerateWatchList";
private const string ProjectExtensionFileExtension = ".dotnetwatch.targets";
private const string ProjectExtensionFileExtension = ".dotnetwatch.g.targets";
private const string WatchTargetsFileName = "DotNetWatchCommon.targets";
private readonly IReporter _reporter;
private readonly string _projectFile;
private readonly string _watchTargetsDir;
private readonly OutputSink _outputSink;
private readonly ProcessRunner _processRunner;
private readonly bool _waitOnError;
public MsBuildFileSetFactory(IReporter reporter, string projectFile)
public MsBuildFileSetFactory(IReporter reporter, string projectFile, bool waitOnError)
: this(reporter, projectFile, new OutputSink())
{
_waitOnError = waitOnError;
}
// output sink is for testing
@ -86,7 +88,7 @@ namespace Microsoft.DotNet.Watcher.Internal
var exitCode = await _processRunner.RunAsync(processSpec, cancellationToken);
if (exitCode == 0)
if (exitCode == 0 && File.Exists(watchList))
{
var fileset = new FileSet(
File.ReadAllLines(watchList)
@ -109,28 +111,41 @@ namespace Microsoft.DotNet.Watcher.Internal
_reporter.Error($"Error(s) finding watch items project file '{Path.GetFileName(_projectFile)}'");
_reporter.Output($"MSBuild output from target '{TargetName}'");
_reporter.Output($"MSBuild output from target '{TargetName}':");
_reporter.Output(string.Empty);
foreach (var line in capture.Lines)
{
_reporter.Output($" [MSBUILD] : {line}");
_reporter.Output($" {line}");
}
_reporter.Warn("Fix the error to continue or press Ctrl+C to exit.");
_reporter.Output(string.Empty);
var fileSet = new FileSet(new[] {_projectFile});
using (var watcher = new FileSetWatcher(fileSet))
if (!_waitOnError)
{
await watcher.GetChangedFileAsync(cancellationToken);
return null;
}
else
{
_reporter.Warn("Fix the error to continue or press Ctrl+C to exit.");
_reporter.Output($"File changed: {_projectFile}");
var fileSet = new FileSet(new[] { _projectFile });
using (var watcher = new FileSetWatcher(fileSet))
{
await watcher.GetChangedFileAsync(cancellationToken);
_reporter.Output($"File changed: {_projectFile}");
}
}
}
}
finally
{
File.Delete(watchList);
if (File.Exists(watchList))
{
File.Delete(watchList);
}
}
}
@ -173,4 +188,4 @@ namespace Microsoft.DotNet.Watcher.Internal
return Path.GetDirectoryName(targetPath);
}
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -80,7 +80,20 @@ namespace Microsoft.DotNet.Watcher
try
{
return await MainInternalAsync(reporter, options.Project, options.RemainingArguments, ctrlCTokenSource.Token);
if (options.ListFiles)
{
return await ListFilesAsync(reporter,
options.Project,
ctrlCTokenSource.Token);
}
else
{
return await MainInternalAsync(reporter,
options.Project,
options.RemainingArguments,
ctrlCTokenSource.Token);
}
}
catch (Exception ex)
{
@ -115,8 +128,7 @@ namespace Microsoft.DotNet.Watcher
return 1;
}
var fileSetFactory = new MsBuildFileSetFactory(reporter, projectFile);
var fileSetFactory = new MsBuildFileSetFactory(reporter, projectFile, waitOnError: true);
var processInfo = new ProcessSpec
{
Executable = DotNetMuxer.MuxerPathOrDefault(),
@ -130,6 +142,39 @@ namespace Microsoft.DotNet.Watcher
return 0;
}
private async Task<int> ListFilesAsync(
IReporter reporter,
string project,
CancellationToken cancellationToken)
{
// TODO multiple projects should be easy enough to add here
string projectFile;
try
{
projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, project);
}
catch (FileNotFoundException ex)
{
reporter.Error(ex.Message);
return 1;
}
var fileSetFactory = new MsBuildFileSetFactory(reporter, projectFile, waitOnError: false);
var files = await fileSetFactory.CreateAsync(cancellationToken);
if (files == null)
{
return 1;
}
foreach (var file in files)
{
_console.Out.WriteLine(file);
}
return 0;
}
private static IReporter CreateReporter(bool verbose, bool quiet, IConsole console)
{
const string prefix = "watch : ";
@ -178,4 +223,4 @@ namespace Microsoft.DotNet.Watcher
.Build();
}
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -98,9 +98,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
// running tool
using (var resource = GetType().GetTypeInfo().Assembly.GetManifestResourceStream("ProjectIdResolverTargets.xml"))
using (var stream = new FileStream(projectExtensionsPath, FileMode.Create))
using (var writer = new StreamWriter(stream))
{
writer.WriteLine("<!-- Auto-generated by dotnet-user-secrets. This file can be deleted and should not be commited to source control. -->");
resource.CopyTo(stream);
}
}
@ -120,4 +118,4 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
}
}
}
}
}

View File

@ -22,7 +22,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[Fact]
public async Task ChangeFileInDependency()
{
await _app.StartWatcher().OrTimeout();
await _app.StartWatcherAsync().OrTimeout();
var fileToChange = Path.Combine(_app.DependencyFolder, "Foo.cs");
var programCs = File.ReadAllText(fileToChange);

View File

@ -2,6 +2,7 @@
// 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.Diagnostics;
using System.IO;
using System.Text;
@ -9,8 +10,8 @@ using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using Microsoft.Extensions.Internal;
using Xunit.Abstractions;
using Microsoft.Extensions.Tools.Internal;
using Xunit.Abstractions;
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
{
@ -71,6 +72,21 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
return null;
}
public async Task<IList<string>> GetAllOutputLines()
{
var lines = new List<string>();
while (!_source.Completion.IsCompleted)
{
while (await _source.OutputAvailableAsync())
{
var next = await _source.ReceiveAsync();
_logger.WriteLine($"{DateTime.Now}: recv: '{next}'");
lines.Add(next);
}
}
return lines;
}
private void StartProcessingOutput(StreamReader streamReader)
{
_source = _source ?? new BufferBlock<string>();
@ -80,7 +96,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
string line;
while ((line = streamReader.ReadLine()) != null)
{
_logger.WriteLine($"{DateTime.Now} post: {line}");
_logger.WriteLine($"{DateTime.Now}: post: '{line}'");
_source.Post(line);
}

View File

@ -5,6 +5,7 @@ using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.DotNet.Watcher.Tools.Tests;
using Xunit;
using Xunit.Abstractions;
@ -24,7 +25,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[InlineData(false)]
public async Task ChangeCompiledFile(bool usePollingWatcher)
{
await _app.StartWatcher().OrTimeout();
await _app.StartWatcherAsync().OrTimeout();
var types = await _app.GetCompiledAppDefinedTypes().OrTimeout();
Assert.Equal(2, types);
@ -41,7 +42,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[Fact(Skip = "Broken. See https://github.com/aspnet/DotNetTools/issues/212")]
public async Task AddCompiledFile()
{
await _app.StartWatcher().OrTimeout();
await _app.StartWatcherAsync().OrTimeout();
var types = await _app.GetCompiledAppDefinedTypes().OrTimeout();
Assert.Equal(2, types);
@ -57,7 +58,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[Fact]
public async Task DeleteCompiledFile()
{
await _app.StartWatcher().OrTimeout();
await _app.StartWatcherAsync().OrTimeout();
var types = await _app.GetCompiledAppDefinedTypes().OrTimeout();
Assert.Equal(2, types);
@ -73,7 +74,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[Fact]
public async Task DeleteSourceFolder()
{
await _app.StartWatcher().OrTimeout();
await _app.StartWatcherAsync().OrTimeout();
var types = await _app.GetCompiledAppDefinedTypes().OrTimeout();
Assert.Equal(2, types);
@ -89,7 +90,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[Fact]
public async Task RenameCompiledFile()
{
await _app.StartWatcher().OrTimeout();
await _app.StartWatcherAsync().OrTimeout();
var oldFile = Path.Combine(_app.SourceDirectory, "include", "Foo.cs");
var newFile = Path.Combine(_app.SourceDirectory, "include", "Foo_new.cs");
@ -101,7 +102,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[Fact]
public async Task ChangeExcludedFile()
{
await _app.StartWatcher().OrTimeout();
await _app.StartWatcherAsync().OrTimeout();
var changedFile = Path.Combine(_app.SourceDirectory, "exclude", "Baz.cs");
File.WriteAllText(changedFile, "");
@ -111,6 +112,23 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
Assert.NotSame(restart, finished);
}
[Fact]
public async Task ListsFiles()
{
_app.Start(new [] { "--list" });
var lines = await _app.Process.GetAllOutputLines();
AssertEx.EqualFileList(
_app.Scenario.WorkFolder,
new[]
{
"GlobbingApp/Program.cs",
"GlobbingApp/include/Foo.cs",
"GlobbingApp/GlobbingApp.csproj",
},
lines);
}
public void Dispose()
{
_app.Dispose();

View File

@ -23,7 +23,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[Fact]
public async Task RestartProcessOnFileChange()
{
await _app.StartWatcher(new[] { "--no-exit" }).OrTimeout();
await _app.StartWatcherAsync(new[] { "--no-exit" }).OrTimeout();
var pid = await _app.GetProcessId().OrTimeout();
// Then wait for it to restart when we change a file
@ -42,7 +42,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[Fact]
public async Task RestartProcessThatTerminatesAfterFileChange()
{
await _app.StartWatcher().OrTimeout();
await _app.StartWatcherAsync().OrTimeout();
var pid = await _app.GetProcessId().OrTimeout();
await _app.HasExited().OrTimeout(); // process should exit after run

View File

@ -118,17 +118,17 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
// this launches a new .NET Core process using the runtime of the current test app
// and the version of dotnet-watch that this test app is compiled against
var thisAssembly = Path.GetFileNameWithoutExtension(GetType().GetTypeInfo().Assembly.Location);
var args = new List<string>();
args.Add("exec");
args.Add("--depsfile");
args.Add(Path.Combine(AppContext.BaseDirectory, thisAssembly + ".deps.json"));
var args = new List<string>
{
"exec",
"--depsfile",
Path.Combine(AppContext.BaseDirectory, thisAssembly + ".deps.json"),
"--runtimeconfig",
Path.Combine(AppContext.BaseDirectory, thisAssembly + ".runtimeconfig.json")
};
args.Add("--runtimeconfig");
args.Add(Path.Combine(AppContext.BaseDirectory, thisAssembly + ".runtimeconfig.json"));
var currentFxVersion = AppContext.GetData("FX_DEPS_FILE") as string;
if (currentFxVersion != null)
if (AppContext.GetData("FX_DEPS_FILE") is string currentFxVersion)
{
// This overrides the version of shared fx in the runtimeconfig.json file.
// Tests do this to ensure dotnet-watch is executing on the version of Microsoft.NETCore.App

View File

@ -1,12 +1,13 @@
// 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 Microsoft.Extensions.Tools.Internal;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Tools.Internal;
using Xunit.Abstractions;
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
@ -16,12 +17,11 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
private const string StartedMessage = "Started";
private const string ExitingMessage = "Exiting";
protected ProjectToolScenario Scenario { get; }
private readonly ITestOutputHelper _logger;
protected AwaitableProcess Process { get; set; }
private string _appName;
private bool _prepared;
public WatchableApp(string appName, ITestOutputHelper logger)
{
_logger = logger;
@ -31,6 +31,10 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
SourceDirectory = Path.Combine(Scenario.WorkFolder, appName);
}
public ProjectToolScenario Scenario { get; }
public AwaitableProcess Process { get; protected set; }
public string SourceDirectory { get; }
public Task HasRestarted()
@ -41,9 +45,6 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
public bool UsePollingWatcher { get; set; }
public Task StartWatcher([CallerMemberName] string name = null)
=> StartWatcher(Array.Empty<string>(), name);
public async Task<int> GetProcessId()
{
var line = await Process.GetOutputLineAsync(l => l.StartsWith("PID ="));
@ -58,7 +59,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
_prepared = true;
}
public async Task StartWatcher(string[] arguments, [CallerMemberName] string name = null)
public void Start(IEnumerable<string> arguments, [CallerMemberName] string name = null)
{
if (!_prepared)
{
@ -67,7 +68,6 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
var args = Scenario
.GetDotnetWatchArguments()
.Concat(new[] { "run", "--" })
.Concat(arguments);
var spec = new ProcessSpec
@ -79,6 +79,15 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
Process = new AwaitableProcess(spec, _logger);
Process.Start();
}
public Task StartWatcherAsync([CallerMemberName] string name = null)
=> StartWatcherAsync(Array.Empty<string>(), name);
public async Task StartWatcherAsync(string[] arguments, [CallerMemberName] string name = null)
{
var args = new[] { "run", "--" }.Concat(arguments);
Start(args, name);
await Process.GetOutputLineAsync(StartedMessage);
}

View File

@ -297,7 +297,8 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
}
private Task<IFileSet> GetFileSet(TemporaryCSharpProject target)
=> GetFileSet(new MsBuildFileSetFactory(_reporter, target.Path));
=> GetFileSet(new MsBuildFileSetFactory(_reporter, target.Path, waitOnError: false));
private async Task<IFileSet> GetFileSet(MsBuildFileSetFactory filesetFactory)
{
_tempDir.Create();
@ -305,6 +306,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
var finished = await Task.WhenAny(createTask, Task.Delay(TimeSpan.FromSeconds(10)));
Assert.Same(createTask, finished);
Assert.NotNull(createTask.Result);
return createTask.Result;
}