diff --git a/eng/NuGetPackageVerifier.json b/eng/NuGetPackageVerifier.json index 4ce6d1a168..54e39b2ccd 100644 --- a/eng/NuGetPackageVerifier.json +++ b/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" } } } diff --git a/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj b/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj index ce61b3ab62..c5b21cb478 100644 --- a/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj +++ b/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj @@ -1,7 +1,7 @@ - netcoreapp2.1 + netcoreapp2.2 Package for the CLI first run experience. $(DefineConstants);XPLAT aspnet;cli diff --git a/src/Tools/dotnet-dev-certs/src/dotnet-dev-certs.csproj b/src/Tools/dotnet-dev-certs/src/dotnet-dev-certs.csproj index 1f0f906e50..a0fe307a72 100644 --- a/src/Tools/dotnet-dev-certs/src/dotnet-dev-certs.csproj +++ b/src/Tools/dotnet-dev-certs/src/dotnet-dev-certs.csproj @@ -1,7 +1,7 @@ - netcoreapp2.1 + netcoreapp2.2 exe Command line tool to generate certificates used in ASP.NET Core during development. Microsoft.AspNetCore.DeveloperCertificates.Tools diff --git a/src/Tools/dotnet-sql-cache/src/dotnet-sql-cache.csproj b/src/Tools/dotnet-sql-cache/src/dotnet-sql-cache.csproj index 3856c4877c..b4db8f100f 100644 --- a/src/Tools/dotnet-sql-cache/src/dotnet-sql-cache.csproj +++ b/src/Tools/dotnet-sql-cache/src/dotnet-sql-cache.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp2.2 exe Command line tool to create tables and indexes in a Microsoft SQL Server database for distributed caching. cache;distributedcache;sqlserver diff --git a/src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj b/src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj index a66a31dd7e..46feeeb1a6 100644 --- a/src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj +++ b/src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp2.2 exe Command line tool to manage user secrets for Microsoft.Extensions.Configuration. false diff --git a/src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs b/src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs index 14e62805bc..590787ec31 100644 --- a/src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs +++ b/src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs @@ -35,7 +35,7 @@ namespace Microsoft.Extensions.Configuration.UserSecrets.Tests private const string ProjectTemplate = @" Exe - netcoreapp2.1 + netcoreapp2.2 {0} false diff --git a/src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj b/src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj index 0254a866e8..56c964c016 100644 --- a/src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj +++ b/src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp2.2 Microsoft.Extensions.SecretManager.Tools.Tests diff --git a/src/Tools/dotnet-watch/src/CommandLineOptions.cs b/src/Tools/dotnet-watch/src/CommandLineOptions.cs index e6e23890d5..5cbaab33e9 100644 --- a/src/Tools/dotnet-watch/src/CommandLineOptions.cs +++ b/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. diff --git a/src/Tools/dotnet-watch/src/DotNetWatcher.cs b/src/Tools/dotnet-watch/src/DotNetWatcher.cs index 258ba0ab1e..8431615c1e 100644 --- a/src/Tools/dotnet-watch/src/DotNetWatcher.cs +++ b/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) 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) diff --git a/src/Tools/dotnet-watch/src/Internal/FileWatcher/DotnetFileWatcher.cs b/src/Tools/dotnet-watch/src/Internal/FileWatcher/DotnetFileWatcher.cs index d1103c41e6..0372327819 100644 --- a/src/Tools/dotnet-watch/src/Internal/FileWatcher/DotnetFileWatcher.cs +++ b/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 _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(); } } } diff --git a/src/Tools/dotnet-watch/src/Internal/ProcessRunner.cs b/src/Tools/dotnet-watch/src/Internal/ProcessRunner.cs index a5f7cac8ef..bbded25611 100644 --- a/src/Tools/dotnet-watch/src/Internal/ProcessRunner.cs +++ b/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 _tcs = new TaskCompletionSource(); 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(); + _process.WaitForExit(); + } + catch (InvalidOperationException) + { + // suppress if this throws if no process is associated with this object anymore. + } }); } @@ -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(); } diff --git a/src/Tools/dotnet-watch/src/Program.cs b/src/Tools/dotnet-watch/src/Program.cs index 7e8200b102..25317fb6b2 100644 --- a/src/Tools/dotnet-watch/src/Program.cs +++ b/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; diff --git a/src/Tools/dotnet-watch/src/dotnet-watch.csproj b/src/Tools/dotnet-watch/src/dotnet-watch.csproj index 4aff804186..09f60754c3 100644 --- a/src/Tools/dotnet-watch/src/dotnet-watch.csproj +++ b/src/Tools/dotnet-watch/src/dotnet-watch.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp2.2 exe Command line tool to watch for source file changes during development and restart the dotnet command. Microsoft.DotNet.Watcher.Tools diff --git a/src/Tools/dotnet-watch/test/AwaitableProcess.cs b/src/Tools/dotnet-watch/test/AwaitableProcess.cs index 91b53133eb..3e22d53245 100644 --- a/src/Tools/dotnet-watch/test/AwaitableProcess.cs +++ b/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 _lines; private BufferBlock _source; private ITestOutputHelper _logger; + private TaskCompletionSource _exited; public AwaitableProcess(ProcessSpec spec, ITestOutputHelper logger) { _spec = spec; _logger = logger; _source = new BufferBlock(); + _lines = new List(); + _exited = new TaskCompletionSource(); } + public IEnumerable 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 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 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 GetOutputLineAsync(Predicate predicate) + private async Task GetOutputLineAsync(string predicateName, Predicate 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> GetAllOutputLines() + public async Task> GetAllOutputLinesAsync(CancellationToken cancellationToken) { var lines = new List(); 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(); } } } diff --git a/src/Tools/dotnet-watch/test/DotNetWatcherTests.cs b/src/Tools/dotnet-watch/test/DotNetWatcherTests.cs index d0dc735247..d31f650aa6 100644 --- a/src/Tools/dotnet-watch/test/DotNetWatcherTests.cs +++ b/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(); diff --git a/src/Tools/dotnet-watch/test/FileWatcherTests.cs b/src/Tools/dotnet-watch/test/FileWatcherTests.cs index 7cd4bd15aa..51c1895e05 100644 --- a/src/Tools/dotnet-watch/test/FileWatcherTests.cs +++ b/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 handler = (object _, string f) => + using (var semaphoreSlim = new SemaphoreSlim(0)) { - _output.WriteLine("File changed: " + f); + var expectedPath = Path.Combine(directory, Path.GetRandomFileName()); + EventHandler handler = (object _, string f) => + { + _output.WriteLine("File changed: " + f); + try + { + if (string.Equals(f, expectedPath, StringComparison.OrdinalIgnoreCase)) + { + semaphoreSlim.Release(); + } + } + catch (ObjectDisposedException) + { + // 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 { - if (string.Equals(f, expectedPath, StringComparison.OrdinalIgnoreCase)) - { - semaphoreSlim.Release(); - } + // 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; } } diff --git a/src/Tools/dotnet-watch/test/GlobbingAppTests.cs b/src/Tools/dotnet-watch/test/GlobbingAppTests.cs index 71b5d068bf..3658261049 100644 --- a/src/Tools/dotnet-watch/test/GlobbingAppTests.cs +++ b/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() diff --git a/src/Tools/dotnet-watch/test/NoDepsAppTests.cs b/src/Tools/dotnet-watch/test/NoDepsAppTests.cs index 8ac2f694e7..d1eead9048 100644 --- a/src/Tools/dotnet-watch/test/NoDepsAppTests.cs +++ b/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(() => 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 diff --git a/src/Tools/dotnet-watch/test/ProgramTests.cs b/src/Tools/dotnet-watch/test/ProgramTests.cs index 8c116b5595..a5998b25c8 100644 --- a/src/Tools/dotnet-watch/test/ProgramTests.cs +++ b/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() diff --git a/src/Tools/dotnet-watch/test/Scenario/ProjectToolScenario.cs b/src/Tools/dotnet-watch/test/Scenario/ProjectToolScenario.cs index a0a14093ec..b5c064563b 100644 --- a/src/Tools/dotnet-watch/test/Scenario/ProjectToolScenario.cs +++ b/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 diff --git a/src/Tools/dotnet-watch/test/Scenario/WatchableApp.cs b/src/Tools/dotnet-watch/test/Scenario/WatchableApp.cs index 56702afac2..4f2d575f01 100644 --- a/src/Tools/dotnet-watch/test/Scenario/WatchableApp.cs +++ b/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() diff --git a/src/Tools/dotnet-watch/test/TestProjects/AppWithDeps/AppWithDeps.csproj b/src/Tools/dotnet-watch/test/TestProjects/AppWithDeps/AppWithDeps.csproj index 0dcb552112..79ccf5fc71 100644 --- a/src/Tools/dotnet-watch/test/TestProjects/AppWithDeps/AppWithDeps.csproj +++ b/src/Tools/dotnet-watch/test/TestProjects/AppWithDeps/AppWithDeps.csproj @@ -1,7 +1,7 @@ - netcoreapp2.1 + netcoreapp2.2 exe true diff --git a/src/Tools/dotnet-watch/test/TestProjects/GlobbingApp/GlobbingApp.csproj b/src/Tools/dotnet-watch/test/TestProjects/GlobbingApp/GlobbingApp.csproj index a01efb4b2f..3376aea0ae 100644 --- a/src/Tools/dotnet-watch/test/TestProjects/GlobbingApp/GlobbingApp.csproj +++ b/src/Tools/dotnet-watch/test/TestProjects/GlobbingApp/GlobbingApp.csproj @@ -1,7 +1,7 @@ - + - netcoreapp2.1 + netcoreapp2.2 exe false true diff --git a/src/Tools/dotnet-watch/test/TestProjects/KitchenSink/KitchenSink.csproj b/src/Tools/dotnet-watch/test/TestProjects/KitchenSink/KitchenSink.csproj index 72f7d5cae4..767c7e69ac 100644 --- a/src/Tools/dotnet-watch/test/TestProjects/KitchenSink/KitchenSink.csproj +++ b/src/Tools/dotnet-watch/test/TestProjects/KitchenSink/KitchenSink.csproj @@ -9,7 +9,7 @@ Exe - netcoreapp2.1 + netcoreapp2.2 true diff --git a/src/Tools/dotnet-watch/test/TestProjects/KitchenSink/Program.cs b/src/Tools/dotnet-watch/test/TestProjects/KitchenSink/Program.cs index 5251cdc1e0..f38dc8231b 100644 --- a/src/Tools/dotnet-watch/test/TestProjects/KitchenSink/Program.cs +++ b/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")); } } } diff --git a/src/Tools/dotnet-watch/test/TestProjects/NoDepsApp/NoDepsApp.csproj b/src/Tools/dotnet-watch/test/TestProjects/NoDepsApp/NoDepsApp.csproj index b242bd2546..031f1bb1a2 100644 --- a/src/Tools/dotnet-watch/test/TestProjects/NoDepsApp/NoDepsApp.csproj +++ b/src/Tools/dotnet-watch/test/TestProjects/NoDepsApp/NoDepsApp.csproj @@ -1,7 +1,7 @@ - + - netcoreapp2.1 + netcoreapp2.2 exe true diff --git a/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj b/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj index 2bad8c40c4..ecb1cdf907 100644 --- a/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj +++ b/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj @@ -1,7 +1,7 @@ - + - netcoreapp2.1 + netcoreapp2.2 Microsoft.DotNet.Watcher.Tools.Tests $(DefaultItemExcludes);TestProjects\**\* diff --git a/src/Tools/shared/src/CliContext.cs b/src/Tools/shared/src/CliContext.cs index ad766a2e3b..854ea0fef6 100644 --- a/src/Tools/shared/src/CliContext.cs +++ b/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 { /// - /// dotnet --verbose subcommand + /// dotnet -d|--diagnostics subcommand /// /// 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; } } -} \ No newline at end of file +}