Browse Source

Merge aspnet/DotNetTools release/2.2

Nate McMaster 7 years ago
parent
commit
0782a9dfa9
28 changed files with 246 additions and 177 deletions
  1. 3 64
      eng/NuGetPackageVerifier.json
  2. 1 1
      src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj
  3. 1 1
      src/Tools/dotnet-dev-certs/src/dotnet-dev-certs.csproj
  4. 1 1
      src/Tools/dotnet-sql-cache/src/dotnet-sql-cache.csproj
  5. 1 1
      src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj
  6. 1 1
      src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs
  7. 1 1
      src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj
  8. 4 0
      src/Tools/dotnet-watch/src/CommandLineOptions.cs
  9. 12 4
      src/Tools/dotnet-watch/src/DotNetWatcher.cs
  10. 33 10
      src/Tools/dotnet-watch/src/Internal/FileWatcher/DotnetFileWatcher.cs
  11. 35 15
      src/Tools/dotnet-watch/src/Internal/ProcessRunner.cs
  12. 1 1
      src/Tools/dotnet-watch/src/Program.cs
  13. 1 1
      src/Tools/dotnet-watch/src/dotnet-watch.csproj
  14. 36 12
      src/Tools/dotnet-watch/test/AwaitableProcess.cs
  15. 35 0
      src/Tools/dotnet-watch/test/DotNetWatcherTests.cs
  16. 31 29
      src/Tools/dotnet-watch/test/FileWatcherTests.cs
  17. 6 2
      src/Tools/dotnet-watch/test/GlobbingAppTests.cs
  18. 16 6
      src/Tools/dotnet-watch/test/NoDepsAppTests.cs
  19. 12 10
      src/Tools/dotnet-watch/test/ProgramTests.cs
  20. 1 3
      src/Tools/dotnet-watch/test/Scenario/ProjectToolScenario.cs
  21. 1 1
      src/Tools/dotnet-watch/test/Scenario/WatchableApp.cs
  22. 1 1
      src/Tools/dotnet-watch/test/TestProjects/AppWithDeps/AppWithDeps.csproj
  23. 2 2
      src/Tools/dotnet-watch/test/TestProjects/GlobbingApp/GlobbingApp.csproj
  24. 1 1
      src/Tools/dotnet-watch/test/TestProjects/KitchenSink/KitchenSink.csproj
  25. 1 0
      src/Tools/dotnet-watch/test/TestProjects/KitchenSink/Program.cs
  26. 2 2
      src/Tools/dotnet-watch/test/TestProjects/NoDepsApp/NoDepsApp.csproj
  27. 2 2
      src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj
  28. 4 5
      src/Tools/shared/src/CliContext.cs

+ 3 - 64
eng/NuGetPackageVerifier.json

@@ -12,73 +12,12 @@
       "dotnet-sql-cache": {
         "packageTypes": [
           "DotnetTool"
-        ],
-        "Exclusions": {
-          "NEUTRAL_RESOURCES_LANGUAGE": {
-            "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
-          },
-          "WRONG_PUBLICKEYTOKEN": {
-            "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.0/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/runtimes/unix/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg."
-          },
-          "ASSEMBLY_INFORMATIONAL_VERSION_MISMATCH": {
-            "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.0/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/runtimes/unix/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg."
-          },
-          "ASSEMBLY_FILE_VERSION_MISMATCH": {
-            "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.0/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/runtimes/unix/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg."
-          },
-          "ASSEMBLY_VERSION_MISMATCH": {
-            "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.0/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/runtimes/unix/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg."
-          }
-        }
+        ]
       },
       "dotnet-user-secrets": {
         "packageTypes": [
           "DotnetTool"
-        ],
-        "Exclusions": {
-          "NEUTRAL_RESOURCES_LANGUAGE": {
-            "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
-          },
-          "SERVICING_ATTRIBUTE": {
-            "tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Assembly is built by another project but bundled in our nupkg."
-          },
-          "WRONG_PUBLICKEYTOKEN": {
-            "tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
-          },
-          "ASSEMBLY_INFORMATIONAL_VERSION_MISMATCH": {
-            "tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
-          },
-          "ASSEMBLY_FILE_VERSION_MISMATCH": {
-            "tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
-          },
-          "ASSEMBLY_VERSION_MISMATCH": {
-            "tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Assembly is built by another project but bundled in our nupkg.",
-            "tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
-          }
-        }
+        ]
       },
       "dotnet-dev-certs": {
         "packageTypes": [
@@ -88,7 +27,7 @@
       "Microsoft.AspNetCore.DeveloperCertificates.XPlat": {
         "Exclusions": {
           "DOC_MISSING": {
-            "lib/netcoreapp2.1/Microsoft.AspNetCore.DeveloperCertificates.XPlat.dll": "Docs not required to shipoob package"
+            "lib/netcoreapp2.2/Microsoft.AspNetCore.DeveloperCertificates.XPlat.dll": "Docs not required to shipoob package"
           }
         }
       }

+ 1 - 1
src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <TargetFramework>netcoreapp2.2</TargetFramework>
     <Description>Package for the CLI first run experience.</Description>
     <DefineConstants>$(DefineConstants);XPLAT</DefineConstants>
     <PackageTags>aspnet;cli</PackageTags>

+ 1 - 1
src/Tools/dotnet-dev-certs/src/dotnet-dev-certs.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <TargetFramework>netcoreapp2.2</TargetFramework>
     <OutputType>exe</OutputType>
     <Description>Command line tool to generate certificates used in ASP.NET Core during development.</Description>
     <RootNamespace>Microsoft.AspNetCore.DeveloperCertificates.Tools</RootNamespace>

+ 1 - 1
src/Tools/dotnet-sql-cache/src/dotnet-sql-cache.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <TargetFramework>netcoreapp2.2</TargetFramework>
     <OutputType>exe</OutputType>
     <Description>Command line tool to create tables and indexes in a Microsoft SQL Server database for distributed caching.</Description>
     <PackageTags>cache;distributedcache;sqlserver</PackageTags>

+ 1 - 1
src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <TargetFramework>netcoreapp2.2</TargetFramework>
     <OutputType>exe</OutputType>
     <Description>Command line tool to manage user secrets for Microsoft.Extensions.Configuration.</Description>
     <GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute>

+ 1 - 1
src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs

@@ -35,7 +35,7 @@ namespace Microsoft.Extensions.Configuration.UserSecrets.Tests
         private const string ProjectTemplate = @"<Project ToolsVersion=""15.0"" Sdk=""Microsoft.NET.Sdk"">
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFrameworks>netcoreapp2.1</TargetFrameworks>
+    <TargetFrameworks>netcoreapp2.2</TargetFrameworks>
     {0}
     <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
   </PropertyGroup>

+ 1 - 1
src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <TargetFramework>netcoreapp2.2</TargetFramework>
     <AssemblyName>Microsoft.Extensions.SecretManager.Tools.Tests</AssemblyName>
   </PropertyGroup>
 

+ 4 - 0
src/Tools/dotnet-watch/src/CommandLineOptions.cs

@@ -53,6 +53,10 @@ Environment variables:
   DOTNET_WATCH
   dotnet-watch sets this variable to '1' on all child processes launched.
 
+  DOTNET_WATCH_ITERATION
+  dotnet-watch sets this variable to '1' and increments by one each time
+  a file is changed and the command is restarted.
+
 Remarks:
   The special option '--' is used to delimit the end of the options and
   the beginning of arguments that will be passed to the child dotnet process.

+ 12 - 4
src/Tools/dotnet-watch/src/DotNetWatcher.cs

@@ -1,7 +1,8 @@
-// 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;
+using System.Globalization;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.DotNet.Watcher.Internal;
@@ -32,8 +33,13 @@ namespace Microsoft.DotNet.Watcher
             cancellationToken.Register(state => ((TaskCompletionSource<object>) state).TrySetResult(null),
                 cancelledTaskSource);
 
+            var iteration = 1;
+
             while (true)
             {
+                processSpec.EnvironmentVariables["DOTNET_WATCH_ITERATION"] = iteration.ToString(CultureInfo.InvariantCulture);
+                iteration++;
+
                 var fileSet = await fileSetFactory.CreateAsync(cancellationToken);
 
                 if (fileSet == null)
@@ -69,13 +75,15 @@ namespace Microsoft.DotNet.Watcher
 
                     await Task.WhenAll(processTask, fileSetTask);
 
-                    if (processTask.Result == 0)
+                    if (processTask.Result != 0 && finishedTask == processTask && !cancellationToken.IsCancellationRequested)
                     {
-                        _reporter.Output("Exited");
+                        // Only show this error message if the process exited non-zero due to a normal process exit.
+                        // Don't show this if dotnet-watch killed the inner process due to file change or CTRL+C by the user
+                        _reporter.Error($"Exited with error code {processTask.Result}");
                     }
                     else
                     {
-                        _reporter.Error($"Exited with error code {processTask.Result}");
+                        _reporter.Output("Exited");
                     }
 
                     if (finishedTask == cancelledTaskSource.Task || cancellationToken.IsCancellationRequested)

+ 33 - 10
src/Tools/dotnet-watch/src/Internal/FileWatcher/DotnetFileWatcher.cs

@@ -10,6 +10,8 @@ namespace Microsoft.DotNet.Watcher.Internal
 {
     internal class DotnetFileWatcher : IFileSystemWatcher
     {
+        private volatile bool _disposed;
+
         private readonly Func<string, FileSystemWatcher> _watcherFactory;
 
         private FileSystemWatcher _fileSystemWatcher;
@@ -46,6 +48,11 @@ namespace Microsoft.DotNet.Watcher.Internal
 
         private void WatcherErrorHandler(object sender, ErrorEventArgs e)
         {
+            if (_disposed)
+            {
+                return;
+            }
+
             var exception = e.GetException();
 
             // Win32Exception may be triggered when setting EnableRaisingEvents on a file system type
@@ -62,6 +69,11 @@ namespace Microsoft.DotNet.Watcher.Internal
 
         private void WatcherRenameHandler(object sender, RenamedEventArgs e)
         {
+            if (_disposed)
+            {
+                return;
+            }
+
             NotifyChange(e.OldFullPath);
             NotifyChange(e.FullPath);
 
@@ -79,6 +91,11 @@ namespace Microsoft.DotNet.Watcher.Internal
 
         private void WatcherChangeHandler(object sender, FileSystemEventArgs e)
         {
+            if (_disposed)
+            {
+                return;
+            }
+
             NotifyChange(e.FullPath);
         }
 
@@ -98,15 +115,7 @@ namespace Microsoft.DotNet.Watcher.Internal
                 {
                     enableEvents = _fileSystemWatcher.EnableRaisingEvents;
 
-                    _fileSystemWatcher.EnableRaisingEvents = false;
-
-                    _fileSystemWatcher.Created -= WatcherChangeHandler;
-                    _fileSystemWatcher.Deleted -= WatcherChangeHandler;
-                    _fileSystemWatcher.Changed -= WatcherChangeHandler;
-                    _fileSystemWatcher.Renamed -= WatcherRenameHandler;
-                    _fileSystemWatcher.Error -= WatcherErrorHandler;
-
-                    _fileSystemWatcher.Dispose();
+                    DisposeInnerWatcher();
                 }
 
                 _fileSystemWatcher = _watcherFactory(BasePath);
@@ -122,6 +131,19 @@ namespace Microsoft.DotNet.Watcher.Internal
             }
         }
 
+        private void DisposeInnerWatcher()
+        {
+            _fileSystemWatcher.EnableRaisingEvents = false;
+
+            _fileSystemWatcher.Created -= WatcherChangeHandler;
+            _fileSystemWatcher.Deleted -= WatcherChangeHandler;
+            _fileSystemWatcher.Changed -= WatcherChangeHandler;
+            _fileSystemWatcher.Renamed -= WatcherRenameHandler;
+            _fileSystemWatcher.Error -= WatcherErrorHandler;
+
+            _fileSystemWatcher.Dispose();
+        }
+
         public bool EnableRaisingEvents
         {
             get => _fileSystemWatcher.EnableRaisingEvents;
@@ -130,7 +152,8 @@ namespace Microsoft.DotNet.Watcher.Internal
 
         public void Dispose()
         {
-            _fileSystemWatcher.Dispose();
+            _disposed = true;
+            DisposeInnerWatcher();
         }
     }
 }

+ 35 - 15
src/Tools/dotnet-watch/src/Internal/ProcessRunner.cs

@@ -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;
@@ -33,7 +33,7 @@ namespace Microsoft.DotNet.Watcher.Internal
             var stopwatch = new Stopwatch();
 
             using (var process = CreateProcess(processSpec))
-            using (var processState = new ProcessState(process))
+            using (var processState = new ProcessState(process, _reporter))
             {
                 cancellationToken.Register(() => processState.TryKill());
 
@@ -97,27 +97,36 @@ namespace Microsoft.DotNet.Watcher.Internal
 
         private class ProcessState : IDisposable
         {
+            private readonly IReporter _reporter;
             private readonly Process _process;
             private readonly TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>();
             private volatile bool _disposed;
 
-            public ProcessState(Process process)
+            public ProcessState(Process process, IReporter reporter)
             {
+                _reporter = reporter;
                 _process = process;
                 _process.Exited += OnExited;
                 Task = _tcs.Task.ContinueWith(_ =>
                 {
-                    // We need to use two WaitForExit calls to ensure that all of the output/events are processed. Previously
-                    // this code used Process.Exited, which could result in us missing some output due to the ordering of
-                    // events.
-                    //
-                    // See the remarks here: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit#System_Diagnostics_Process_WaitForExit_System_Int32_
-                    if (!process.WaitForExit(Int32.MaxValue))
+                    try
                     {
-                        throw new TimeoutException();
+                        // We need to use two WaitForExit calls to ensure that all of the output/events are processed. Previously
+                        // this code used Process.Exited, which could result in us missing some output due to the ordering of
+                        // events.
+                        //
+                        // See the remarks here: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit#System_Diagnostics_Process_WaitForExit_System_Int32_
+                        if (!_process.WaitForExit(Int32.MaxValue))
+                        {
+                            throw new TimeoutException();
+                        }
+
+                        _process.WaitForExit();
+                    }
+                    catch (InvalidOperationException)
+                    {
+                        // suppress if this throws if no process is associated with this object anymore.
                     }
-
-                    process.WaitForExit();
                 });
             }
 
@@ -125,15 +134,26 @@ namespace Microsoft.DotNet.Watcher.Internal
 
             public void TryKill()
             {
+                if (_disposed)
+                {
+                    return;
+                }
+
                 try
                 {
                     if (!_process.HasExited)
                     {
+                        _reporter.Verbose($"Killing process {_process.Id}");
                         _process.KillTree();
                     }
                 }
-                catch
-                { }
+                catch (Exception ex)
+                {
+                    _reporter.Verbose($"Error while killing process '{_process.StartInfo.FileName} {_process.StartInfo.Arguments}': {ex.Message}");
+#if DEBUG
+                    _reporter.Verbose(ex.ToString());
+#endif
+                }
             }
 
             private void OnExited(object sender, EventArgs args)
@@ -143,8 +163,8 @@ namespace Microsoft.DotNet.Watcher.Internal
             {
                 if (!_disposed)
                 {
-                    _disposed = true;
                     TryKill();
+                    _disposed = true;
                     _process.Exited -= OnExited;
                     _process.Dispose();
                 }

+ 1 - 1
src/Tools/dotnet-watch/src/Program.cs

@@ -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;

+ 1 - 1
src/Tools/dotnet-watch/src/dotnet-watch.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <TargetFramework>netcoreapp2.2</TargetFramework>
     <OutputType>exe</OutputType>
     <Description>Command line tool to watch for source file changes during development and restart the dotnet command.</Description>
     <RootNamespace>Microsoft.DotNet.Watcher.Tools</RootNamespace>

+ 36 - 12
src/Tools/dotnet-watch/test/AwaitableProcess.cs

@@ -1,12 +1,12 @@
-// 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;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks.Dataflow;
-using Microsoft.AspNetCore.Testing;
 using Microsoft.Extensions.Internal;
 using Microsoft.Extensions.CommandLineUtils;
 using Xunit.Abstractions;
@@ -17,16 +17,26 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
     {
         private Process _process;
         private readonly ProcessSpec _spec;
+        private readonly List<string> _lines;
         private BufferBlock<string> _source;
         private ITestOutputHelper _logger;
+        private TaskCompletionSource<int> _exited;
 
         public AwaitableProcess(ProcessSpec spec, ITestOutputHelper logger)
         {
             _spec = spec;
             _logger = logger;
             _source = new BufferBlock<string>();
+            _lines = new List<string>();
+            _exited = new TaskCompletionSource<int>();
         }
 
+        public IEnumerable<string> Output => _lines;
+
+        public Task Exited => _exited.Task;
+
+        public int Id => _process.Id;
+
         public void Start()
         {
             if (_process != null)
@@ -52,6 +62,11 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
                 }
             };
 
+            foreach (var env in _spec.EnvironmentVariables)
+            {
+                _process.StartInfo.EnvironmentVariables[env.Key] = env.Value;
+            }
+
             _process.OutputDataReceived += OnData;
             _process.ErrorDataReceived += OnData;
             _process.Exited += OnExit;
@@ -65,24 +80,30 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
         public async Task<string> GetOutputLineAsync(string message, TimeSpan timeout)
         {
             _logger.WriteLine($"Waiting for output line [msg == '{message}']. Will wait for {timeout.TotalSeconds} sec.");
-            return await GetOutputLineAsync(m => message == m).TimeoutAfter(timeout);
+            var cts = new CancellationTokenSource();
+            cts.CancelAfter(timeout);
+            return await GetOutputLineAsync($"[msg == '{message}']", m => string.Equals(m, message, StringComparison.Ordinal), cts.Token);
         }
 
         public async Task<string> GetOutputLineStartsWithAsync(string message, TimeSpan timeout)
         {
             _logger.WriteLine($"Waiting for output line [msg.StartsWith('{message}')]. Will wait for {timeout.TotalSeconds} sec.");
-            return await GetOutputLineAsync(m => m.StartsWith(message)).TimeoutAfter(timeout);
+            var cts = new CancellationTokenSource();
+            cts.CancelAfter(timeout);
+            return await GetOutputLineAsync($"[msg.StartsWith('{message}')]", m => m != null && m.StartsWith(message, StringComparison.Ordinal), cts.Token);
         }
 
-        private async Task<string> GetOutputLineAsync(Predicate<string> predicate)
+        private async Task<string> GetOutputLineAsync(string predicateName, Predicate<string> predicate, CancellationToken cancellationToken)
         {
             while (!_source.Completion.IsCompleted)
             {
-                while (await _source.OutputAvailableAsync())
+                while (await _source.OutputAvailableAsync(cancellationToken))
                 {
-                    var next = await _source.ReceiveAsync();
-                    _logger.WriteLine($"{DateTime.Now}: recv: '{next}'");
-                    if (predicate(next))
+                    var next = await _source.ReceiveAsync(cancellationToken);
+                    _lines.Add(next);
+                    var match = predicate(next);
+                    _logger.WriteLine($"{DateTime.Now}: recv: '{next}'. {(match ? "Matches" : "Does not match")} condition '{predicateName}'.");
+                    if (match)
                     {
                         return next;
                     }
@@ -92,14 +113,14 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
             return null;
         }
 
-        public async Task<IList<string>> GetAllOutputLines()
+        public async Task<IList<string>> GetAllOutputLinesAsync(CancellationToken cancellationToken)
         {
             var lines = new List<string>();
             while (!_source.Completion.IsCompleted)
             {
-                while (await _source.OutputAvailableAsync())
+                while (await _source.OutputAvailableAsync(cancellationToken))
                 {
-                    var next = await _source.ReceiveAsync();
+                    var next = await _source.ReceiveAsync(cancellationToken);
                     _logger.WriteLine($"{DateTime.Now}: recv: '{next}'");
                     lines.Add(next);
                 }
@@ -119,6 +140,8 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
             // Wait to ensure the process has exited and all output consumed
             _process.WaitForExit();
             _source.Complete();
+            _exited.TrySetResult(_process.ExitCode);
+            _logger.WriteLine($"Process {_process.Id} has exited");
         }
 
         public void Dispose()
@@ -135,6 +158,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
                 _process.ErrorDataReceived -= OnData;
                 _process.OutputDataReceived -= OnData;
                 _process.Exited -= OnExit;
+                _process.Dispose();
             }
         }
     }

+ 35 - 0
src/Tools/dotnet-watch/test/DotNetWatcherTests.cs

@@ -3,6 +3,8 @@
 
 using System;
 using System.Collections.Generic;
+using System.IO;
+using System.Globalization;
 using System.Threading.Tasks;
 using Xunit;
 using Xunit.Abstractions;
@@ -11,10 +13,12 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
 {
     public class DotNetWatcherTests : IDisposable
     {
+        private readonly ITestOutputHelper _logger;
         private readonly KitchenSinkApp _app;
 
         public DotNetWatcherTests(ITestOutputHelper logger)
         {
+            _logger = logger;
             _app = new KitchenSinkApp(logger);
         }
 
@@ -30,6 +34,37 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
             Assert.Equal("1", envValue);
         }
 
+        [Fact]
+        public async Task RunsWithIterationEnvVariable()
+        {
+            await _app.StartWatcherAsync();
+            var source = Path.Combine(_app.SourceDirectory, "Program.cs");
+            var contents = File.ReadAllText(source);
+            const string messagePrefix = "DOTNET_WATCH_ITERATION = ";
+            for (var i = 1; i <= 3; i++)
+            {
+                var message = await _app.Process.GetOutputLineStartsWithAsync(messagePrefix, TimeSpan.FromMinutes(2));
+                var count = int.Parse(message.Substring(messagePrefix.Length), CultureInfo.InvariantCulture);
+                Assert.Equal(i, count);
+
+                await _app.IsWaitingForFileChange();
+
+                try
+                {
+                    File.SetLastWriteTime(source, DateTime.Now);
+                    await _app.HasRestarted();
+                }
+                catch (Exception ex)
+                {
+                    _logger.WriteLine("Retrying. First attempt to restart app failed: " + ex.Message);
+
+                    // retry
+                    File.SetLastWriteTime(source, DateTime.Now);
+                    await _app.HasRestarted();
+                }
+            }
+        }
+
         public void Dispose()
         {
             _app.Dispose();

+ 31 - 29
src/Tools/dotnet-watch/test/FileWatcherTests.cs

@@ -304,42 +304,44 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
 
         private void AssertFileChangeRaisesEvent(string directory, IFileSystemWatcher watcher)
         {
-            var semaphoreSlim = new SemaphoreSlim(0);
-            var expectedPath = Path.Combine(directory, Path.GetRandomFileName());
-            EventHandler<string> handler = (object _, string f) =>
+            using (var semaphoreSlim = new SemaphoreSlim(0))
             {
-                _output.WriteLine("File changed: " + f);
-                try
+                var expectedPath = Path.Combine(directory, Path.GetRandomFileName());
+                EventHandler<string> handler = (object _, string f) =>
                 {
-                    if (string.Equals(f, expectedPath, StringComparison.OrdinalIgnoreCase))
+                    _output.WriteLine("File changed: " + f);
+                    try
+                    {
+                        if (string.Equals(f, expectedPath, StringComparison.OrdinalIgnoreCase))
+                        {
+                            semaphoreSlim.Release();
+                        }
+                    }
+                    catch (ObjectDisposedException)
                     {
-                        semaphoreSlim.Release();
+                        // There's a known race condition here:
+                        // even though we tell the watcher to stop raising events and we unsubscribe the handler
+                        // there might be in-flight events that will still process. Since we dispose the reset
+                        // event, this code will fail if the handler executes after Dispose happens.
                     }
+                };
+
+                File.AppendAllText(expectedPath, " ");
+
+                watcher.OnFileChange += handler;
+                try
+                {
+                    // On Unix the file write time is in 1s increments;
+                    // if we don't wait, there's a chance that the polling
+                    // watcher will not detect the change
+                    Thread.Sleep(1000);
+                    File.AppendAllText(expectedPath, " ");
+                    Assert.True(semaphoreSlim.Wait(DefaultTimeout), "Expected a file change event for " + expectedPath);
                 }
-                catch (ObjectDisposedException)
+                finally
                 {
-                    // There's a known race condition here:
-                    // even though we tell the watcher to stop raising events and we unsubscribe the handler
-                    // there might be in-flight events that will still process. Since we dispose the reset
-                    // event, this code will fail if the handler executes after Dispose happens.
+                    watcher.OnFileChange -= handler;
                 }
-            };
-
-            File.AppendAllText(expectedPath, " ");
-
-            watcher.OnFileChange += handler;
-            try
-            {
-                // On Unix the file write time is in 1s increments;
-                // if we don't wait, there's a chance that the polling
-                // watcher will not detect the change
-                Thread.Sleep(1000);
-                File.AppendAllText(expectedPath, " ");
-                Assert.True(semaphoreSlim.Wait(DefaultTimeout), "Expected a file change event for " + expectedPath);
-            }
-            finally
-            {
-                watcher.OnFileChange -= handler;
             }
         }
 

+ 6 - 2
src/Tools/dotnet-watch/test/GlobbingAppTests.cs

@@ -4,6 +4,7 @@
 using System;
 using System.IO;
 using System.Linq;
+using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.DotNet.Watcher.Tools.Tests;
 using Xunit;
@@ -101,7 +102,10 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
         {
             await _app.PrepareAsync();
             _app.Start(new [] { "--list" });
-            var lines = await _app.Process.GetAllOutputLines();
+            var cts = new CancellationTokenSource();
+            cts.CancelAfter(TimeSpan.FromSeconds(30));
+            var lines = await _app.Process.GetAllOutputLinesAsync(cts.Token);
+            var files = lines.Where(l => !l.StartsWith("watch :"));
 
             AssertEx.EqualFileList(
                 _app.Scenario.WorkFolder,
@@ -111,7 +115,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
                     "GlobbingApp/include/Foo.cs",
                     "GlobbingApp/GlobbingApp.csproj",
                 },
-                lines);
+                files);
         }
 
         public void Dispose()

+ 16 - 6
src/Tools/dotnet-watch/test/NoDepsAppTests.cs

@@ -15,10 +15,12 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
         private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
 
         private readonly WatchableApp _app;
+        private readonly ITestOutputHelper _output;
 
         public NoDepsAppTests(ITestOutputHelper logger)
         {
             _app = new WatchableApp("NoDepsApp", logger);
+            _output = logger;
         }
 
         [Fact]
@@ -33,11 +35,10 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
             File.WriteAllText(fileToChange, programCs);
 
             await _app.HasRestarted();
+            Assert.DoesNotContain(_app.Process.Output, l => l.StartsWith("Exited with error code"));
+
             var pid2 = await _app.GetProcessId();
             Assert.NotEqual(pid, pid2);
-
-            // first app should have shut down
-            Assert.Throws<ArgumentException>(() => Process.GetProcessById(pid));
         }
 
         [Fact]
@@ -49,10 +50,19 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
             await _app.IsWaitingForFileChange();
 
             var fileToChange = Path.Combine(_app.SourceDirectory, "Program.cs");
-            var programCs = File.ReadAllText(fileToChange);
-            File.WriteAllText(fileToChange, programCs);
 
-            await _app.HasRestarted();
+            try
+            {
+                File.SetLastWriteTime(fileToChange, DateTime.Now);
+                await _app.HasRestarted();
+            }
+            catch
+            {
+                // retry
+                File.SetLastWriteTime(fileToChange, DateTime.Now);
+                await _app.HasRestarted();
+            }
+
             var pid2 = await _app.GetProcessId();
             Assert.NotEqual(pid, pid2);
             await _app.HasExited(); // process should exit after run

+ 12 - 10
src/Tools/dotnet-watch/test/ProgramTests.cs

@@ -28,23 +28,25 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
         {
             _tempDir
                 .WithCSharpProject("testproj")
-                .WithTargetFrameworks("netcoreapp1.0")
+                .WithTargetFrameworks("netcoreapp2.2")
                 .Dir()
                 .WithFile("Program.cs")
                 .Create();
 
-            var stdout = new StringBuilder();
-            _console.Out = new StringWriter(stdout);
-            var program = new Program(_console, _tempDir.Root)
-                .RunAsync(new[] { "run" });
+            var output = new StringBuilder();
+            _console.Error = _console.Out = new StringWriter(output);
+            using (var app = new Program(_console, _tempDir.Root))
+            {
+                var run = app.RunAsync(new[] { "run" });
 
-            await _console.CancelKeyPressSubscribed.TimeoutAfter(TimeSpan.FromSeconds(30));
-            _console.ConsoleCancelKey();
+                await _console.CancelKeyPressSubscribed.TimeoutAfter(TimeSpan.FromSeconds(30));
+                _console.ConsoleCancelKey();
 
-            var exitCode = await program.TimeoutAfter(TimeSpan.FromSeconds(30));
+                var exitCode = await run.TimeoutAfter(TimeSpan.FromSeconds(30));
 
-            Assert.Contains("Shutdown requested. Press Ctrl+C again to force exit.", stdout.ToString());
-            Assert.Equal(0, exitCode);
+                Assert.Contains("Shutdown requested. Press Ctrl+C again to force exit.", output.ToString());
+                Assert.Equal(0, exitCode);
+            }
         }
 
         public void Dispose()

+ 1 - 3
src/Tools/dotnet-watch/test/Scenario/ProjectToolScenario.cs

@@ -1,8 +1,7 @@
-// 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;
-using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
@@ -12,7 +11,6 @@ using System.Threading.Tasks;
 using System.Xml.Linq;
 using Microsoft.Extensions.CommandLineUtils;
 using Microsoft.Extensions.Internal;
-using Microsoft.Extensions.Tools.Internal;
 using Xunit.Abstractions;
 
 namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests

+ 1 - 1
src/Tools/dotnet-watch/test/Scenario/WatchableApp.cs

@@ -47,7 +47,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
         public async Task HasExited()
         {
             await Process.GetOutputLineAsync(ExitingMessage, DefaultMessageTimeOut);
-            await Process.GetOutputLineAsync(WatchExitedMessage, DefaultMessageTimeOut);
+            await Process.GetOutputLineStartsWithAsync(WatchExitedMessage, DefaultMessageTimeOut);
         }
 
         public async Task IsWaitingForFileChange()

+ 1 - 1
src/Tools/dotnet-watch/test/TestProjects/AppWithDeps/AppWithDeps.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <TargetFramework>netcoreapp2.2</TargetFramework>
     <OutputType>exe</OutputType>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
   </PropertyGroup>

+ 2 - 2
src/Tools/dotnet-watch/test/TestProjects/GlobbingApp/GlobbingApp.csproj

@@ -1,7 +1,7 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <TargetFramework>netcoreapp2.2</TargetFramework>
     <OutputType>exe</OutputType>
     <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

+ 1 - 1
src/Tools/dotnet-watch/test/TestProjects/KitchenSink/KitchenSink.csproj

@@ -9,7 +9,7 @@
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <TargetFramework>netcoreapp2.2</TargetFramework>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
   </PropertyGroup>
 

+ 1 - 0
src/Tools/dotnet-watch/test/TestProjects/KitchenSink/Program.cs

@@ -13,6 +13,7 @@ namespace KitchenSink
             Console.WriteLine("Started");
             Console.WriteLine("PID = " + Process.GetCurrentProcess().Id);
             Console.WriteLine("DOTNET_WATCH = " + Environment.GetEnvironmentVariable("DOTNET_WATCH"));
+            Console.WriteLine("DOTNET_WATCH_ITERATION = " + Environment.GetEnvironmentVariable("DOTNET_WATCH_ITERATION"));
         }
     }
 }

+ 2 - 2
src/Tools/dotnet-watch/test/TestProjects/NoDepsApp/NoDepsApp.csproj

@@ -1,7 +1,7 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <TargetFramework>netcoreapp2.2</TargetFramework>
     <OutputType>exe</OutputType>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
   </PropertyGroup>

+ 2 - 2
src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj

@@ -1,7 +1,7 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <TargetFramework>netcoreapp2.2</TargetFramework>
     <AssemblyName>Microsoft.DotNet.Watcher.Tools.Tests</AssemblyName>
     <DefaultItemExcludes>$(DefaultItemExcludes);TestProjects\**\*</DefaultItemExcludes>
   </PropertyGroup>

+ 4 - 5
src/Tools/shared/src/CliContext.cs

@@ -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;
@@ -8,14 +8,13 @@ namespace Microsoft.Extensions.Tools.Internal
     public static class CliContext
     {
         /// <summary>
-        /// dotnet --verbose subcommand
+        /// dotnet -d|--diagnostics subcommand
         /// </summary>
         /// <returns></returns>
         public static bool IsGlobalVerbose()
         {
-            bool globalVerbose;
-            bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE"), out globalVerbose);
+            bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE"), out bool globalVerbose);
             return globalVerbose;
         }
     }
-}
+}