Avoid running restores for dotnet-watch run (#23421)

* Tweaks to make dotnet-watch run faster

* Previously dotnet-watch calculated the watch file list on every run by invoking MSBuild. This
  changes the tool to only calculate it if an MSBuild file (.targets, .props, .csproj etc) file changed
* For dotnet watch run and dotnet watch test command, use --no-restore if changed file is not an MSBuild file.

* Add opt-out switch

* Update src/Tools/dotnet-watch/README.md

* Fixup typo

* Update src/Tools/dotnet-watch/README.md
This commit is contained in:
Pranav K 2020-07-08 11:56:25 -07:00 committed by GitHub
parent 769fc6d289
commit 763a18ee56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 680 additions and 16 deletions

View File

@ -29,6 +29,7 @@ Some configuration options can be passed to `dotnet watch` through environment v
| Variable | Effect |
| ---------------------------------------------- | -------------------------------------------------------- |
| DOTNET_USE_POLLING_FILE_WATCHER | If set to "1" or "true", `dotnet watch` will use a polling file watcher instead of CoreFx's `FileSystemWatcher`. Used when watching files on network shares or Docker mounted volumes. |
| DOTNET_WATCH_SUPPRESS_MSBUILD_INCREMENTALISM | By default, `dotnet watch` optimizes the build by avoiding certain operations such as running restore or re-evaluating the set of watched files on every file change. If set to "1" or "true", these optimizations are disabled. |
### MSBuild

View File

@ -0,0 +1,24 @@
// 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;
namespace Microsoft.DotNet.Watcher.Tools
{
public class DotNetWatchContext
{
public IReporter Reporter { get; set; } = NullReporter.Singleton;
public ProcessSpec ProcessSpec { get; set; }
public IFileSet FileSet { get; set; }
public int Iteration { get; set; }
public string ChangedFile { get; set; }
public bool RequiresMSBuildRevaluation { get; set; }
public bool SuppressMSBuildIncrementalism { get; set; }
}
}

View File

@ -3,9 +3,12 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.Watcher.Internal;
using Microsoft.DotNet.Watcher.Tools;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
@ -15,33 +18,63 @@ namespace Microsoft.DotNet.Watcher
{
private readonly IReporter _reporter;
private readonly ProcessRunner _processRunner;
private readonly IWatchFilter[] _filters;
public DotNetWatcher(IReporter reporter)
public DotNetWatcher(IReporter reporter, IFileSetFactory fileSetFactory)
{
Ensure.NotNull(reporter, nameof(reporter));
_reporter = reporter;
_processRunner = new ProcessRunner(reporter);
_filters = new IWatchFilter[]
{
new MSBuildEvaluationFilter(fileSetFactory),
new NoRestoreFilter(),
};
}
public async Task WatchAsync(ProcessSpec processSpec, IFileSetFactory fileSetFactory,
CancellationToken cancellationToken)
public async Task WatchAsync(ProcessSpec processSpec, CancellationToken cancellationToken)
{
Ensure.NotNull(processSpec, nameof(processSpec));
var cancelledTaskSource = new TaskCompletionSource<object>();
cancellationToken.Register(state => ((TaskCompletionSource<object>) state).TrySetResult(null),
var cancelledTaskSource = new TaskCompletionSource();
cancellationToken.Register(state => ((TaskCompletionSource)state).TrySetResult(),
cancelledTaskSource);
var iteration = 1;
var initialArguments = processSpec.Arguments.ToArray();
var suppressMSBuildIncrementalism = Environment.GetEnvironmentVariable("DOTNET_WATCH_SUPPRESS_MSBUILD_INCREMENTALISM");
var context = new DotNetWatchContext
{
Iteration = -1,
ProcessSpec = processSpec,
Reporter = _reporter,
SuppressMSBuildIncrementalism = suppressMSBuildIncrementalism == "1" || suppressMSBuildIncrementalism == "true",
};
if (context.SuppressMSBuildIncrementalism)
{
_reporter.Verbose("MSBuild incremental optimizations suppressed.");
}
while (true)
{
processSpec.EnvironmentVariables["DOTNET_WATCH_ITERATION"] = iteration.ToString(CultureInfo.InvariantCulture);
iteration++;
context.Iteration++;
var fileSet = await fileSetFactory.CreateAsync(cancellationToken);
// Reset arguments
processSpec.Arguments = initialArguments;
for (var i = 0; i < _filters.Length; i++)
{
await _filters[i].ProcessAsync(context, cancellationToken);
}
// Reset for next run
context.RequiresMSBuildRevaluation = false;
processSpec.EnvironmentVariables["DOTNET_WATCH_ITERATION"] = (context.Iteration + 1).ToString(CultureInfo.InvariantCulture);
var fileSet = context.FileSet;
if (fileSet == null)
{
_reporter.Error("Failed to find a list of files to watch");
@ -91,10 +124,13 @@ namespace Microsoft.DotNet.Watcher
return;
}
context.ChangedFile = fileSetTask.Result;
if (finishedTask == processTask)
{
// Process exited. Redo evaludation
context.RequiresMSBuildRevaluation = true;
// Now wait for a file to change before restarting process
await fileSetWatcher.GetChangedFileAsync(cancellationToken, () => _reporter.Warn("Waiting for a file to change before restarting dotnet..."));
context.ChangedFile = await fileSetWatcher.GetChangedFileAsync(cancellationToken, () => _reporter.Warn("Waiting for a file to change before restarting dotnet..."));
}
if (!string.IsNullOrEmpty(fileSetTask.Result))

View File

@ -0,0 +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 System.Threading;
using System.Threading.Tasks;
namespace Microsoft.DotNet.Watcher.Tools
{
public interface IWatchFilter
{
ValueTask ProcessAsync(DotNetWatchContext context, CancellationToken cancellationToken);
}
}

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;
@ -20,6 +20,8 @@ namespace Microsoft.DotNet.Watcher.Internal
public int Count => _files.Count;
public static IFileSet Empty = new FileSet(Array.Empty<string>());
public IEnumerator<string> GetEnumerator() => _files.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _files.GetEnumerator();
}

View File

@ -0,0 +1,124 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.DotNet.Watcher.Tools
{
public class MSBuildEvaluationFilter : IWatchFilter
{
// File types that require an MSBuild re-evaluation
private static readonly string[] _msBuildFileExtensions = new[]
{
".csproj", ".props", ".targets", ".fsproj", ".vbproj", ".vcxproj",
};
private static readonly int[] _msBuildFileExtensionHashes = _msBuildFileExtensions
.Select(e => e.GetHashCode(StringComparison.OrdinalIgnoreCase))
.ToArray();
private readonly IFileSetFactory _factory;
private List<(string fileName, DateTime lastWriteTimeUtc)> _msbuildFileTimestamps;
public MSBuildEvaluationFilter(IFileSetFactory factory)
{
_factory = factory;
}
public async ValueTask ProcessAsync(DotNetWatchContext context, CancellationToken cancellationToken)
{
if (context.SuppressMSBuildIncrementalism)
{
context.RequiresMSBuildRevaluation = true;
context.FileSet = await _factory.CreateAsync(cancellationToken);
return;
}
if (context.Iteration == 0 || RequiresMSBuildRevaluation(context))
{
context.RequiresMSBuildRevaluation = true;
}
if (context.RequiresMSBuildRevaluation)
{
context.Reporter.Verbose("Evaluating dotnet-watch file set.");
context.FileSet = await _factory.CreateAsync(cancellationToken);
_msbuildFileTimestamps = GetMSBuildFileTimeStamps(context);
}
}
private bool RequiresMSBuildRevaluation(DotNetWatchContext context)
{
var changedFile = context.ChangedFile;
if (!string.IsNullOrEmpty(changedFile) && IsMsBuildFileExtension(changedFile))
{
return true;
}
// The filewatcher may miss changes to files. For msbuild files, we can verify that they haven't been modified
// since the previous iteration.
// We do not have a way to identify renames or new additions that the file watcher did not pick up,
// without performing an evaluation. We will start off by keeping it simple and comparing the timestamps
// of known MSBuild files from previous run. This should cover the vast majority of cases.
foreach (var (file, lastWriteTimeUtc) in _msbuildFileTimestamps)
{
if (GetLastWriteTimeUtcSafely(file) != lastWriteTimeUtc)
{
context.Reporter.Verbose($"Re-evaluation needed due to changes in {file}.");
return true;
}
}
return false;
}
private List<(string fileName, DateTime lastModifiedUtc)> GetMSBuildFileTimeStamps(DotNetWatchContext context)
{
var msbuildFiles = new List<(string fileName, DateTime lastModifiedUtc)>();
foreach (var file in context.FileSet)
{
if (!string.IsNullOrEmpty(file) && IsMsBuildFileExtension(file))
{
msbuildFiles.Add((file, GetLastWriteTimeUtcSafely(file)));
}
}
return msbuildFiles;
}
protected virtual DateTime GetLastWriteTimeUtcSafely(string file)
{
try
{
return File.GetLastWriteTimeUtc(file);
}
catch
{
return DateTime.UtcNow;
}
}
static bool IsMsBuildFileExtension(string fileName)
{
var extension = Path.GetExtension(fileName.AsSpan());
var hashCode = string.GetHashCode(extension, StringComparison.OrdinalIgnoreCase);
for (var i = 0; i < _msBuildFileExtensionHashes.Length; i++)
{
if (_msBuildFileExtensionHashes[i] == hashCode && extension.Equals(_msBuildFileExtensions[i], StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
}
}

View File

@ -0,0 +1,75 @@
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher.Tools
{
public sealed class NoRestoreFilter : IWatchFilter
{
private bool _canUseNoRestore;
private string[] _noRestoreArguments;
public ValueTask ProcessAsync(DotNetWatchContext context, CancellationToken cancellationToken)
{
if (context.SuppressMSBuildIncrementalism)
{
return default;
}
if (context.Iteration == 0)
{
var arguments = context.ProcessSpec.Arguments;
_canUseNoRestore = CanUseNoRestore(arguments, context.Reporter);
if (_canUseNoRestore)
{
// Create run --no-restore <other args>
_noRestoreArguments = arguments.Take(1).Append("--no-restore").Concat(arguments.Skip(1)).ToArray();
context.Reporter.Verbose($"No restore arguments: {string.Join(" ", _noRestoreArguments)}");
}
}
else if (_canUseNoRestore)
{
if (context.RequiresMSBuildRevaluation)
{
context.Reporter.Verbose("Cannot use --no-restore since msbuild project files have changed.");
}
else
{
context.Reporter.Verbose("Modifying command to use --no-restore");
context.ProcessSpec.Arguments = _noRestoreArguments;
}
}
return default;
}
private static bool CanUseNoRestore(IEnumerable<string> arguments, IReporter reporter)
{
// For some well-known dotnet commands, we can pass in the --no-restore switch to avoid unnecessary restores between iterations.
// For now we'll support the "run" and "test" commands.
if (arguments.Any(a => string.Equals(a, "--no-restore", StringComparison.Ordinal)))
{
// Did the user already configure a --no-restore?
return false;
}
var dotnetCommand = arguments.FirstOrDefault();
if (string.Equals(dotnetCommand, "run", StringComparison.Ordinal) || string.Equals(dotnetCommand, "test", StringComparison.Ordinal))
{
reporter.Verbose("Watch command can be configured to use --no-restore.");
return true;
}
else
{
reporter.Verbose($"Watch command will not use --no-restore. Unsupport dotnet-command '{dotnetCommand}'.");
return false;
}
}
}
}

View File

@ -162,8 +162,8 @@ namespace Microsoft.DotNet.Watcher
_reporter.Output("Polling file watcher is enabled");
}
await new DotNetWatcher(reporter)
.WatchAsync(processInfo, fileSetFactory, cancellationToken);
await new DotNetWatcher(reporter, fileSetFactory)
.WatchAsync(processInfo, cancellationToken);
return 0;
}

View File

@ -2,10 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing;
using Xunit;
using Xunit.Abstractions;
@ -54,6 +53,51 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
}
}
[Fact]
public async Task RunsWithNoRestoreOnOrdinaryFileChanges()
{
_app.DotnetWatchArgs.Add("--verbose");
await _app.StartWatcherAsync(arguments: new[] { "wait" });
var source = Path.Combine(_app.SourceDirectory, "Program.cs");
const string messagePrefix = "watch : Running dotnet with the following arguments: run";
// Verify that the first run does not use --no-restore
Assert.Contains(_app.Process.Output, p => string.Equals(messagePrefix + " -- wait", p.Trim()));
for (var i = 0; i < 3; i++)
{
File.SetLastWriteTime(source, DateTime.Now);
var message = await _app.Process.GetOutputLineStartsWithAsync(messagePrefix, TimeSpan.FromMinutes(2));
Assert.Equal(messagePrefix + " --no-restore -- wait", message.Trim());
}
}
[Fact]
public async Task RunsWithRestoreIfCsprojChanges()
{
_app.DotnetWatchArgs.Add("--verbose");
await _app.StartWatcherAsync(arguments: new[] { "wait" });
var source = Path.Combine(_app.SourceDirectory, "KitchenSink.csproj");
const string messagePrefix = "watch : Running dotnet with the following arguments: run";
// Verify that the first run does not use --no-restore
Assert.Contains(_app.Process.Output, p => string.Equals(messagePrefix + " -- wait", p.Trim()));
File.SetLastWriteTime(source, DateTime.Now);
var message = await _app.Process.GetOutputLineStartsWithAsync(messagePrefix, TimeSpan.FromMinutes(2));
// csproj changed. Do not expect a --no-restore
Assert.Equal(messagePrefix + " -- wait", message.Trim());
// regular file changed after csproj changes. Should use --no-restore
File.SetLastWriteTime(Path.Combine(_app.SourceDirectory, "Program.cs"), DateTime.Now);
message = await _app.Process.GetOutputLineStartsWithAsync(messagePrefix, TimeSpan.FromMinutes(2));
Assert.Equal(messagePrefix + " --no-restore -- wait", message.Trim());
}
public void Dispose()
{
_app.Dispose();

View File

@ -0,0 +1,142 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.Watcher.Internal;
using Moq;
using Xunit;
namespace Microsoft.DotNet.Watcher.Tools
{
public class MSBuildEvaluationFilterTest
{
private readonly IFileSetFactory _fileSetFactory = Mock.Of<IFileSetFactory>(
f => f.CreateAsync(It.IsAny<CancellationToken>()) == Task.FromResult<IFileSet>(new FileSet(Enumerable.Empty<string>())));
[Fact]
public async Task ProcessAsync_EvaluatesFileSetIfProjFileChanges()
{
// Arrange
var filter = new MSBuildEvaluationFilter(_fileSetFactory);
var context = new DotNetWatchContext
{
Iteration = 0,
};
await filter.ProcessAsync(context, default);
context.Iteration++;
context.ChangedFile = "Test.csproj";
context.RequiresMSBuildRevaluation = false;
// Act
await filter.ProcessAsync(context, default);
// Assert
Assert.True(context.RequiresMSBuildRevaluation);
}
[Fact]
public async Task ProcessAsync_DoesNotEvaluateFileSetIfNonProjFileChanges()
{
// Arrange
var filter = new MSBuildEvaluationFilter(_fileSetFactory);
var context = new DotNetWatchContext
{
Iteration = 0,
};
await filter.ProcessAsync(context, default);
context.Iteration++;
context.ChangedFile = "Controller.cs";
context.RequiresMSBuildRevaluation = false;
// Act
await filter.ProcessAsync(context, default);
// Assert
Assert.False(context.RequiresMSBuildRevaluation);
Mock.Get(_fileSetFactory).Verify(v => v.CreateAsync(It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ProcessAsync_EvaluateFileSetOnEveryChangeIfOptimizationIsSuppressed()
{
// Arrange
var filter = new MSBuildEvaluationFilter(_fileSetFactory);
var context = new DotNetWatchContext
{
Iteration = 0,
SuppressMSBuildIncrementalism = true,
};
await filter.ProcessAsync(context, default);
context.Iteration++;
context.ChangedFile = "Controller.cs";
context.RequiresMSBuildRevaluation = false;
// Act
await filter.ProcessAsync(context, default);
// Assert
Assert.True(context.RequiresMSBuildRevaluation);
Mock.Get(_fileSetFactory).Verify(v => v.CreateAsync(It.IsAny<CancellationToken>()), Times.Exactly(2));
}
[Fact]
public async Task ProcessAsync_SetsEvaluationRequired_IfMSBuildFileChanges_ButIsNotChangedFile()
{
// There's a chance that the watcher does not correctly report edits to msbuild files on
// concurrent edits. MSBuildEvaluationFilter uses timestamps to additionally track changes to these files.
// Arrange
var fileSet = new FileSet(new[] { "Controlller.cs", "Proj.csproj" });
var fileSetFactory = Mock.Of<IFileSetFactory>(f => f.CreateAsync(It.IsAny<CancellationToken>()) == Task.FromResult<IFileSet>(fileSet));
var filter = new TestableMSBuildEvaluationFilter(fileSetFactory)
{
Timestamps =
{
["Controller.cs"] = new DateTime(1000),
["Proj.csproj"] = new DateTime(1000),
}
};
var context = new DotNetWatchContext
{
Iteration = 0,
};
await filter.ProcessAsync(context, default);
context.RequiresMSBuildRevaluation = false;
context.ChangedFile = "Controller.cs";
context.Iteration++;
filter.Timestamps["Proj.csproj"] = new DateTime(1007);
// Act
await filter.ProcessAsync(context, default);
// Assert
Assert.True(context.RequiresMSBuildRevaluation);
}
public class TestableMSBuildEvaluationFilter : MSBuildEvaluationFilter
{
public TestableMSBuildEvaluationFilter(IFileSetFactory factory)
: base(factory)
{
}
public Dictionary<string, DateTime> Timestamps { get; } = new Dictionary<string, DateTime>();
protected override DateTime GetLastWriteTimeUtcSafely(string file) => Timestamps[file];
}
}
}

View File

@ -0,0 +1,193 @@
// 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.Threading.Tasks;
using Xunit;
namespace Microsoft.DotNet.Watcher.Tools
{
public class NoRestoreFilterTest
{
private readonly string[] _arguments = new[] { "run" };
[Fact]
public async Task ProcessAsync_LeavesArgumentsUnchangedOnFirstRun()
{
// Arrange
var filter = new NoRestoreFilter();
var context = new DotNetWatchContext
{
ProcessSpec = new ProcessSpec
{
Arguments = _arguments,
}
};
// Act
await filter.ProcessAsync(context, default);
// Assert
Assert.Same(_arguments, context.ProcessSpec.Arguments);
}
[Fact]
public async Task ProcessAsync_LeavesArgumentsUnchangedIfMsBuildRevaluationIsRequired()
{
// Arrange
var filter = new NoRestoreFilter();
var context = new DotNetWatchContext
{
Iteration = 0,
ProcessSpec = new ProcessSpec
{
Arguments = _arguments,
}
};
await filter.ProcessAsync(context, default);
context.ChangedFile = "Test.proj";
context.RequiresMSBuildRevaluation = true;
context.Iteration++;
// Act
await filter.ProcessAsync(context, default);
// Assert
Assert.Same(_arguments, context.ProcessSpec.Arguments);
}
[Fact]
public async Task ProcessAsync_LeavesArgumentsUnchangedIfOptimizationIsSuppressed()
{
// Arrange
var filter = new NoRestoreFilter();
var context = new DotNetWatchContext
{
Iteration = 0,
ProcessSpec = new ProcessSpec
{
Arguments = _arguments,
},
SuppressMSBuildIncrementalism = true,
};
await filter.ProcessAsync(context, default);
context.ChangedFile = "Program.cs";
context.Iteration++;
// Act
await filter.ProcessAsync(context, default);
// Assert
Assert.Same(_arguments, context.ProcessSpec.Arguments);
}
[Fact]
public async Task ProcessAsync_AddsNoRestoreSwitch()
{
// Arrange
var filter = new NoRestoreFilter();
var context = new DotNetWatchContext
{
Iteration = 0,
ProcessSpec = new ProcessSpec
{
Arguments = _arguments,
}
};
await filter.ProcessAsync(context, default);
context.ChangedFile = "Program.cs";
context.Iteration++;
// Act
await filter.ProcessAsync(context, default);
// Assert
Assert.Equal(new[] { "run", "--no-restore" }, context.ProcessSpec.Arguments);
}
[Fact]
public async Task ProcessAsync_AddsNoRestoreSwitch_WithAdditionalArguments()
{
// Arrange
var filter = new NoRestoreFilter();
var context = new DotNetWatchContext
{
Iteration = 0,
ProcessSpec = new ProcessSpec
{
Arguments = new[] { "run", "-f", "net5.0", "--", "foo=bar" },
}
};
await filter.ProcessAsync(context, default);
context.ChangedFile = "Program.cs";
context.Iteration++;
// Act
await filter.ProcessAsync(context, default);
// Assert
Assert.Equal(new[] { "run", "--no-restore", "-f", "net5.0", "--", "foo=bar" }, context.ProcessSpec.Arguments);
}
[Fact]
public async Task ProcessAsync_AddsNoRestoreSwitch_ForTestCommand()
{
// Arrange
var filter = new NoRestoreFilter();
var context = new DotNetWatchContext
{
Iteration = 0,
ProcessSpec = new ProcessSpec
{
Arguments = new[] { "test", "--filter SomeFilter" },
}
};
await filter.ProcessAsync(context, default);
context.ChangedFile = "Program.cs";
context.Iteration++;
// Act
await filter.ProcessAsync(context, default);
// Assert
Assert.Equal(new[] { "test", "--no-restore", "--filter SomeFilter" }, context.ProcessSpec.Arguments);
}
[Fact]
public async Task ProcessAsync_DoesNotModifyArgumentsForUnknownCommands()
{
// Arrange
var filter = new NoRestoreFilter();
var arguments = new[] { "ef", "database", "update" };
var context = new DotNetWatchContext
{
Iteration = 0,
ProcessSpec = new ProcessSpec
{
Arguments = arguments,
}
};
await filter.ProcessAsync(context, default);
context.ChangedFile = "Program.cs";
context.Iteration++;
// Act
await filter.ProcessAsync(context, default);
// Assert
Assert.Same(arguments, context.ProcessSpec.Arguments);
}
}
}

View File

@ -39,6 +39,8 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
public AwaitableProcess Process { get; protected set; }
public List<string> DotnetWatchArgs { get; } = new List<string>();
public string SourceDirectory { get; }
public Task HasRestarted()
@ -86,6 +88,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
{
Scenario.DotNetWatchPath,
};
args.AddRange(DotnetWatchArgs);
args.AddRange(arguments);
var dotnetPath = "dotnet";

View File

@ -1,8 +1,9 @@
// 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.Diagnostics;
using System.Threading;
namespace KitchenSink
{
@ -15,6 +16,12 @@ namespace KitchenSink
Console.WriteLine($"Process identifier = {Process.GetCurrentProcess().Id}, {Process.GetCurrentProcess().StartTime:hh:mm:ss.FF}");
Console.WriteLine("DOTNET_WATCH = " + Environment.GetEnvironmentVariable("DOTNET_WATCH"));
Console.WriteLine("DOTNET_WATCH_ITERATION = " + Environment.GetEnvironmentVariable("DOTNET_WATCH_ITERATION"));
if (args.Length > 0 && args[0] == "wait")
{
Console.WriteLine("Waiting for process to be terminated.");
Thread.Sleep(Timeout.Infinite);
}
}
}
}