Refactor dotnet-watch to isolate project.json dependency
This commit is contained in:
parent
62df63ada8
commit
721cbe3435
|
|
@ -1,11 +1,11 @@
|
||||||
// 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.
|
// 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.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Microsoft.DotNet.Cli.Utils;
|
using Microsoft.DotNet.Cli.Utils;
|
||||||
using Microsoft.DotNet.Watcher.Tools;
|
using Microsoft.DotNet.Watcher.Tools;
|
||||||
|
using Microsoft.DotNet.Watcher.Internal;
|
||||||
using Microsoft.Extensions.CommandLineUtils;
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher
|
namespace Microsoft.DotNet.Watcher
|
||||||
|
|
@ -18,10 +18,7 @@ namespace Microsoft.DotNet.Watcher
|
||||||
public IList<string> RemainingArguments { get; private set; }
|
public IList<string> RemainingArguments { get; private set; }
|
||||||
public static CommandLineOptions Parse(string[] args, TextWriter stdout, TextWriter stderr)
|
public static CommandLineOptions Parse(string[] args, TextWriter stdout, TextWriter stderr)
|
||||||
{
|
{
|
||||||
if (args == null)
|
Ensure.NotNull(args, nameof(args));
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(args));
|
|
||||||
}
|
|
||||||
|
|
||||||
var app = new CommandLineApplication(throwOnUnexpectedArg: false)
|
var app = new CommandLineApplication(throwOnUnexpectedArg: false)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,12 +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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.DotNet.Cli.Utils;
|
|
||||||
using Microsoft.DotNet.Watcher.Internal;
|
using Microsoft.DotNet.Watcher.Internal;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|
@ -14,163 +10,68 @@ namespace Microsoft.DotNet.Watcher
|
||||||
{
|
{
|
||||||
public class DotNetWatcher
|
public class DotNetWatcher
|
||||||
{
|
{
|
||||||
private readonly Func<IFileWatcher> _fileWatcherFactory;
|
|
||||||
private readonly Func<IProcessWatcher> _processWatcherFactory;
|
|
||||||
private readonly IProjectProvider _projectProvider;
|
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private readonly ProcessRunner _processRunner;
|
||||||
|
|
||||||
public DotNetWatcher(
|
public DotNetWatcher(ILogger logger)
|
||||||
Func<IFileWatcher> fileWatcherFactory,
|
|
||||||
Func<IProcessWatcher> processWatcherFactory,
|
|
||||||
IProjectProvider projectProvider,
|
|
||||||
ILoggerFactory loggerFactory)
|
|
||||||
{
|
{
|
||||||
_fileWatcherFactory = fileWatcherFactory;
|
Ensure.NotNull(logger, nameof(logger));
|
||||||
_processWatcherFactory = processWatcherFactory;
|
|
||||||
_projectProvider = projectProvider;
|
|
||||||
_loggerFactory = loggerFactory;
|
|
||||||
|
|
||||||
_logger = _loggerFactory.CreateLogger(nameof(DotNetWatcher));
|
_logger = logger;
|
||||||
|
_processRunner = new ProcessRunner(logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task WatchAsync(string projectFile, IEnumerable<string> dotnetArguments, CancellationToken cancellationToken)
|
public async Task WatchAsync(ProcessSpec processSpec, IFileSetFactory fileSetFactory, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(projectFile))
|
Ensure.NotNull(processSpec, nameof(processSpec));
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(projectFile));
|
|
||||||
}
|
|
||||||
if (dotnetArguments == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(dotnetArguments));
|
|
||||||
}
|
|
||||||
if (cancellationToken == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(cancellationToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
var dotnetArgumentsAsString = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(dotnetArguments);
|
var cancelledTaskSource = new TaskCompletionSource<object>();
|
||||||
|
cancellationToken.Register(state => ((TaskCompletionSource<object>)state).TrySetResult(null), cancelledTaskSource);
|
||||||
var workingDir = Path.GetDirectoryName(projectFile);
|
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
await WaitForValidProjectJsonAsync(projectFile, cancellationToken);
|
var fileSet = await fileSetFactory.CreateAsync(cancellationToken);
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using (var currentRunCancellationSource = new CancellationTokenSource())
|
using (var currentRunCancellationSource = new CancellationTokenSource())
|
||||||
using (var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(
|
using (var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(
|
||||||
cancellationToken,
|
cancellationToken,
|
||||||
currentRunCancellationSource.Token))
|
currentRunCancellationSource.Token))
|
||||||
|
using (var fileSetWatcher = new FileSetWatcher(fileSet))
|
||||||
{
|
{
|
||||||
var fileWatchingTask = WaitForProjectFileToChangeAsync(projectFile, combinedCancellationSource.Token);
|
var fileSetTask = fileSetWatcher.GetChangedFileAsync(combinedCancellationSource.Token);
|
||||||
var dotnetTask = WaitForDotnetToExitAsync(dotnetArgumentsAsString, workingDir, combinedCancellationSource.Token);
|
var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token);
|
||||||
|
|
||||||
var tasksToWait = new Task[] { dotnetTask, fileWatchingTask };
|
var finishedTask = await Task.WhenAny(processTask, fileSetTask, cancelledTaskSource.Task);
|
||||||
|
|
||||||
int finishedTaskIndex = Task.WaitAny(tasksToWait, cancellationToken);
|
// Regardless of the which task finished first, make sure everything is cancelled
|
||||||
|
|
||||||
// Regardless of the outcome, make sure everything is cancelled
|
|
||||||
// and wait for dotnet to exit. We don't want orphan processes
|
// and wait for dotnet to exit. We don't want orphan processes
|
||||||
currentRunCancellationSource.Cancel();
|
currentRunCancellationSource.Cancel();
|
||||||
Task.WaitAll(tasksToWait);
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
await Task.WhenAll(processTask, fileSetTask);
|
||||||
|
|
||||||
string changedFile;
|
if (finishedTask == cancelledTaskSource.Task || cancellationToken.IsCancellationRequested)
|
||||||
if (finishedTaskIndex == 0)
|
|
||||||
{
|
|
||||||
// This is the dotnet task
|
|
||||||
var dotnetExitCode = dotnetTask.Result;
|
|
||||||
|
|
||||||
if (dotnetExitCode == 0)
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"dotnet exit code: {dotnetExitCode}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogError($"dotnet exit code: {dotnetExitCode}");
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Waiting for a file to change before restarting dotnet...");
|
|
||||||
// Now wait for a file to change before restarting dotnet
|
|
||||||
changedFile = await WaitForProjectFileToChangeAsync(projectFile, cancellationToken);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// This is a file watcher task
|
|
||||||
changedFile = fileWatchingTask.Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(changedFile))
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"File changed: {changedFile}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> WaitForProjectFileToChangeAsync(string projectFile, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using (var projectWatcher = CreateProjectWatcher(projectFile, watchProjectJsonOnly: false))
|
|
||||||
{
|
|
||||||
return await projectWatcher.WaitForChangeAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task<int> WaitForDotnetToExitAsync(string dotnetArguments, string workingDir, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_logger.LogDebug($"Running dotnet with the following arguments: {dotnetArguments}");
|
|
||||||
|
|
||||||
var dotnetWatcher = _processWatcherFactory();
|
|
||||||
int dotnetProcessId = dotnetWatcher.Start("dotnet", dotnetArguments, workingDir);
|
|
||||||
_logger.LogInformation($"dotnet process id: {dotnetProcessId}");
|
|
||||||
|
|
||||||
return dotnetWatcher.WaitForExitAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task WaitForValidProjectJsonAsync(string projectFile, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
IProject project;
|
|
||||||
string errors;
|
|
||||||
if (_projectProvider.TryReadProject(projectFile, out project, out errors))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogError($"Error(s) reading project file '{projectFile}': ");
|
|
||||||
_logger.LogError(errors);
|
|
||||||
_logger.LogInformation("Fix the error to continue.");
|
|
||||||
|
|
||||||
using (var projectWatcher = CreateProjectWatcher(projectFile, watchProjectJsonOnly: true))
|
|
||||||
{
|
|
||||||
await projectWatcher.WaitForChangeAsync(cancellationToken);
|
|
||||||
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation($"File changed: {projectFile}");
|
if (finishedTask == processTask)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Waiting for a file to change before restarting dotnet...");
|
||||||
|
|
||||||
|
// Now wait for a file to change before restarting process
|
||||||
|
await fileSetWatcher.GetChangedFileAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(fileSetTask.Result))
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"File changed: {fileSetTask.Result}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProjectWatcher CreateProjectWatcher(string projectFile, bool watchProjectJsonOnly)
|
|
||||||
{
|
|
||||||
return new ProjectWatcher(projectFile, watchProjectJsonOnly, _fileWatcherFactory, _projectProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DotNetWatcher CreateDefault(ILoggerFactory loggerFactory)
|
|
||||||
{
|
|
||||||
return new DotNetWatcher(
|
|
||||||
fileWatcherFactory: () => new FileWatcher(),
|
|
||||||
processWatcherFactory: () => new ProcessWatcher(),
|
|
||||||
projectProvider: new ProjectProvider(),
|
|
||||||
loggerFactory: loggerFactory);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Internal
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher
|
||||||
{
|
{
|
||||||
public interface IProjectProvider
|
public interface IFileSet : IEnumerable<string>
|
||||||
{
|
{
|
||||||
bool TryReadProject(string projectFile, out IProject project, out string errors);
|
bool Contains(string filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,13 @@
|
||||||
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Internal
|
namespace Microsoft.DotNet.Watcher
|
||||||
{
|
{
|
||||||
public interface IProcessWatcher
|
public interface IFileSetFactory
|
||||||
{
|
{
|
||||||
int Start(string executable, string arguments, string workingDir);
|
Task<IFileSet> CreateAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
Task<int> WaitForExitAsync(CancellationToken cancellationToken);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
// 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 Microsoft.DotNet.Watcher.Tools;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
internal static class Ensure
|
||||||
|
{
|
||||||
|
public static T NotNull<T>(T obj, string paramName)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(paramName);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NotNullOrEmpty(string obj, string paramName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(obj))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.Error_StringNullOrEmpty, paramName);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
public class FileSet : IFileSet
|
||||||
|
{
|
||||||
|
private readonly HashSet<string> _files;
|
||||||
|
|
||||||
|
public FileSet(IEnumerable<string> files)
|
||||||
|
{
|
||||||
|
_files = new HashSet<string>(files, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(string filePath) => _files.Contains(filePath);
|
||||||
|
|
||||||
|
public IEnumerator<string> GetEnumerator() => _files.GetEnumerator();
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => _files.GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
// 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.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
public class FileSetWatcher : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IFileWatcher _fileWatcher;
|
||||||
|
private readonly IFileSet _fileSet;
|
||||||
|
|
||||||
|
public FileSetWatcher(IFileSet fileSet)
|
||||||
|
{
|
||||||
|
_fileSet = fileSet;
|
||||||
|
_fileWatcher = new FileWatcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetChangedFileAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
foreach (var file in _fileSet)
|
||||||
|
{
|
||||||
|
_fileWatcher.WatchDirectory(Path.GetDirectoryName(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<string>();
|
||||||
|
cancellationToken.Register(() => tcs.TrySetResult(null));
|
||||||
|
|
||||||
|
Action<string> callback = path =>
|
||||||
|
{
|
||||||
|
if (_fileSet.Contains(path))
|
||||||
|
{
|
||||||
|
tcs.TrySetResult(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_fileWatcher.OnFileChange += callback;
|
||||||
|
var changedFile = await tcs.Task;
|
||||||
|
_fileWatcher.OnFileChange -= callback;
|
||||||
|
|
||||||
|
return changedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_fileWatcher.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,10 +22,8 @@ namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
|
||||||
internal DotnetFileWatcher(string watchedDirectory, Func<string, FileSystemWatcher> fileSystemWatcherFactory)
|
internal DotnetFileWatcher(string watchedDirectory, Func<string, FileSystemWatcher> fileSystemWatcherFactory)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(watchedDirectory))
|
Ensure.NotNull(fileSystemWatcherFactory, nameof(fileSystemWatcherFactory));
|
||||||
{
|
Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory));
|
||||||
throw new ArgumentNullException(nameof(watchedDirectory));
|
|
||||||
}
|
|
||||||
|
|
||||||
_watchedDirectory = watchedDirectory;
|
_watchedDirectory = watchedDirectory;
|
||||||
_watcherFactory = fileSystemWatcherFactory;
|
_watcherFactory = fileSystemWatcherFactory;
|
||||||
|
|
@ -38,10 +36,7 @@ namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
|
||||||
private static FileSystemWatcher DefaultWatcherFactory(string watchedDirectory)
|
private static FileSystemWatcher DefaultWatcherFactory(string watchedDirectory)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(watchedDirectory))
|
Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory));
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(watchedDirectory));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new FileSystemWatcher(watchedDirectory);
|
return new FileSystemWatcher(watchedDirectory);
|
||||||
}
|
}
|
||||||
|
|
@ -51,10 +46,7 @@ namespace Microsoft.DotNet.Watcher.Internal
|
||||||
// Recreate the watcher
|
// Recreate the watcher
|
||||||
CreateFileSystemWatcher();
|
CreateFileSystemWatcher();
|
||||||
|
|
||||||
if (OnError != null)
|
OnError?.Invoke(this, null);
|
||||||
{
|
|
||||||
OnError(this, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WatcherRenameHandler(object sender, RenamedEventArgs e)
|
private void WatcherRenameHandler(object sender, RenamedEventArgs e)
|
||||||
|
|
@ -81,11 +73,8 @@ namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
|
||||||
private void NotifyChange(string fullPath)
|
private void NotifyChange(string fullPath)
|
||||||
{
|
{
|
||||||
if (OnFileChange != null)
|
// Only report file changes
|
||||||
{
|
OnFileChange?.Invoke(this, fullPath);
|
||||||
// Only report file changes
|
|
||||||
OnFileChange(this, fullPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateFileSystemWatcher()
|
private void CreateFileSystemWatcher()
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,7 @@ namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
|
||||||
public PollingFileWatcher(string watchedDirectory)
|
public PollingFileWatcher(string watchedDirectory)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(watchedDirectory))
|
Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory));
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(watchedDirectory));
|
|
||||||
}
|
|
||||||
|
|
||||||
_watchedDirectory = new DirectoryInfo(watchedDirectory);
|
_watchedDirectory = new DirectoryInfo(watchedDirectory);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
// 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.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Internal
|
|
||||||
{
|
|
||||||
public interface IProject
|
|
||||||
{
|
|
||||||
string ProjectFile { get; }
|
|
||||||
|
|
||||||
IEnumerable<string> Files { get; }
|
|
||||||
|
|
||||||
IEnumerable<string> ProjectDependencies { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
// 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;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Internal;
|
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Internal
|
|
||||||
{
|
|
||||||
public class ProcessWatcher : IProcessWatcher
|
|
||||||
{
|
|
||||||
private Process _runningProcess;
|
|
||||||
|
|
||||||
public int Start(string executable, string arguments, string workingDir)
|
|
||||||
{
|
|
||||||
// This is not thread safe but it will not run in a multithreaded environment so don't worry
|
|
||||||
if (_runningProcess != null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("The previous process is still running");
|
|
||||||
}
|
|
||||||
|
|
||||||
_runningProcess = new Process();
|
|
||||||
_runningProcess.StartInfo = new ProcessStartInfo()
|
|
||||||
{
|
|
||||||
FileName = executable,
|
|
||||||
Arguments = arguments,
|
|
||||||
UseShellExecute = false,
|
|
||||||
WorkingDirectory = workingDir
|
|
||||||
};
|
|
||||||
|
|
||||||
_runningProcess.Start();
|
|
||||||
|
|
||||||
return _runningProcess.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<int> WaitForExitAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
cancellationToken.Register(() =>
|
|
||||||
{
|
|
||||||
if (_runningProcess != null)
|
|
||||||
{
|
|
||||||
_runningProcess.KillTree();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Task.Run(() =>
|
|
||||||
{
|
|
||||||
_runningProcess.WaitForExit();
|
|
||||||
|
|
||||||
var exitCode = _runningProcess.ExitCode;
|
|
||||||
_runningProcess = null;
|
|
||||||
|
|
||||||
return exitCode;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// 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.
|
// 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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.DotNet.ProjectModel.Files;
|
using Microsoft.DotNet.ProjectModel.Files;
|
||||||
|
|
@ -12,10 +11,7 @@ namespace Microsoft.DotNet.Watcher.Internal
|
||||||
{
|
{
|
||||||
public static IEnumerable<string> ResolveFiles(this IncludeContext context)
|
public static IEnumerable<string> ResolveFiles(this IncludeContext context)
|
||||||
{
|
{
|
||||||
if (context == null)
|
Ensure.NotNull(context, nameof(context));
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
return IncludeFilesResolver
|
return IncludeFilesResolver
|
||||||
.GetIncludeFiles(context, "/", diagnostics: null)
|
.GetIncludeFiles(context, "/", diagnostics: null)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
// 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;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Internal;
|
||||||
|
using Microsoft.DotNet.Cli.Utils;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
public class ProcessRunner
|
||||||
|
{
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public ProcessRunner(ILogger logger)
|
||||||
|
{
|
||||||
|
Ensure.NotNull(logger, nameof(logger));
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// May not be necessary in the future. See https://github.com/dotnet/corefx/issues/12039
|
||||||
|
public async Task<int> RunAsync(ProcessSpec processSpec, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Ensure.NotNull(processSpec, nameof(processSpec));
|
||||||
|
|
||||||
|
int exitCode;
|
||||||
|
|
||||||
|
using (var process = CreateProcess(processSpec))
|
||||||
|
using (var processState = new ProcessState(process))
|
||||||
|
{
|
||||||
|
cancellationToken.Register(() => processState.TryKill());
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
_logger.LogInformation("{execName} process id: {pid}", processSpec.ShortDisplayName(), process.Id);
|
||||||
|
|
||||||
|
await processState.Task;
|
||||||
|
|
||||||
|
exitCode = process.ExitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogResult(processSpec, exitCode);
|
||||||
|
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Process CreateProcess(ProcessSpec processSpec)
|
||||||
|
{
|
||||||
|
var arguments = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(processSpec.Arguments);
|
||||||
|
|
||||||
|
_logger.LogInformation("Running {execName} with the following arguments: {args}", processSpec.ShortDisplayName(), arguments);
|
||||||
|
|
||||||
|
var startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = processSpec.Executable,
|
||||||
|
Arguments = arguments,
|
||||||
|
UseShellExecute = false,
|
||||||
|
WorkingDirectory = processSpec.WorkingDirectory
|
||||||
|
};
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = startInfo,
|
||||||
|
EnableRaisingEvents = true
|
||||||
|
};
|
||||||
|
return process;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogResult(ProcessSpec processSpec, int exitCode)
|
||||||
|
{
|
||||||
|
var processName = processSpec.ShortDisplayName();
|
||||||
|
if (exitCode == 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("{execName} exit code: {code}", processName, exitCode);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogError("{execName} exit code: {code}", processName, exitCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ProcessState : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Process _process;
|
||||||
|
private readonly TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>();
|
||||||
|
private volatile bool _disposed;
|
||||||
|
|
||||||
|
public ProcessState(Process process)
|
||||||
|
{
|
||||||
|
_process = process;
|
||||||
|
_process.Exited += OnExited;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Task => _tcs.Task;
|
||||||
|
|
||||||
|
public void TryKill()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_process.HasExited)
|
||||||
|
{
|
||||||
|
_process.KillTree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExited(object sender, EventArgs args)
|
||||||
|
=> _tcs.TrySetResult(null);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
_disposed = true;
|
||||||
|
TryKill();
|
||||||
|
_process.Exited -= OnExited;
|
||||||
|
_process.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
// 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.
|
// 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.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.DotNet.ProjectModel.Files;
|
|
||||||
using Microsoft.DotNet.ProjectModel.Graph;
|
using Microsoft.DotNet.ProjectModel.Graph;
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Internal
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
{
|
{
|
||||||
internal class Project : IProject
|
public class Project
|
||||||
{
|
{
|
||||||
public Project(ProjectModel.Project runtimeProject)
|
public Project(ProjectModel.Project runtimeProject)
|
||||||
{
|
{
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
public class ProjectJsonFileSet : IFileSet
|
||||||
|
{
|
||||||
|
private readonly string _projectFile;
|
||||||
|
private ISet<string> _currentFiles;
|
||||||
|
|
||||||
|
public ProjectJsonFileSet(string projectFile)
|
||||||
|
{
|
||||||
|
_projectFile = projectFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(string filePath)
|
||||||
|
{
|
||||||
|
// if it was in the original list of files we were watching
|
||||||
|
if (_currentFiles?.Contains(filePath) == true)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's possible the new file was not in the old set but will be in the new set.
|
||||||
|
// Additions should be considered part of this.
|
||||||
|
RefreshFileList();
|
||||||
|
|
||||||
|
return _currentFiles.Contains(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<string> GetEnumerator()
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
return _currentFiles.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
return _currentFiles.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureInitialized()
|
||||||
|
{
|
||||||
|
if (_currentFiles == null)
|
||||||
|
{
|
||||||
|
RefreshFileList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshFileList()
|
||||||
|
{
|
||||||
|
_currentFiles = new HashSet<string>(FindFiles(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<string> FindFiles()
|
||||||
|
{
|
||||||
|
var projects = new HashSet<string>(); // temporary store to prevent re-parsing a project multiple times
|
||||||
|
return GetProjectFilesClosure(_projectFile, projects);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<string> GetProjectFilesClosure(string projectFile, ISet<string> projects)
|
||||||
|
{
|
||||||
|
if (projects.Contains(projectFile))
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
projects.Add(projectFile);
|
||||||
|
|
||||||
|
Project project;
|
||||||
|
string errors;
|
||||||
|
|
||||||
|
if (ProjectReader.TryReadProject(projectFile, out project, out errors))
|
||||||
|
{
|
||||||
|
foreach (var file in project.Files)
|
||||||
|
{
|
||||||
|
yield return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dependency in project.ProjectDependencies)
|
||||||
|
{
|
||||||
|
foreach (var file in GetProjectFilesClosure(dependency, projects))
|
||||||
|
{
|
||||||
|
yield return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
// 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;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
public class ProjectJsonFileSetFactory : IFileSetFactory
|
||||||
|
{
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly string _projectFile;
|
||||||
|
public ProjectJsonFileSetFactory(ILogger logger, string projectFile)
|
||||||
|
{
|
||||||
|
Ensure.NotNull(logger, nameof(logger));
|
||||||
|
Ensure.NotNullOrEmpty(projectFile, nameof(projectFile));
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
|
_projectFile = projectFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IFileSet> CreateAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
Project project;
|
||||||
|
string errors;
|
||||||
|
if (ProjectReader.TryReadProject(_projectFile, out project, out errors))
|
||||||
|
{
|
||||||
|
return new ProjectJsonFileSet(_projectFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogError($"Error(s) reading project file '{_projectFile}': ");
|
||||||
|
_logger.LogError(errors);
|
||||||
|
_logger.LogInformation("Fix the error to continue.");
|
||||||
|
|
||||||
|
var fileSet = new FileSet(new[] { _projectFile });
|
||||||
|
|
||||||
|
using (var watcher = new FileSetWatcher(fileSet))
|
||||||
|
{
|
||||||
|
await watcher.GetChangedFileAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation($"File changed: {_projectFile}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,16 +2,14 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.DotNet.ProjectModel;
|
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Internal
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
{
|
{
|
||||||
public class ProjectProvider : IProjectProvider
|
public class ProjectReader
|
||||||
{
|
{
|
||||||
public bool TryReadProject(string projectFile, out IProject project, out string errors)
|
public static bool TryReadProject(string projectFile, out Project project, out string errors)
|
||||||
{
|
{
|
||||||
errors = null;
|
errors = null;
|
||||||
project = null;
|
project = null;
|
||||||
|
|
@ -35,12 +33,11 @@ namespace Microsoft.DotNet.Watcher.Internal
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same as TryGetProject but it doesn't throw
|
private static bool TryGetProject(string projectFile, out ProjectModel.Project project, out string errorMessage)
|
||||||
private bool TryGetProject(string projectFile, out ProjectModel.Project project, out string errorMessage)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!ProjectReader.TryGetProject(projectFile, out project))
|
if (!ProjectModel.ProjectReader.TryGetProject(projectFile, out project))
|
||||||
{
|
{
|
||||||
if (project?.Diagnostics != null && project.Diagnostics.Any())
|
if (project?.Diagnostics != null && project.Diagnostics.Any())
|
||||||
{
|
{
|
||||||
|
|
@ -66,7 +63,7 @@ namespace Microsoft.DotNet.Watcher.Internal
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string CollectMessages(Exception exception)
|
private static string CollectMessages(Exception exception)
|
||||||
{
|
{
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
builder.AppendLine(exception.Message);
|
builder.AppendLine(exception.Message);
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
// 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.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Internal
|
|
||||||
{
|
|
||||||
public class ProjectWatcher : IDisposable
|
|
||||||
{
|
|
||||||
private readonly IProjectProvider _projectProvider;
|
|
||||||
private readonly IFileWatcher _fileWatcher;
|
|
||||||
|
|
||||||
private readonly string _rootProject;
|
|
||||||
private readonly bool _watchProjectJsonOnly;
|
|
||||||
|
|
||||||
private ISet<string> _watchedFiles;
|
|
||||||
|
|
||||||
public ProjectWatcher(
|
|
||||||
string projectToWatch,
|
|
||||||
bool watchProjectJsonOnly,
|
|
||||||
Func<IFileWatcher> fileWatcherFactory,
|
|
||||||
IProjectProvider projectProvider)
|
|
||||||
{
|
|
||||||
_projectProvider = projectProvider;
|
|
||||||
_fileWatcher = fileWatcherFactory();
|
|
||||||
|
|
||||||
_rootProject = projectToWatch;
|
|
||||||
_watchProjectJsonOnly = watchProjectJsonOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> WaitForChangeAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_watchedFiles = GetProjectFilesClosure(_rootProject);
|
|
||||||
|
|
||||||
foreach (var file in _watchedFiles)
|
|
||||||
{
|
|
||||||
_fileWatcher.WatchDirectory(Path.GetDirectoryName(file));
|
|
||||||
}
|
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource<string>();
|
|
||||||
cancellationToken.Register(() => tcs.TrySetResult(null));
|
|
||||||
|
|
||||||
Action<string> callback = path =>
|
|
||||||
{
|
|
||||||
// If perf becomes a problem, this could be a good starting point
|
|
||||||
// because it reparses the project on every change
|
|
||||||
// Maybe it could time-buffer the changes in case there are a lot
|
|
||||||
// of files changed at the same time
|
|
||||||
if (IsFileInTheWatchedSet(path))
|
|
||||||
{
|
|
||||||
tcs.TrySetResult(path);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_fileWatcher.OnFileChange += callback;
|
|
||||||
var changedFile = await tcs.Task;
|
|
||||||
_fileWatcher.OnFileChange -= callback;
|
|
||||||
|
|
||||||
return changedFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_fileWatcher?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsFileInTheWatchedSet(string file)
|
|
||||||
{
|
|
||||||
// If the file was already watched
|
|
||||||
// or if the new project file closure determined
|
|
||||||
// by file globbing patterns contains the new file
|
|
||||||
// Note, we cannot simply rebuild the closure every time because it wouldn't
|
|
||||||
// detect renamed files that have the new name outside of the closure
|
|
||||||
return
|
|
||||||
_watchedFiles.Contains(file) ||
|
|
||||||
GetProjectFilesClosure(_rootProject).Contains(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ISet<string> GetProjectFilesClosure(string projectFile)
|
|
||||||
{
|
|
||||||
var closure = new HashSet<string>();
|
|
||||||
|
|
||||||
if (_watchProjectJsonOnly)
|
|
||||||
{
|
|
||||||
closure.Add(projectFile);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
GetProjectFilesClosure(projectFile, closure);
|
|
||||||
}
|
|
||||||
return closure;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void GetProjectFilesClosure(string projectFile, ISet<string> closure)
|
|
||||||
{
|
|
||||||
closure.Add(projectFile);
|
|
||||||
|
|
||||||
IProject project;
|
|
||||||
string errors;
|
|
||||||
|
|
||||||
if (_projectProvider.TryReadProject(projectFile, out project, out errors))
|
|
||||||
{
|
|
||||||
foreach (var file in project.Files)
|
|
||||||
{
|
|
||||||
closure.Add(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var dependency in project.ProjectDependencies)
|
|
||||||
{
|
|
||||||
GetProjectFilesClosure(dependency, closure);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||||
<PropertyGroup Label="Globals">
|
<PropertyGroup Label="Globals">
|
||||||
<ProjectGuid>8a8ceabc-ac47-43ff-a5df-69224f7e1f46</ProjectGuid>
|
<ProjectGuid>8a8ceabc-ac47-43ff-a5df-69224f7e1f46</ProjectGuid>
|
||||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||||
|
|
@ -16,5 +16,5 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<SchemaVersion>2.0</SchemaVersion>
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher
|
||||||
|
{
|
||||||
|
public class ProcessSpec
|
||||||
|
{
|
||||||
|
public string Executable { get; set; }
|
||||||
|
public string WorkingDirectory { get; set; }
|
||||||
|
public IEnumerable<string> Arguments { get; set; }
|
||||||
|
|
||||||
|
public string ShortDisplayName()
|
||||||
|
=> Path.GetFileNameWithoutExtension(Executable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,29 +5,23 @@ using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.DotNet.Cli.Utils;
|
using Microsoft.DotNet.Cli.Utils;
|
||||||
|
using Microsoft.DotNet.Watcher.Internal;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher
|
namespace Microsoft.DotNet.Watcher
|
||||||
{
|
{
|
||||||
public class Program
|
public class Program
|
||||||
{
|
{
|
||||||
private readonly ILoggerFactory _loggerFactory = new LoggerFactory();
|
private const string LoggerName = "DotNetWatcher";
|
||||||
private readonly CancellationToken _cancellationToken;
|
private readonly CancellationToken _cancellationToken;
|
||||||
private readonly TextWriter _stdout;
|
private readonly TextWriter _stdout;
|
||||||
private readonly TextWriter _stderr;
|
private readonly TextWriter _stderr;
|
||||||
|
|
||||||
public Program(TextWriter consoleOutput, TextWriter consoleError, CancellationToken cancellationToken)
|
public Program(TextWriter consoleOutput, TextWriter consoleError, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (consoleOutput == null)
|
Ensure.NotNull(consoleOutput, nameof(consoleOutput));
|
||||||
{
|
Ensure.NotNull(consoleError, nameof(consoleError));
|
||||||
throw new ArgumentNullException(nameof(consoleOutput));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancellationToken == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(cancellationToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
_cancellationToken = cancellationToken;
|
_cancellationToken = cancellationToken;
|
||||||
_stdout = consoleOutput;
|
_stdout = consoleOutput;
|
||||||
|
|
@ -36,28 +30,43 @@ namespace Microsoft.DotNet.Watcher
|
||||||
|
|
||||||
public static int Main(string[] args)
|
public static int Main(string[] args)
|
||||||
{
|
{
|
||||||
|
DebugHelper.HandleDebugSwitch(ref args);
|
||||||
|
|
||||||
using (CancellationTokenSource ctrlCTokenSource = new CancellationTokenSource())
|
using (CancellationTokenSource ctrlCTokenSource = new CancellationTokenSource())
|
||||||
{
|
{
|
||||||
Console.CancelKeyPress += (sender, ev) =>
|
Console.CancelKeyPress += (sender, ev) =>
|
||||||
{
|
{
|
||||||
|
if (!ctrlCTokenSource.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[{LoggerName}] Shutdown requested. Press CTRL+C again to force exit.");
|
||||||
|
ev.Cancel = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ev.Cancel = false;
|
||||||
|
}
|
||||||
ctrlCTokenSource.Cancel();
|
ctrlCTokenSource.Cancel();
|
||||||
ev.Cancel = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
int exitCode;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
exitCode = new Program(Console.Out, Console.Error, ctrlCTokenSource.Token)
|
return new Program(Console.Out, Console.Error, ctrlCTokenSource.Token)
|
||||||
.MainInternalAsync(args)
|
.MainInternalAsync(args)
|
||||||
.GetAwaiter()
|
.GetAwaiter()
|
||||||
.GetResult();
|
.GetResult();
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// swallow when only exception is the CTRL+C exit cancellation task
|
if (ex is TaskCanceledException || ex is OperationCanceledException)
|
||||||
exitCode = 0;
|
{
|
||||||
|
// swallow when only exception is the CTRL+C forced an exit
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(ex.ToString());
|
||||||
|
Console.Error.WriteLine($"[{LoggerName}] An unexpected error occurred".Bold().Red());
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
return exitCode;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,17 +84,25 @@ namespace Microsoft.DotNet.Watcher
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var loggerFactory = new LoggerFactory();
|
||||||
var commandProvider = new CommandOutputProvider
|
var commandProvider = new CommandOutputProvider
|
||||||
{
|
{
|
||||||
LogLevel = ResolveLogLevel(options)
|
LogLevel = ResolveLogLevel(options)
|
||||||
};
|
};
|
||||||
_loggerFactory.AddProvider(commandProvider);
|
loggerFactory.AddProvider(commandProvider);
|
||||||
|
var logger = loggerFactory.CreateLogger(LoggerName);
|
||||||
|
|
||||||
var projectToWatch = Path.Combine(Directory.GetCurrentDirectory(), ProjectModel.Project.FileName);
|
var projectFile = Path.Combine(Directory.GetCurrentDirectory(), ProjectModel.Project.FileName);
|
||||||
|
var projectFileSetFactory = new ProjectJsonFileSetFactory(logger, projectFile);
|
||||||
|
var processInfo = new ProcessSpec
|
||||||
|
{
|
||||||
|
Executable = new Muxer().MuxerPath,
|
||||||
|
WorkingDirectory = Path.GetDirectoryName(projectFile),
|
||||||
|
Arguments = options.RemainingArguments
|
||||||
|
};
|
||||||
|
|
||||||
await DotNetWatcher
|
await new DotNetWatcher(logger)
|
||||||
.CreateDefault(_loggerFactory)
|
.WatchAsync(processInfo, projectFileSetFactory, _cancellationToken);
|
||||||
.WatchAsync(projectToWatch, options.RemainingArguments, _cancellationToken);
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -97,7 +114,7 @@ namespace Microsoft.DotNet.Watcher
|
||||||
return LogLevel.Warning;
|
return LogLevel.Warning;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool globalVerbose;
|
bool globalVerbose;
|
||||||
bool.TryParse(Environment.GetEnvironmentVariable(CommandContext.Variables.Verbose), out globalVerbose);
|
bool.TryParse(Environment.GetEnvironmentVariable(CommandContext.Variables.Verbose), out globalVerbose);
|
||||||
|
|
||||||
if (options.IsVerbose // dotnet watch --verbose
|
if (options.IsVerbose // dotnet watch --verbose
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,22 @@ namespace Microsoft.DotNet.Watcher.Tools
|
||||||
return GetString("Error_QuietAndVerboseSpecified");
|
return GetString("Error_QuietAndVerboseSpecified");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Value cannot be null or an empty string.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Error_StringNullOrEmpty
|
||||||
|
{
|
||||||
|
get { return GetString("Error_StringNullOrEmpty"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Value cannot be null or an empty string.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatError_StringNullOrEmpty()
|
||||||
|
{
|
||||||
|
return GetString("Error_StringNullOrEmpty");
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetString(string name, params string[] formatterNames)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -120,4 +120,7 @@
|
||||||
<data name="Error_QuietAndVerboseSpecified" xml:space="preserve">
|
<data name="Error_QuietAndVerboseSpecified" xml:space="preserve">
|
||||||
<value>Cannot specify both '--quiet' and '--verbose' options.</value>
|
<value>Cannot specify both '--quiet' and '--verbose' options.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Error_StringNullOrEmpty" xml:space="preserve">
|
||||||
|
<value>Value cannot be null or an empty string.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -5,14 +5,11 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
|
||||||
using System.Xml.Linq;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using Microsoft.DotNet.Cli.Utils;
|
|
||||||
using Microsoft.Extensions.DependencyModel;
|
|
||||||
using Microsoft.DotNet.ProjectModel;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.DotNet.Cli.Utils;
|
||||||
|
using Microsoft.DotNet.ProjectModel;
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue