Refactor dotnet-watch to isolate project.json dependency

This commit is contained in:
Nate McMaster 2016-10-12 17:34:59 -07:00
Родитель 62df63ada8
Коммит 721cbe3435
25 изменённых файлов: 520 добавлений и 411 удалений

Просмотреть файл

@ -1,11 +1,11 @@
// 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 Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Watcher.Tools;
using Microsoft.DotNet.Watcher.Internal;
using Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.DotNet.Watcher
@ -18,10 +18,7 @@ namespace Microsoft.DotNet.Watcher
public IList<string> RemainingArguments { get; private set; }
public static CommandLineOptions Parse(string[] args, TextWriter stdout, TextWriter stderr)
{
if (args == null)
{
throw new ArgumentNullException(nameof(args));
}
Ensure.NotNull(args, nameof(args));
var app = new CommandLineApplication(throwOnUnexpectedArg: false)
{

Просмотреть файл

@ -1,12 +1,8 @@
// 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;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Watcher.Internal;
using Microsoft.Extensions.Logging;
@ -14,163 +10,68 @@ namespace Microsoft.DotNet.Watcher
{
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 ProcessRunner _processRunner;
public DotNetWatcher(
Func<IFileWatcher> fileWatcherFactory,
Func<IProcessWatcher> processWatcherFactory,
IProjectProvider projectProvider,
ILoggerFactory loggerFactory)
public DotNetWatcher(ILogger logger)
{
_fileWatcherFactory = fileWatcherFactory;
_processWatcherFactory = processWatcherFactory;
_projectProvider = projectProvider;
_loggerFactory = loggerFactory;
Ensure.NotNull(logger, nameof(logger));
_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))
{
throw new ArgumentNullException(nameof(projectFile));
}
if (dotnetArguments == null)
{
throw new ArgumentNullException(nameof(dotnetArguments));
}
if (cancellationToken == null)
{
throw new ArgumentNullException(nameof(cancellationToken));
}
Ensure.NotNull(processSpec, nameof(processSpec));
var dotnetArgumentsAsString = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(dotnetArguments);
var workingDir = Path.GetDirectoryName(projectFile);
var cancelledTaskSource = new TaskCompletionSource<object>();
cancellationToken.Register(state => ((TaskCompletionSource<object>)state).TrySetResult(null), cancelledTaskSource);
while (true)
{
await WaitForValidProjectJsonAsync(projectFile, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
var fileSet = await fileSetFactory.CreateAsync(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
using (var currentRunCancellationSource = new CancellationTokenSource())
using (var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
currentRunCancellationSource.Token))
using (var fileSetWatcher = new FileSetWatcher(fileSet))
{
var fileWatchingTask = WaitForProjectFileToChangeAsync(projectFile, combinedCancellationSource.Token);
var dotnetTask = WaitForDotnetToExitAsync(dotnetArgumentsAsString, workingDir, combinedCancellationSource.Token);
var fileSetTask = fileSetWatcher.GetChangedFileAsync(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 outcome, make sure everything is cancelled
// Regardless of the which task finished first, make sure everything is cancelled
// and wait for dotnet to exit. We don't want orphan processes
currentRunCancellationSource.Cancel();
Task.WaitAll(tasksToWait);
cancellationToken.ThrowIfCancellationRequested();
await Task.WhenAll(processTask, fileSetTask);
string changedFile;
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)
if (finishedTask == cancelledTaskSource.Task || cancellationToken.IsCancellationRequested)
{
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.
// 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.
// 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.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<int> WaitForExitAsync(CancellationToken cancellationToken);
Task<IFileSet> CreateAsync(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)
{
if (string.IsNullOrEmpty(watchedDirectory))
{
throw new ArgumentNullException(nameof(watchedDirectory));
}
Ensure.NotNull(fileSystemWatcherFactory, nameof(fileSystemWatcherFactory));
Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory));
_watchedDirectory = watchedDirectory;
_watcherFactory = fileSystemWatcherFactory;
@ -38,10 +36,7 @@ namespace Microsoft.DotNet.Watcher.Internal
private static FileSystemWatcher DefaultWatcherFactory(string watchedDirectory)
{
if (string.IsNullOrEmpty(watchedDirectory))
{
throw new ArgumentNullException(nameof(watchedDirectory));
}
Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory));
return new FileSystemWatcher(watchedDirectory);
}
@ -51,10 +46,7 @@ namespace Microsoft.DotNet.Watcher.Internal
// Recreate the watcher
CreateFileSystemWatcher();
if (OnError != null)
{
OnError(this, null);
}
OnError?.Invoke(this, null);
}
private void WatcherRenameHandler(object sender, RenamedEventArgs e)
@ -81,11 +73,8 @@ namespace Microsoft.DotNet.Watcher.Internal
private void NotifyChange(string fullPath)
{
if (OnFileChange != null)
{
// Only report file changes
OnFileChange(this, fullPath);
}
// Only report file changes
OnFileChange?.Invoke(this, fullPath);
}
private void CreateFileSystemWatcher()

Просмотреть файл

@ -27,10 +27,7 @@ namespace Microsoft.DotNet.Watcher.Internal
public PollingFileWatcher(string watchedDirectory)
{
if (string.IsNullOrEmpty(watchedDirectory))
{
throw new ArgumentNullException(nameof(watchedDirectory));
}
Ensure.NotNullOrEmpty(watchedDirectory, nameof(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.
// 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 Microsoft.DotNet.ProjectModel.Files;
@ -12,10 +11,7 @@ namespace Microsoft.DotNet.Watcher.Internal
{
public static IEnumerable<string> ResolveFiles(this IncludeContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
Ensure.NotNull(context, nameof(context));
return IncludeFilesResolver
.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.
// 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 Microsoft.DotNet.ProjectModel.Files;
using Microsoft.DotNet.ProjectModel.Graph;
namespace Microsoft.DotNet.Watcher.Internal
{
internal class Project : IProject
public class Project
{
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.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.DotNet.ProjectModel;
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;
project = null;
@ -35,12 +33,11 @@ namespace Microsoft.DotNet.Watcher.Internal
return true;
}
// Same as TryGetProject but it doesn't throw
private bool TryGetProject(string projectFile, out ProjectModel.Project project, out string errorMessage)
private static bool TryGetProject(string projectFile, out ProjectModel.Project project, out string errorMessage)
{
try
{
if (!ProjectReader.TryGetProject(projectFile, out project))
if (!ProjectModel.ProjectReader.TryGetProject(projectFile, out project))
{
if (project?.Diagnostics != null && project.Diagnostics.Any())
{
@ -66,7 +63,7 @@ namespace Microsoft.DotNet.Watcher.Internal
return false;
}
private string CollectMessages(Exception exception)
private static string CollectMessages(Exception exception)
{
var builder = new StringBuilder();
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>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>8a8ceabc-ac47-43ff-a5df-69224f7e1f46</ProjectGuid>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
@ -16,5 +16,5 @@
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</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.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Watcher.Internal;
using Microsoft.Extensions.Logging;
namespace Microsoft.DotNet.Watcher
{
public class Program
{
private readonly ILoggerFactory _loggerFactory = new LoggerFactory();
private const string LoggerName = "DotNetWatcher";
private readonly CancellationToken _cancellationToken;
private readonly TextWriter _stdout;
private readonly TextWriter _stderr;
public Program(TextWriter consoleOutput, TextWriter consoleError, CancellationToken cancellationToken)
{
if (consoleOutput == null)
{
throw new ArgumentNullException(nameof(consoleOutput));
}
if (cancellationToken == null)
{
throw new ArgumentNullException(nameof(cancellationToken));
}
Ensure.NotNull(consoleOutput, nameof(consoleOutput));
Ensure.NotNull(consoleError, nameof(consoleError));
_cancellationToken = cancellationToken;
_stdout = consoleOutput;
@ -36,28 +30,43 @@ namespace Microsoft.DotNet.Watcher
public static int Main(string[] args)
{
DebugHelper.HandleDebugSwitch(ref args);
using (CancellationTokenSource ctrlCTokenSource = new CancellationTokenSource())
{
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();
ev.Cancel = false;
};
int exitCode;
try
{
exitCode = new Program(Console.Out, Console.Error, ctrlCTokenSource.Token)
return new Program(Console.Out, Console.Error, ctrlCTokenSource.Token)
.MainInternalAsync(args)
.GetAwaiter()
.GetResult();
}
catch (TaskCanceledException)
catch (Exception ex)
{
// swallow when only exception is the CTRL+C exit cancellation task
exitCode = 0;
if (ex is TaskCanceledException || ex is OperationCanceledException)
{
// 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;
}
var loggerFactory = new LoggerFactory();
var commandProvider = new CommandOutputProvider
{
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
.CreateDefault(_loggerFactory)
.WatchAsync(projectToWatch, options.RemainingArguments, _cancellationToken);
await new DotNetWatcher(logger)
.WatchAsync(processInfo, projectFileSetFactory, _cancellationToken);
return 0;
}
@ -97,7 +114,7 @@ namespace Microsoft.DotNet.Watcher
return LogLevel.Warning;
}
bool globalVerbose;
bool globalVerbose;
bool.TryParse(Environment.GetEnvironmentVariable(CommandContext.Variables.Verbose), out globalVerbose);
if (options.IsVerbose // dotnet watch --verbose

Просмотреть файл

@ -26,6 +26,22 @@ namespace Microsoft.DotNet.Watcher.Tools
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)
{
var value = _resourceManager.GetString(name);

Просмотреть файл

@ -120,4 +120,7 @@
<data name="Error_QuietAndVerboseSpecified" xml:space="preserve">
<value>Cannot specify both '--quiet' and '--verbose' options.</value>
</data>
<data name="Error_StringNullOrEmpty" xml:space="preserve">
<value>Value cannot be null or an empty string.</value>
</data>
</root>

Просмотреть файл

@ -5,14 +5,11 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
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.Reflection;
using System.Threading;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.ProjectModel;
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
{