diff --git a/dotnet-watch.sln b/dotnet-watch.sln index 3c8e1e6..c35ff1f 100644 --- a/dotnet-watch.sln +++ b/dotnet-watch.sln @@ -15,6 +15,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution NuGet.Config = NuGet.Config EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F5B382BC-258F-46E1-AC3D-10E5CCD55134}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "dotnet-watch.FunctionalTests", "test\dotnet-watch.FunctionalTests\dotnet-watch.FunctionalTests.xproj", "{16BADE2F-1184-4518-8A70-B68A19D0805B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestApps", "TestApps", "{2876B12E-5841-4792-85A8-2929AEE11885}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "NoDepsApp", "test\TestApps\NoDepsApp\NoDepsApp.xproj", "{4F0D8A80-221F-4BCB-822E-44A0655F537E}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "GlobbingApp", "test\TestApps\GlobbingApp\GlobbingApp.xproj", "{2AB1A28B-2022-49EA-AF77-AC8A875915CC}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "AppWithDeps", "test\TestApps\AppWithDeps\AppWithDeps.xproj", "{F7734E61-F510-41E0-AD15-301A64081CD1}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Dependency", "test\TestApps\Dependency\Dependency.xproj", "{2F48041A-F7D1-478F-9C38-D41F0F05E8CA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,6 +43,26 @@ Global {D3DA3BBB-E206-404F-AEE6-17FB9B6F1221}.Debug|Any CPU.Build.0 = Debug|Any CPU {D3DA3BBB-E206-404F-AEE6-17FB9B6F1221}.Release|Any CPU.ActiveCfg = Release|Any CPU {D3DA3BBB-E206-404F-AEE6-17FB9B6F1221}.Release|Any CPU.Build.0 = Release|Any CPU + {16BADE2F-1184-4518-8A70-B68A19D0805B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16BADE2F-1184-4518-8A70-B68A19D0805B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16BADE2F-1184-4518-8A70-B68A19D0805B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16BADE2F-1184-4518-8A70-B68A19D0805B}.Release|Any CPU.Build.0 = Release|Any CPU + {4F0D8A80-221F-4BCB-822E-44A0655F537E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F0D8A80-221F-4BCB-822E-44A0655F537E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F0D8A80-221F-4BCB-822E-44A0655F537E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F0D8A80-221F-4BCB-822E-44A0655F537E}.Release|Any CPU.Build.0 = Release|Any CPU + {2AB1A28B-2022-49EA-AF77-AC8A875915CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2AB1A28B-2022-49EA-AF77-AC8A875915CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2AB1A28B-2022-49EA-AF77-AC8A875915CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2AB1A28B-2022-49EA-AF77-AC8A875915CC}.Release|Any CPU.Build.0 = Release|Any CPU + {F7734E61-F510-41E0-AD15-301A64081CD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7734E61-F510-41E0-AD15-301A64081CD1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7734E61-F510-41E0-AD15-301A64081CD1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7734E61-F510-41E0-AD15-301A64081CD1}.Release|Any CPU.Build.0 = Release|Any CPU + {2F48041A-F7D1-478F-9C38-D41F0F05E8CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F48041A-F7D1-478F-9C38-D41F0F05E8CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F48041A-F7D1-478F-9C38-D41F0F05E8CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F48041A-F7D1-478F-9C38-D41F0F05E8CA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -36,5 +70,11 @@ Global GlobalSection(NestedProjects) = preSolution {8A8CEABC-AC47-43FF-A5DF-69224F7E1F46} = {66517987-2A5A-4330-B130-207039378FD4} {D3DA3BBB-E206-404F-AEE6-17FB9B6F1221} = {66517987-2A5A-4330-B130-207039378FD4} + {16BADE2F-1184-4518-8A70-B68A19D0805B} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134} + {2876B12E-5841-4792-85A8-2929AEE11885} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134} + {4F0D8A80-221F-4BCB-822E-44A0655F537E} = {2876B12E-5841-4792-85A8-2929AEE11885} + {2AB1A28B-2022-49EA-AF77-AC8A875915CC} = {2876B12E-5841-4792-85A8-2929AEE11885} + {F7734E61-F510-41E0-AD15-301A64081CD1} = {2876B12E-5841-4792-85A8-2929AEE11885} + {2F48041A-F7D1-478F-9C38-D41F0F05E8CA} = {2876B12E-5841-4792-85A8-2929AEE11885} EndGlobalSection EndGlobal diff --git a/src/Microsoft.DotNet.Watcher.Core/DictionaryExtensions.cs b/src/Microsoft.DotNet.Watcher.Core/DictionaryExtensions.cs deleted file mode 100644 index 0b318da..0000000 --- a/src/Microsoft.DotNet.Watcher.Core/DictionaryExtensions.cs +++ /dev/null @@ -1,23 +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. - -namespace System.Collections.Generic -{ - internal static class DictionaryExtensions - { - public static TValue GetOrAdd(this IDictionary dictionary, TKey key, Func factory) - { - lock (dictionary) - { - TValue value; - if (!dictionary.TryGetValue(key, out value)) - { - value = factory(key); - dictionary[key] = value; - } - - return value; - } - } - } -} diff --git a/src/Microsoft.DotNet.Watcher.Core/DotNetWatcher.cs b/src/Microsoft.DotNet.Watcher.Core/DotNetWatcher.cs index f105727..9ef05d2 100644 --- a/src/Microsoft.DotNet.Watcher.Core/DotNetWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Core/DotNetWatcher.cs @@ -2,17 +2,17 @@ // 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.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.DotNet.Watcher.Core.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watcher.Core { public class DotNetWatcher { - private readonly Func _fileWatcherFactory; + private readonly Func _fileWatcherFactory; private readonly Func _processWatcherFactory; private readonly IProjectProvider _projectProvider; private readonly ILoggerFactory _loggerFactory; @@ -22,7 +22,7 @@ namespace Microsoft.DotNet.Watcher.Core public bool ExitOnChange { get; set; } public DotNetWatcher( - Func fileWatcherFactory, + Func fileWatcherFactory, Func processWatcherFactory, IProjectProvider projectProvider, ILoggerFactory loggerFactory) @@ -86,7 +86,7 @@ namespace Microsoft.DotNet.Watcher.Core while (true) { - var project = await WaitForValidProjectJsonAsync(projectFile, cancellationToken); + await WaitForValidProjectJsonAsync(projectFile, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); using (var currentRunCancellationSource = new CancellationTokenSource()) @@ -94,7 +94,7 @@ namespace Microsoft.DotNet.Watcher.Core cancellationToken, currentRunCancellationSource.Token)) { - var fileWatchingTask = WaitForProjectFileToChangeAsync(project, combinedCancellationSource.Token); + var fileWatchingTask = WaitForProjectFileToChangeAsync(projectFile, combinedCancellationSource.Token); var dotnetTask = WaitForDotnetToExitAsync(dotnetArgumentsAsString, workingDir, combinedCancellationSource.Token); var tasksToWait = new Task[] { dotnetTask, fileWatchingTask }; @@ -129,7 +129,7 @@ namespace Microsoft.DotNet.Watcher.Core _logger.LogInformation("Waiting for a file to change before restarting dotnet..."); // Now wait for a file to change before restarting dotnet - await WaitForProjectFileToChangeAsync(project, cancellationToken); + await WaitForProjectFileToChangeAsync(projectFile, cancellationToken); } else { @@ -146,12 +146,11 @@ namespace Microsoft.DotNet.Watcher.Core } } - private async Task WaitForProjectFileToChangeAsync(IProject project, CancellationToken cancellationToken) + private async Task WaitForProjectFileToChangeAsync(string projectFile, CancellationToken cancellationToken) { - using (var fileWatcher = _fileWatcherFactory(Path.GetDirectoryName(project.ProjectFile))) + using (var projectWatcher = CreateProjectWatcher(projectFile, watchProjectJsonOnly: false)) { - AddProjectAndDependeciesToWatcher(project, fileWatcher); - return await WatchForFileChangeAsync(fileWatcher, cancellationToken); + return await projectWatcher.WaitForChangeAsync(cancellationToken); } } @@ -161,37 +160,33 @@ namespace Microsoft.DotNet.Watcher.Core var dotnetWatcher = _processWatcherFactory(); int dotnetProcessId = dotnetWatcher.Start("dotnet", dotnetArguments, workingDir); - _logger.LogInformation($"dotnet run process id: {dotnetProcessId}"); + _logger.LogInformation($"dotnet process id: {dotnetProcessId}"); return dotnetWatcher.WaitForExitAsync(cancellationToken); } - private async Task WaitForValidProjectJsonAsync(string projectFile, CancellationToken cancellationToken) + private async Task WaitForValidProjectJsonAsync(string projectFile, CancellationToken cancellationToken) { - IProject project = null; - while (true) { + IProject project; string errors; if (_projectProvider.TryReadProject(projectFile, out project, out errors)) { - return project; + return; } _logger.LogError($"Error(s) reading project file '{projectFile}': "); _logger.LogError(errors); _logger.LogInformation("Fix the error to continue."); - using (var fileWatcher = _fileWatcherFactory(Path.GetDirectoryName(projectFile))) + using (var projectWatcher = CreateProjectWatcher(projectFile, watchProjectJsonOnly: true)) { - fileWatcher.WatchFile(projectFile); - fileWatcher.WatchProject(projectFile); - - await WatchForFileChangeAsync(fileWatcher, cancellationToken); + await projectWatcher.WaitForChangeAsync(cancellationToken); if (cancellationToken.IsCancellationRequested) { - return null; + return; } _logger.LogInformation($"File changed: {projectFile}"); @@ -199,66 +194,18 @@ namespace Microsoft.DotNet.Watcher.Core } } - private void AddProjectAndDependeciesToWatcher(string projectFile, IFileWatcher fileWatcher) + private ProjectWatcher CreateProjectWatcher(string projectFile, bool watchProjectJsonOnly) { - IProject project; - string errors; - - if (_projectProvider.TryReadProject(projectFile, out project, out errors)) - { - AddProjectAndDependeciesToWatcher(project, fileWatcher); - } - } - - private void AddProjectAndDependeciesToWatcher(IProject project, IFileWatcher fileWatcher) - { - foreach (var file in project.Files) - { - if (!string.IsNullOrEmpty(file)) - { - fileWatcher.WatchDirectory( - Path.GetDirectoryName(file), - Path.GetExtension(file)); - } - } - - fileWatcher.WatchProject(project.ProjectFile); - - foreach (var projFile in project.ProjectDependencies) - { - AddProjectAndDependeciesToWatcher(projFile, fileWatcher); - } - } - - private async Task WatchForFileChangeAsync(IFileWatcher fileWatcher, CancellationToken cancellationToken) - { - var tcs = new TaskCompletionSource(); - - cancellationToken.Register(() => tcs.TrySetResult(null)); - - Action callback = path => - { - tcs.TrySetResult(path); - }; - - fileWatcher.OnChanged += callback; - - var changedPath = await tcs.Task; - - // Don't need to listen anymore - fileWatcher.OnChanged -= callback; - - return changedPath; + return new ProjectWatcher(projectFile, watchProjectJsonOnly, _fileWatcherFactory, _projectProvider); } public static DotNetWatcher CreateDefault(ILoggerFactory loggerFactory) { return new DotNetWatcher( - fileWatcherFactory: root => new FileWatcher(root), + fileWatcherFactory: () => new FileWatcher(), processWatcherFactory: () => new ProcessWatcher(), projectProvider: new ProjectProvider(), loggerFactory: loggerFactory); } - } } diff --git a/src/Microsoft.DotNet.Watcher.Core/FileSystem/FileSystemWatcherRoot.cs b/src/Microsoft.DotNet.Watcher.Core/FileSystem/FileSystemWatcherRoot.cs deleted file mode 100644 index ff8534b..0000000 --- a/src/Microsoft.DotNet.Watcher.Core/FileSystem/FileSystemWatcherRoot.cs +++ /dev/null @@ -1,30 +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.IO; - -namespace Microsoft.DotNet.Watcher.Core -{ - internal class FileSystemWatcherRoot : IWatcherRoot - { - private readonly FileSystemWatcher _watcher; - - public FileSystemWatcherRoot(FileSystemWatcher watcher) - { - _watcher = watcher; - } - - public string Path - { - get - { - return _watcher.Path; - } - } - - public void Dispose() - { - _watcher.Dispose(); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Watcher.Core/FileSystem/FileWatcher.cs b/src/Microsoft.DotNet.Watcher.Core/FileSystem/FileWatcher.cs deleted file mode 100644 index e3d3abd..0000000 --- a/src/Microsoft.DotNet.Watcher.Core/FileSystem/FileWatcher.cs +++ /dev/null @@ -1,217 +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; - -namespace Microsoft.DotNet.Watcher.Core -{ - public class FileWatcher : IFileWatcher - { - private readonly HashSet _files = new HashSet(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary> _directories = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - private readonly List _watchers = new List(); - - internal FileWatcher() - { - } - - public FileWatcher(string path) - { - AddWatcher(path); - } - - public event Action OnChanged; - - public void WatchDirectory(string path, string extension) - { - var extensions = _directories.GetOrAdd(path, _ => new HashSet(StringComparer.OrdinalIgnoreCase)); - - extensions.Add(extension); - } - - public bool WatchFile(string path) - { - return _files.Add(path); - } - - public void WatchProject(string projectPath) - { - if (string.IsNullOrEmpty(projectPath)) - { - return; - } - - // If any watchers already handle this path then noop - if (!IsAlreadyWatched(projectPath)) - { - // To reduce the number of watchers we have we add a watcher to the root - // of this project so that we'll be notified if anything we care - // about changes - var rootPath = ResolveRootDirectory(projectPath); - AddWatcher(rootPath); - } - } - - // For testing - internal bool IsAlreadyWatched(string projectPath) - { - if (string.IsNullOrEmpty(projectPath)) - { - return false; - } - - bool anyWatchers = false; - - foreach (var watcher in _watchers) - { - // REVIEW: This needs to work x-platform, should this be case - // sensitive? - if (EnsureTrailingSlash(projectPath).StartsWith(EnsureTrailingSlash(watcher.Path), StringComparison.OrdinalIgnoreCase)) - { - anyWatchers = true; - } - } - - return anyWatchers; - } - - public void Dispose() - { - foreach (var w in _watchers) - { - w.Dispose(); - } - - _watchers.Clear(); - } - - public bool ReportChange(string newPath, WatcherChangeTypes changeType) - { - return ReportChange(oldPath: null, newPath: newPath, changeType: changeType); - } - - public bool ReportChange(string oldPath, string newPath, WatcherChangeTypes changeType) - { - if (HasChanged(oldPath, newPath, changeType)) - { - if (OnChanged != null) - { - OnChanged(oldPath ?? newPath); - } - - return true; - } - - return false; - } - - private static string EnsureTrailingSlash(string path) - { - if (string.IsNullOrEmpty(path)) - { - return path; - } - - if (path[path.Length - 1] != Path.DirectorySeparatorChar) - { - return path + Path.DirectorySeparatorChar; - } - - return path; - } - - // For testing only - internal void AddWatcher(IWatcherRoot watcherRoot) - { - _watchers.Add(watcherRoot); - } - - private void AddWatcher(string path) - { - var watcher = new FileSystemWatcher(path); - watcher.IncludeSubdirectories = true; - watcher.EnableRaisingEvents = true; - - watcher.Changed += OnWatcherChanged; - watcher.Renamed += OnRenamed; - watcher.Deleted += OnWatcherChanged; - watcher.Created += OnWatcherChanged; - - _watchers.Add(new FileSystemWatcherRoot(watcher)); - } - - private void OnRenamed(object sender, RenamedEventArgs e) - { - ReportChange(e.OldFullPath, e.FullPath, e.ChangeType); - } - - private void OnWatcherChanged(object sender, FileSystemEventArgs e) - { - ReportChange(e.FullPath, e.ChangeType); - } - - private bool HasChanged(string oldPath, string newPath, WatcherChangeTypes changeType) - { - // File changes - if (_files.Contains(newPath) || - (oldPath != null && _files.Contains(oldPath))) - { - return true; - } - - HashSet extensions; - if (_directories.TryGetValue(newPath, out extensions) || - _directories.TryGetValue(Path.GetDirectoryName(newPath), out extensions)) - { - string extension = Path.GetExtension(newPath); - - if (String.IsNullOrEmpty(extension)) - { - // Assume it's a directory - if (changeType == WatcherChangeTypes.Created || - changeType == WatcherChangeTypes.Renamed) - { - foreach (var e in extensions) - { - WatchDirectory(newPath, e); - } - } - else if (changeType == WatcherChangeTypes.Deleted) - { - return true; - } - - // Ignore anything else - return false; - } - - return extensions.Contains(extension); - } - - return false; - } - - private static string ResolveRootDirectory(string projectPath) - { - var di = new DirectoryInfo(projectPath); - - while (di.Parent != null) - { - var globalJsonPath = Path.Combine(di.FullName, "global.json"); - - if (File.Exists(globalJsonPath)) - { - return di.FullName; - } - - di = di.Parent; - } - - // If we don't find any files then make the project folder the root - return projectPath; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Watcher.Core/FileSystem/IWatcherRoot.cs b/src/Microsoft.DotNet.Watcher.Core/FileSystem/IWatcherRoot.cs deleted file mode 100644 index eaf0e66..0000000 --- a/src/Microsoft.DotNet.Watcher.Core/FileSystem/IWatcherRoot.cs +++ /dev/null @@ -1,12 +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; - -namespace Microsoft.DotNet.Watcher.Core -{ - internal interface IWatcherRoot : IDisposable - { - string Path { get; } - } -} diff --git a/src/Microsoft.DotNet.Watcher.Core/Abstractions/IFileWatcher.cs b/src/Microsoft.DotNet.Watcher.Core/Internal/IFileWatcher.cs similarity index 53% rename from src/Microsoft.DotNet.Watcher.Core/Abstractions/IFileWatcher.cs rename to src/Microsoft.DotNet.Watcher.Core/Internal/IFileWatcher.cs index c5f01a6..ab1f255 100644 --- a/src/Microsoft.DotNet.Watcher.Core/Abstractions/IFileWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Core/Internal/IFileWatcher.cs @@ -3,16 +3,12 @@ using System; -namespace Microsoft.DotNet.Watcher.Core +namespace Microsoft.DotNet.Watcher.Core.Internal { public interface IFileWatcher : IDisposable { - event Action OnChanged; + event Action OnFileChange; - void WatchDirectory(string path, string extension); - - bool WatchFile(string path); - - void WatchProject(string path); + void WatchDirectory(string directory); } } diff --git a/src/Microsoft.DotNet.Watcher.Core/Abstractions/IProcessWatcher.cs b/src/Microsoft.DotNet.Watcher.Core/Internal/IProcessWatcher.cs similarity index 90% rename from src/Microsoft.DotNet.Watcher.Core/Abstractions/IProcessWatcher.cs rename to src/Microsoft.DotNet.Watcher.Core/Internal/IProcessWatcher.cs index ad975f3..86960b2 100644 --- a/src/Microsoft.DotNet.Watcher.Core/Abstractions/IProcessWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Core/Internal/IProcessWatcher.cs @@ -5,7 +5,7 @@ using System; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.DotNet.Watcher.Core +namespace Microsoft.DotNet.Watcher.Core.Internal { public interface IProcessWatcher { diff --git a/src/Microsoft.DotNet.Watcher.Core/Abstractions/IProject.cs b/src/Microsoft.DotNet.Watcher.Core/Internal/IProject.cs similarity index 88% rename from src/Microsoft.DotNet.Watcher.Core/Abstractions/IProject.cs rename to src/Microsoft.DotNet.Watcher.Core/Internal/IProject.cs index fc4556c..a2a2389 100644 --- a/src/Microsoft.DotNet.Watcher.Core/Abstractions/IProject.cs +++ b/src/Microsoft.DotNet.Watcher.Core/Internal/IProject.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; -namespace Microsoft.DotNet.Watcher.Core +namespace Microsoft.DotNet.Watcher.Core.Internal { public interface IProject { diff --git a/src/Microsoft.DotNet.Watcher.Core/Abstractions/IProjectProvider.cs b/src/Microsoft.DotNet.Watcher.Core/Internal/IProjectProvider.cs similarity index 86% rename from src/Microsoft.DotNet.Watcher.Core/Abstractions/IProjectProvider.cs rename to src/Microsoft.DotNet.Watcher.Core/Internal/IProjectProvider.cs index 44e46cd..bcbf63a 100644 --- a/src/Microsoft.DotNet.Watcher.Core/Abstractions/IProjectProvider.cs +++ b/src/Microsoft.DotNet.Watcher.Core/Internal/IProjectProvider.cs @@ -1,7 +1,7 @@ // 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.Core +namespace Microsoft.DotNet.Watcher.Core.Internal { public interface IProjectProvider { diff --git a/src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/FileWatcher.cs b/src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/FileWatcher.cs new file mode 100644 index 0000000..48f1353 --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/FileWatcher.cs @@ -0,0 +1,111 @@ +// 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; + +namespace Microsoft.DotNet.Watcher.Core.Internal +{ + public class FileWatcher : IFileWatcher + { + private bool _disposed; + + private readonly IDictionary _watchers = new Dictionary(); + + public event Action OnFileChange; + + public void WatchDirectory(string directory) + { + EnsureNotDisposed(); + AddDirectoryWatcher(directory); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + foreach (var watcher in _watchers) + { + watcher.Value.Dispose(); + } + _watchers.Clear(); + } + + private void AddDirectoryWatcher(string directory) + { + var alreadyWatched = _watchers + .Where(d => directory.StartsWith(d.Key)) + .Any(); + + if (alreadyWatched) + { + return; + } + + var redundantWatchers = _watchers + .Where(d => d.Key.StartsWith(directory)) + .Select(d => d.Key) + .ToList(); + + if (redundantWatchers.Any()) + { + foreach (var watcher in redundantWatchers) + { + DisposeWatcher(watcher); + } + } + + var newWatcher = new FileSystemWatcher(directory); + newWatcher.IncludeSubdirectories = true; + + newWatcher.Changed += WatcherChangedHandler; + newWatcher.Created += WatcherChangedHandler; + newWatcher.Deleted += WatcherChangedHandler; + newWatcher.Renamed += WatcherRenamedHandler; + + newWatcher.EnableRaisingEvents = true; + + _watchers.Add(directory, newWatcher); + } + + private void WatcherRenamedHandler(object sender, RenamedEventArgs e) + { + NotifyChange(e.OldFullPath); + NotifyChange(e.FullPath); + } + + private void WatcherChangedHandler(object sender, FileSystemEventArgs e) + { + NotifyChange(e.FullPath); + } + + private void NotifyChange(string path) + { + if (OnFileChange != null) + { + OnFileChange(path); + } + } + + private void DisposeWatcher(string directory) + { + _watchers[directory].Dispose(); + _watchers.Remove(directory); + } + + private void EnsureNotDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(FileWatcher)); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Watcher.Core/Impl/ProcessWatcher.cs b/src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/ProcessWatcher.cs similarity index 51% rename from src/Microsoft.DotNet.Watcher.Core/Impl/ProcessWatcher.cs rename to src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/ProcessWatcher.cs index 3fc42a5..0524284 100644 --- a/src/Microsoft.DotNet.Watcher.Core/Impl/ProcessWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/ProcessWatcher.cs @@ -7,11 +7,11 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.PlatformAbstractions; -namespace Microsoft.DotNet.Watcher.Core +namespace Microsoft.DotNet.Watcher.Core.Internal { public class ProcessWatcher : IProcessWatcher { - private static readonly bool _isWindows = PlatformServices.Default.Runtime.OperatingSystem.Equals("Windows", StringComparison.OrdinalIgnoreCase); + private static readonly TimeSpan _processKillTimeout = TimeSpan.FromSeconds(30); private Process _runningProcess; @@ -39,7 +39,13 @@ namespace Microsoft.DotNet.Watcher.Core public Task WaitForExitAsync(CancellationToken cancellationToken) { - cancellationToken.Register(() => KillProcess(_runningProcess?.Id)); + cancellationToken.Register(() => + { + if (_runningProcess != null) + { + _runningProcess.KillTree(_processKillTimeout); + } + }); return Task.Run(() => { @@ -52,41 +58,5 @@ namespace Microsoft.DotNet.Watcher.Core }); } - private void KillProcess(int? processId) - { - if (processId == null) - { - return; - } - - if (_isWindows) - { - var startInfo = new ProcessStartInfo() - { - FileName = "taskkill", - Arguments = $"/T /F /PID {processId}", - }; - var killProcess = Process.Start(startInfo); - killProcess.WaitForExit(); - } - else - { - var killSubProcessStartInfo = new ProcessStartInfo - { - FileName = "pkill", - Arguments = $"-TERM -P {processId}", - }; - var killSubProcess = Process.Start(killSubProcessStartInfo); - killSubProcess.WaitForExit(); - - var killProcessStartInfo = new ProcessStartInfo - { - FileName = "kill", - Arguments = $"-TERM {processId}", - }; - var killProcess = Process.Start(killProcessStartInfo); - killProcess.WaitForExit(); - } - } } } \ No newline at end of file diff --git a/src/Microsoft.DotNet.Watcher.Core/Impl/Project.cs b/src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/Project.cs similarity index 97% rename from src/Microsoft.DotNet.Watcher.Core/Impl/Project.cs rename to src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/Project.cs index a4e2423..6553080 100644 --- a/src/Microsoft.DotNet.Watcher.Core/Impl/Project.cs +++ b/src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/Project.cs @@ -6,7 +6,7 @@ using System.IO; using System.Linq; using Microsoft.DotNet.ProjectModel.Graph; -namespace Microsoft.DotNet.Watcher.Core +namespace Microsoft.DotNet.Watcher.Core.Internal { internal class Project : IProject { diff --git a/src/Microsoft.DotNet.Watcher.Core/Impl/ProjectProvider.cs b/src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/ProjectProvider.cs similarity index 98% rename from src/Microsoft.DotNet.Watcher.Core/Impl/ProjectProvider.cs rename to src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/ProjectProvider.cs index 1336f50..fc41f81 100644 --- a/src/Microsoft.DotNet.Watcher.Core/Impl/ProjectProvider.cs +++ b/src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/ProjectProvider.cs @@ -7,7 +7,7 @@ using System.Linq; using System.Text; using Microsoft.DotNet.ProjectModel; -namespace Microsoft.DotNet.Watcher.Core +namespace Microsoft.DotNet.Watcher.Core.Internal { public class ProjectProvider : IProjectProvider { diff --git a/src/Microsoft.DotNet.Watcher.Core/Internal/ProcessExtensions.cs b/src/Microsoft.DotNet.Watcher.Core/Internal/ProcessExtensions.cs new file mode 100644 index 0000000..56744e8 --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Core/Internal/ProcessExtensions.cs @@ -0,0 +1,113 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using Microsoft.Extensions.PlatformAbstractions; + +namespace Microsoft.DotNet.Watcher.Core.Internal +{ + public static class ProcessExtensions + { + private static readonly bool _isWindows = PlatformServices.Default.Runtime.OperatingSystemPlatform == Platform.Windows; + + public static void KillTree(this Process process, TimeSpan timeout) + { + string stdout; + if (_isWindows) + { + RunProcessAndWaitForExit( + "taskkill", + $"/T /F /PID {process.Id}", + timeout, + out stdout); + } + else + { + var children = GetAllChildIdsUnix(process, timeout); + foreach (var childId in children) + { + KillProcessUnix(childId, timeout); + } + KillProcessUnix(process.Id, timeout); + } + } + + private static IEnumerable GetAllChildIdsUnix(Process process, TimeSpan timeout) + { + var children = new HashSet(); + GetAllChildIdsUnix(process.Id, children, timeout); + return children; + } + + private static void GetAllChildIdsUnix(int parentId, ISet children, TimeSpan timeout) + { + string stdout; + var exitCode = RunProcessAndWaitForExit( + "pgrep", + $"-P {parentId}", + timeout, + out stdout); + + if (exitCode == 0 && !string.IsNullOrEmpty(stdout)) + { + using (var reader = new StringReader(stdout)) + { + while (true) + { + var text = reader.ReadLine(); + if (text == null) + { + return; + } + + int id; + if (int.TryParse(text, out id)) + { + children.Add(id); + // Recursively get the children + GetAllChildIdsUnix(id, children, timeout); + } + } + } + } + } + + private static void KillProcessUnix(int processId, TimeSpan timeout) + { + string stdout; + RunProcessAndWaitForExit( + "kill", + $"-TERM {processId}", + timeout, + out stdout); + } + + private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout) + { + var startInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + UseShellExecute = false + }; + + var process = Process.Start(startInfo); + + stdout = null; + if (process.WaitForExit((int)timeout.TotalMilliseconds)) + { + stdout = process.StandardOutput.ReadToEnd(); + } + else + { + process.Kill(); + } + + return process.ExitCode; + } + } +} diff --git a/src/Microsoft.DotNet.Watcher.Core/Internal/ProjectWatcher.cs b/src/Microsoft.DotNet.Watcher.Core/Internal/ProjectWatcher.cs new file mode 100644 index 0000000..e94afb7 --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Core/Internal/ProjectWatcher.cs @@ -0,0 +1,119 @@ +// 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.Core.Internal +{ + public class ProjectWatcher : IDisposable + { + private readonly IProjectProvider _projectProvider; + private readonly IFileWatcher _fileWatcher; + + private readonly string _rootProject; + private readonly bool _watchProjectJsonOnly; + + private ISet _watchedFiles; + + public ProjectWatcher( + string projectToWatch, + bool watchProjectJsonOnly, + Func fileWatcherFactory, + IProjectProvider projectProvider) + { + _projectProvider = projectProvider; + _fileWatcher = fileWatcherFactory(); + + _rootProject = projectToWatch; + _watchProjectJsonOnly = watchProjectJsonOnly; + } + + public async Task WaitForChangeAsync(CancellationToken cancellationToken) + { + _watchedFiles = GetProjectFilesClosure(_rootProject); + + foreach (var file in _watchedFiles) + { + _fileWatcher.WatchDirectory(Path.GetDirectoryName(file)); + } + + var tcs = new TaskCompletionSource(); + cancellationToken.Register(() => tcs.TrySetResult(null)); + + Action 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 GetProjectFilesClosure(string projectFile) + { + var closure = new HashSet(); + + if (_watchProjectJsonOnly) + { + closure.Add(projectFile); + } + else + { + GetProjectFilesClosure(projectFile, closure); + } + return closure; + } + + private void GetProjectFilesClosure(string projectFile, ISet 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); + } + } + } + } +} diff --git a/src/Microsoft.DotNet.Watcher.Core/project.json b/src/Microsoft.DotNet.Watcher.Core/project.json index abf9223..a84beb6 100644 --- a/src/Microsoft.DotNet.Watcher.Core/project.json +++ b/src/Microsoft.DotNet.Watcher.Core/project.json @@ -18,7 +18,8 @@ }, "Microsoft.Extensions.Logging.Abstractions": "1.0.0-*", "Microsoft.Extensions.FileSystemGlobbing": "1.0.0-*", - "Microsoft.NETCore.Platforms": "1.0.1-*" + "Microsoft.NETCore.Platforms": "1.0.1-*", + "System.Diagnostics.Process": "4.1.0-*" }, "frameworks": { "dnxcore50": { diff --git a/test/TestApps/AppWithDeps/AppWithDeps.xproj b/test/TestApps/AppWithDeps/AppWithDeps.xproj new file mode 100644 index 0000000..289874a --- /dev/null +++ b/test/TestApps/AppWithDeps/AppWithDeps.xproj @@ -0,0 +1,19 @@ + + + + 14.0.23107 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + f7734e61-f510-41e0-ad15-301a64081cd1 + AppWithDeps + ..\..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/test/TestApps/AppWithDeps/Program.cs b/test/TestApps/AppWithDeps/Program.cs new file mode 100644 index 0000000..6c67ed0 --- /dev/null +++ b/test/TestApps/AppWithDeps/Program.cs @@ -0,0 +1,23 @@ +// 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.IO; + +namespace ConsoleApplication +{ + public class Program + { + public static void Main(string[] args) + { + Console.WriteLine("AppWithDeps started."); + + var processId = Process.GetCurrentProcess().Id; + File.AppendAllLines(args[0], new string[] { $"{processId}" }); + + File.WriteAllText(args[0] + ".started", ""); + Console.ReadLine(); + } + } +} diff --git a/test/TestApps/AppWithDeps/project.json b/test/TestApps/AppWithDeps/project.json new file mode 100644 index 0000000..c9ce3f7 --- /dev/null +++ b/test/TestApps/AppWithDeps/project.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0-*", + "compilationOptions": { + "emitEntryPoint": true + }, + "dependencies": { + "Dependency": "1.0.0", + "NETStandard.Library": "1.0.0-*" + }, + "frameworks": { + "dnxcore50": { + "dependencies": { + "System.Console": "4.0.0-*", + "System.Diagnostics.Process": "4.1.0-*", + "System.IO": "4.0.11-*", + "System.IO.FileSystem": "4.0.1-*" + } + } + } +} \ No newline at end of file diff --git a/test/TestApps/Dependency/Dependency.xproj b/test/TestApps/Dependency/Dependency.xproj new file mode 100644 index 0000000..d1edb4a --- /dev/null +++ b/test/TestApps/Dependency/Dependency.xproj @@ -0,0 +1,19 @@ + + + + 14.0.23107 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 2f48041a-f7d1-478f-9c38-d41f0f05e8ca + Dependency + ..\..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/test/TestApps/Dependency/Foo.cs b/test/TestApps/Dependency/Foo.cs new file mode 100644 index 0000000..3441304 --- /dev/null +++ b/test/TestApps/Dependency/Foo.cs @@ -0,0 +1,9 @@ +// 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 Dependency +{ + public class Foo + { + } +} diff --git a/test/TestApps/Dependency/project.json b/test/TestApps/Dependency/project.json new file mode 100644 index 0000000..89004d9 --- /dev/null +++ b/test/TestApps/Dependency/project.json @@ -0,0 +1,18 @@ +{ + "version": "1.0.0-*", + + "dependencies": { + "NETStandard.Library": "1.0.0-*" + }, + + "frameworks": { + "dnxcore50": { + "dependencies": { + "System.Console": "4.0.0-*", + "System.Diagnostics.Process": "4.1.0-*", + "System.IO": "4.0.11-*", + "System.IO.FileSystem": "4.0.1-*" + } + } + } +} diff --git a/test/TestApps/GlobbingApp/GlobbingApp.xproj b/test/TestApps/GlobbingApp/GlobbingApp.xproj new file mode 100644 index 0000000..791e441 --- /dev/null +++ b/test/TestApps/GlobbingApp/GlobbingApp.xproj @@ -0,0 +1,19 @@ + + + + 14.0.23107 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 2ab1a28b-2022-49ea-af77-ac8a875915cc + GlobbingApp + ..\..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/test/TestApps/GlobbingApp/Program.cs b/test/TestApps/GlobbingApp/Program.cs new file mode 100644 index 0000000..ae99620 --- /dev/null +++ b/test/TestApps/GlobbingApp/Program.cs @@ -0,0 +1,23 @@ +// 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.IO; + +namespace ConsoleApplication +{ + public class Program + { + public static void Main(string[] args) + { + Console.WriteLine("GlobbingApp started."); + + var processId = Process.GetCurrentProcess().Id; + File.AppendAllLines(args[0], new string[] { $"{processId}" }); + + File.WriteAllText(args[0] + ".started", ""); + Console.ReadLine(); + } + } +} diff --git a/test/TestApps/GlobbingApp/exclude/Baz.cs b/test/TestApps/GlobbingApp/exclude/Baz.cs new file mode 100644 index 0000000..fdaebd7 --- /dev/null +++ b/test/TestApps/GlobbingApp/exclude/Baz.cs @@ -0,0 +1,10 @@ +// 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 GlobbingApp.exclude +{ + public class Baz + { + "THIS FILE SHOULD NOT BE INCLUDED IN COMPILATION" + } +} diff --git a/test/TestApps/GlobbingApp/include/Foo.cs b/test/TestApps/GlobbingApp/include/Foo.cs new file mode 100644 index 0000000..d1afb65 --- /dev/null +++ b/test/TestApps/GlobbingApp/include/Foo.cs @@ -0,0 +1,9 @@ +// 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 GlobbingApp.include +{ + public class Foo + { + } +} diff --git a/test/TestApps/GlobbingApp/project.json b/test/TestApps/GlobbingApp/project.json new file mode 100644 index 0000000..1a6b3da --- /dev/null +++ b/test/TestApps/GlobbingApp/project.json @@ -0,0 +1,27 @@ +{ + "version": "1.0.0-*", + "compilationOptions": { + "emitEntryPoint": true + }, + "compile": [ + "Program.cs", + "include/*.cs" + ], + "exclude": [ + "exclude/*" + ], + "dependencies": { + "NETStandard.Library": "1.0.0-*" + }, + + "frameworks": { + "dnxcore50": { + "dependencies": { + "System.Console": "4.0.0-*", + "System.Diagnostics.Process": "4.1.0-*", + "System.IO": "4.0.11-*", + "System.IO.FileSystem": "4.0.1-*" + } + } + } +} diff --git a/test/TestApps/NoDepsApp/NoDepsApp.xproj b/test/TestApps/NoDepsApp/NoDepsApp.xproj new file mode 100644 index 0000000..959a38d --- /dev/null +++ b/test/TestApps/NoDepsApp/NoDepsApp.xproj @@ -0,0 +1,19 @@ + + + + 14.0.23107 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 4f0d8a80-221f-4bcb-822e-44a0655f537e + NoDepsApp + ..\..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/test/TestApps/NoDepsApp/Program.cs b/test/TestApps/NoDepsApp/Program.cs new file mode 100644 index 0000000..2309fab --- /dev/null +++ b/test/TestApps/NoDepsApp/Program.cs @@ -0,0 +1,27 @@ +// 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.IO; + +namespace ConsoleApplication +{ + public class Program + { + public static void Main(string[] args) + { + Console.WriteLine("NoDepsApp started."); + + var processId = Process.GetCurrentProcess().Id; + File.AppendAllLines(args[0], new string[] { $"{processId}" }); + + File.WriteAllText(args[0] + ".started", ""); + + if (args.Length > 1 && args[1] == "--no-exit") + { + Console.ReadLine(); + } + } + } +} diff --git a/test/TestApps/NoDepsApp/project.json b/test/TestApps/NoDepsApp/project.json new file mode 100644 index 0000000..380a03b --- /dev/null +++ b/test/TestApps/NoDepsApp/project.json @@ -0,0 +1,21 @@ +{ + "version": "1.0.0-*", + "compilationOptions": { + "emitEntryPoint": true + }, + + "dependencies": { + "NETStandard.Library": "1.0.0-*" + }, + + "frameworks": { + "dnxcore50": { + "dependencies": { + "System.Console": "4.0.0-*", + "System.Diagnostics.Process": "4.1.0-*", + "System.IO": "4.0.11-*", + "System.IO.FileSystem": "4.0.1-*" + } + } + } +} diff --git a/test/dotnet-watch.FunctionalTests/AppWithDepsTests.cs b/test/dotnet-watch.FunctionalTests/AppWithDepsTests.cs new file mode 100644 index 0000000..0b6144b --- /dev/null +++ b/test/dotnet-watch.FunctionalTests/AppWithDepsTests.cs @@ -0,0 +1,74 @@ +// 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.IO; +using Xunit; + +namespace Microsoft.DotNet.Watcher.FunctionalTests +{ + public class AppWithDepsTests + { + private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30); + + // Change a file included in compilation + [Fact] + public void ChangeFileInDependency() + { + using (var scenario = new AppWithDepsScenario()) + using (var wait = new WaitForFileToChange(scenario.StartedFile)) + { + var fileToChange = Path.Combine(scenario.DependencyFolder, "Foo.cs"); + var programCs = File.ReadAllText(fileToChange); + File.WriteAllText(fileToChange, programCs); + + wait.Wait(_defaultTimeout, + expectedToChange: true, + errorMessage: $"Process did not restart because {scenario.StartedFile} was not changed"); + } + } + + + private class AppWithDepsScenario : DotNetWatchScenario + { + private const string AppWithDeps = "AppWithDeps"; + private const string Dependency = "Dependency"; + + private static readonly string _appWithDepsFolder = Path.Combine(_repositoryRoot, "test", "TestApps", AppWithDeps); + private static readonly string _dependencyFolder = Path.Combine(_repositoryRoot, "test", "TestApps", Dependency); + + public AppWithDepsScenario() + { + StatusFile = Path.Combine(_scenario.TempFolder, "status"); + StartedFile = StatusFile + ".started"; + + _scenario.AddProject(_appWithDepsFolder); + _scenario.AddProject(_dependencyFolder); + + _scenario.AddToolToProject(AppWithDeps, DotnetWatch); + _scenario.Restore(); + + AppWithDepsFolder = Path.Combine(_scenario.WorkFolder, AppWithDeps); + DependencyFolder = Path.Combine(_scenario.WorkFolder, Dependency); + + // Wait for the process to start + using (var wait = new WaitForFileToChange(StatusFile)) + { + RunDotNetWatch(StatusFile, Path.Combine(_scenario.WorkFolder, AppWithDeps)); + + wait.Wait(_defaultTimeout, + expectedToChange: true, + errorMessage: $"File not created: {StatusFile}"); + } + + Waiters.WaitForFileToBeReadable(StatusFile, _defaultTimeout); + } + + public string StatusFile { get; private set; } + public string StartedFile { get; private set; } + public string AppWithDepsFolder { get; private set; } + public string DependencyFolder { get; private set; } + } + } +} diff --git a/test/dotnet-watch.FunctionalTests/GlobbingAppTests.cs b/test/dotnet-watch.FunctionalTests/GlobbingAppTests.cs new file mode 100644 index 0000000..3188706 --- /dev/null +++ b/test/dotnet-watch.FunctionalTests/GlobbingAppTests.cs @@ -0,0 +1,168 @@ +// 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.IO; +using System.Threading; +using Xunit; + +namespace Microsoft.DotNet.Watcher.FunctionalTests +{ + public class GlobbingAppTests + { + private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30); + + private static readonly TimeSpan _negativeTestWaitTime = TimeSpan.FromSeconds(10); + + // Change a file included in compilation + [Fact] + public void ChangeCompiledFile() + { + using (var scenario = new GlobbingAppScenario()) + using (var wait = new WaitForFileToChange(scenario.StartedFile)) + { + var fileToChange = Path.Combine(scenario.TestAppFolder, "include", "Foo.cs"); + var programCs = File.ReadAllText(fileToChange); + File.WriteAllText(fileToChange, programCs); + + wait.Wait(_defaultTimeout, + expectedToChange: true, + errorMessage: $"Process did not restart because {scenario.StartedFile} was not changed"); + } + + } + + // Add a file to a folder included in compilation + [Fact] + public void AddCompiledFile() + { + // Add a file in a folder that's included in compilation + using (var scenario = new GlobbingAppScenario()) + using (var wait = new WaitForFileToChange(scenario.StartedFile)) + { + var fileToChange = Path.Combine(scenario.TestAppFolder, "include", "Bar.cs"); + File.WriteAllText(fileToChange, ""); + + wait.Wait(_defaultTimeout, + expectedToChange: true, + errorMessage: $"Process did not restart because {scenario.StartedFile} was not changed"); + } + } + + // Delete a file included in compilation + [Fact] + public void DeleteCompiledFile() + { + using (var scenario = new GlobbingAppScenario()) + using (var wait = new WaitForFileToChange(scenario.StartedFile)) + { + var fileToChange = Path.Combine(scenario.TestAppFolder, "include", "Foo.cs"); + File.Delete(fileToChange); + + wait.Wait(_defaultTimeout, + expectedToChange: true, + errorMessage: $"Process did not restart because {scenario.StartedFile} was not changed"); + } + } + + // Rename a file included in compilation + [Fact] + public void RenameCompiledFile() + { + using (var scenario = new GlobbingAppScenario()) + + using (var wait = new WaitForFileToChange(scenario.StatusFile)) + { + var oldFile = Path.Combine(scenario.TestAppFolder, "include", "Foo.cs"); + var newFile = Path.Combine(scenario.TestAppFolder, "include", "Foo_new.cs"); + File.Move(oldFile, newFile); + + wait.Wait(_defaultTimeout, + expectedToChange: true, + errorMessage: $"Process did not restart because {scenario.StartedFile} was not changed"); + } + } + + // Add a file that's in a included folder but not matching the globbing pattern + [Fact] + public void ChangeNonCompiledFile() + { + using (var scenario = new GlobbingAppScenario()) + using (var wait = new WaitForFileToChange(scenario.StartedFile)) + { + var ids = File.ReadAllLines(scenario.StatusFile); + var procId = int.Parse(ids[0]); + + var changedFile = Path.Combine(scenario.TestAppFolder, "include", "not_compiled.css"); + File.WriteAllText(changedFile, ""); + + Console.WriteLine($"Waiting {_negativeTestWaitTime} seconds to see if the app restarts"); + Waiters.WaitForProcessToStop( + procId, + _negativeTestWaitTime, + expectedToStop: false, + errorMessage: "Test app restarted"); + } + } + + // Change a file that's in an excluded folder + [Fact] + public void ChangeExcludedFile() + { + using (var scenario = new GlobbingAppScenario()) + { + // Then wait for it to restart when we change a file that's included in the compilation files + using (var wait = new WaitForFileToChange(scenario.StartedFile)) + { + var ids = File.ReadAllLines(scenario.StatusFile); + var procId = int.Parse(ids[0]); + + var changedFile = Path.Combine(scenario.TestAppFolder, "exclude", "Baz.cs"); + File.WriteAllText(changedFile, ""); + + Console.WriteLine($"Waiting {_negativeTestWaitTime} seconds to see if the app restarts"); + Waiters.WaitForProcessToStop( + procId, + _negativeTestWaitTime, + expectedToStop: false, + errorMessage: "Test app restarted"); + } + } + } + + private class GlobbingAppScenario : DotNetWatchScenario + { + private const string TestAppName = "GlobbingApp"; + private static readonly string _testAppFolder = Path.Combine(_repositoryRoot, "test", "TestApps", TestAppName); + + public GlobbingAppScenario() + { + StatusFile = Path.Combine(_scenario.TempFolder, "status"); + StartedFile = StatusFile + ".started"; + + _scenario.AddProject(_testAppFolder); + _scenario.AddToolToProject(TestAppName, DotnetWatch); + _scenario.Restore(); + + TestAppFolder = Path.Combine(_scenario.WorkFolder, TestAppName); + + // Wait for the process to start + using (var wait = new WaitForFileToChange(StartedFile)) + { + RunDotNetWatch(StatusFile, Path.Combine(_scenario.WorkFolder, TestAppName)); + + wait.Wait(_defaultTimeout, + expectedToChange: true, + errorMessage: $"File not created: {StartedFile}"); + } + + Waiters.WaitForFileToBeReadable(StartedFile, _defaultTimeout); + } + + public string StatusFile { get; private set; } + public string StartedFile { get; private set; } + public string TestAppFolder { get; private set; } + } + } +} diff --git a/test/dotnet-watch.FunctionalTests/NoDepsAppTests.cs b/test/dotnet-watch.FunctionalTests/NoDepsAppTests.cs new file mode 100644 index 0000000..418fe53 --- /dev/null +++ b/test/dotnet-watch.FunctionalTests/NoDepsAppTests.cs @@ -0,0 +1,158 @@ +// 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.IO; +using Xunit; + +namespace Microsoft.DotNet.Watcher.FunctionalTests +{ + public class NoDepsAppTests + { + private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30); + + [Fact] + public void RestartProcessOnFileChange() + { + using (var scenario = new NoDepsAppScenario()) + { + // Wait for the process to start + using (var wait = new WaitForFileToChange(scenario.StartedFile)) + { + scenario.RunDotNetWatch($"{scenario.StatusFile} --no-exit"); + + wait.Wait(_defaultTimeout, + expectedToChange: true, + errorMessage: $"File not created: {scenario.StartedFile}"); + } + + // Then wait for it to restart when we change a file + using (var wait = new WaitForFileToChange(scenario.StartedFile)) + { + var fileToChange = Path.Combine(scenario.TestAppFolder, "Program.cs"); + var programCs = File.ReadAllText(fileToChange); + File.WriteAllText(fileToChange, programCs); + + wait.Wait(_defaultTimeout, + expectedToChange: true, + errorMessage: $"Process did not restart because {scenario.StartedFile} was not changed"); + } + + // Check that the first child process is no longer running + Waiters.WaitForFileToBeReadable(scenario.StatusFile, _defaultTimeout); + var ids = File.ReadAllLines(scenario.StatusFile); + var firstProcessId = int.Parse(ids[0]); + Waiters.WaitForProcessToStop( + firstProcessId, + TimeSpan.FromSeconds(1), + expectedToStop: true, + errorMessage: $"PID: {firstProcessId} is still alive"); + } + } + + [Fact] + public void RestartProcessThatTerminatesAfterFileChange() + { + using (var scenario = new NoDepsAppScenario()) + { + // Wait for the process to start + using (var wait = new WaitForFileToChange(scenario.StartedFile)) + { + scenario.RunDotNetWatch(scenario.StatusFile); + + wait.Wait(_defaultTimeout, + expectedToChange: true, + errorMessage: $"File not created: {scenario.StartedFile}"); + } + + // Then wait for the app to exit + Waiters.WaitForFileToBeReadable(scenario.StartedFile, _defaultTimeout); + var ids = File.ReadAllLines(scenario.StatusFile); + var procId = int.Parse(ids[0]); + Waiters.WaitForProcessToStop( + procId, + _defaultTimeout, + expectedToStop: true, + errorMessage: "Test app did not exit"); + + // Then wait for it to restart when we change a file + using (var wait = new WaitForFileToChange(scenario.StartedFile)) + { + var fileToChange = Path.Combine(scenario.TestAppFolder, "Program.cs"); + var programCs = File.ReadAllText(fileToChange); + File.WriteAllText(fileToChange, programCs); + + wait.Wait(_defaultTimeout, + expectedToChange: true, + errorMessage: $"Process did not restart because {scenario.StartedFile} was not changed"); + } + } + } + + + [Fact] + public void ExitOnChange() + { + using (var scenario = new NoDepsAppScenario()) + { + // Wait for the process to start + using (var wait = new WaitForFileToChange(scenario.StartedFile)) + { + scenario.RunDotNetWatch($"--exit-on-change -- {scenario.StatusFile} --no-exit"); + + wait.Wait(_defaultTimeout, + expectedToChange: true, + errorMessage: $"File not created: {scenario.StartedFile}"); + } + + // Change a file + var fileToChange = Path.Combine(scenario.TestAppFolder, "Program.cs"); + var programCs = File.ReadAllText(fileToChange); + File.WriteAllText(fileToChange, programCs); + + Waiters.WaitForProcessToStop( + scenario.WatcherProcess.Id, + _defaultTimeout, + expectedToStop: true, + errorMessage: "The watcher did not stop"); + + // Check that the first child process is no longer running + var ids = File.ReadAllLines(scenario.StatusFile); + var firstProcessId = int.Parse(ids[0]); + Waiters.WaitForProcessToStop( + firstProcessId, + TimeSpan.FromSeconds(1), + expectedToStop: true, + errorMessage: $"PID: {firstProcessId} is still alive"); + } + } + + private class NoDepsAppScenario : DotNetWatchScenario + { + private const string TestAppName = "NoDepsApp"; + private static readonly string _testAppFolder = Path.Combine(_repositoryRoot, "test", "TestApps", TestAppName); + + public NoDepsAppScenario() + { + StatusFile = Path.Combine(_scenario.TempFolder, "status"); + StartedFile = StatusFile + ".started"; + + _scenario.AddProject(_testAppFolder); + _scenario.AddToolToProject(TestAppName, DotnetWatch); + _scenario.Restore(); + + TestAppFolder = Path.Combine(_scenario.WorkFolder, TestAppName); + } + + public string StatusFile { get; private set; } + public string StartedFile { get; private set; } + public string TestAppFolder { get; private set; } + + public void RunDotNetWatch(string args) + { + RunDotNetWatch(args, Path.Combine(_scenario.WorkFolder, TestAppName)); + } + } + } +} diff --git a/test/dotnet-watch.FunctionalTests/Scenario/DotNetWatchScenario.cs b/test/dotnet-watch.FunctionalTests/Scenario/DotNetWatchScenario.cs new file mode 100644 index 0000000..ebcdebc --- /dev/null +++ b/test/dotnet-watch.FunctionalTests/Scenario/DotNetWatchScenario.cs @@ -0,0 +1,67 @@ +// 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.IO; +using Microsoft.DotNet.Watcher.Core.Internal; + +namespace Microsoft.DotNet.Watcher.FunctionalTests +{ + public class DotNetWatchScenario : IDisposable + { + private static readonly TimeSpan _processKillTimeout = TimeSpan.FromSeconds(30); + + protected const string DotnetWatch = "dotnet-watch"; + + protected static readonly string _repositoryRoot = FindRepoRoot(); + protected static readonly string _artifactsFolder = Path.Combine(_repositoryRoot, "artifacts", "build"); + + protected ProjectToolScenario _scenario; + + public DotNetWatchScenario() + { + _scenario = new ProjectToolScenario(); + _scenario.AddNugetFeed(DotnetWatch, _artifactsFolder); + } + + public Process WatcherProcess { get; private set; } + + protected void RunDotNetWatch(string arguments, string workingFolder) + { + WatcherProcess = _scenario.ExecuteDotnet("watch " + arguments, workingFolder); + } + + public virtual void Dispose() + { + if (WatcherProcess != null) + { + if (!WatcherProcess.HasExited) + { + WatcherProcess.KillTree(_processKillTimeout); + } + WatcherProcess.Dispose(); + } + _scenario.Dispose(); + } + + private static string FindRepoRoot() + { + var di = new DirectoryInfo(Directory.GetCurrentDirectory()); + + while (di.Parent != null) + { + var globalJsonFile = Path.Combine(di.FullName, "global.json"); + + if (File.Exists(globalJsonFile)) + { + return di.FullName; + } + + di = di.Parent; + } + + return null; + } + } +} diff --git a/test/dotnet-watch.FunctionalTests/Scenario/ProjectToolScenario.cs b/test/dotnet-watch.FunctionalTests/Scenario/ProjectToolScenario.cs new file mode 100644 index 0000000..41ee122 --- /dev/null +++ b/test/dotnet-watch.FunctionalTests/Scenario/ProjectToolScenario.cs @@ -0,0 +1,150 @@ +// 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.IO; +using System.Linq; +using System.Threading; +using System.Xml.Linq; +using Newtonsoft.Json.Linq; + +namespace Microsoft.DotNet.Watcher.FunctionalTests +{ + public class ProjectToolScenario: IDisposable + { + private const string NugetConfigFileName = "NuGet.config"; + + public ProjectToolScenario() + { + Console.WriteLine($"The temporary test folder is {TempFolder}"); + + WorkFolder = Path.Combine(TempFolder, "work"); + PackagesFolder = Path.Combine(TempFolder, "packages"); + + CreateTestDirectory(); + } + + public string TempFolder { get; } = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + + public string WorkFolder { get; } + + public string PackagesFolder { get; } + + public void AddProject(string projectFolder) + { + var destinationFolder = Path.Combine(WorkFolder, Path.GetFileName(projectFolder)); + Console.WriteLine($"Copying project {projectFolder} to {destinationFolder}"); + + Directory.CreateDirectory(destinationFolder); + + foreach (var directory in Directory.GetDirectories(projectFolder, "*", SearchOption.AllDirectories)) + { + Directory.CreateDirectory(directory.Replace(projectFolder, destinationFolder)); + } + + foreach (var file in Directory.GetFiles(projectFolder, "*.*", SearchOption.AllDirectories)) + { + File.Copy(file, file.Replace(projectFolder, destinationFolder), true); + } + } + + public void AddNugetFeed(string feedName, string feed) + { + var tempNugetConfigFile = Path.Combine(WorkFolder, NugetConfigFileName); + + var nugetConfig = XDocument.Load(tempNugetConfigFile); + var packageSource = nugetConfig.Element("configuration").Element("packageSources"); + packageSource.Add(new XElement("add", new XAttribute("key", feedName), new XAttribute("value", feed))); + using (var stream = File.OpenWrite(tempNugetConfigFile)) + { + nugetConfig.Save(stream); + } + } + + public void AddToolToProject(string projectName, string toolName) + { + var projectFile = Path.Combine(WorkFolder, projectName, "project.json"); + Console.WriteLine($"Adding {toolName} to {projectFile}"); + + var projectJson = JObject.Parse(File.ReadAllText(projectFile)); + projectJson.Add("tools", new JObject(new JProperty(toolName, "1.0.0-*"))); + File.WriteAllText(projectFile, projectJson.ToString()); + } + + public void Restore(string project = null) + { + if (project == null) + { + project = WorkFolder; + } + else + { + project = Path.Combine(WorkFolder, project); + } + + var restore = ExecuteDotnet($"restore -v Minimal", project); + restore.WaitForExit(); + + if (restore.ExitCode != 0) + { + throw new Exception($"Exit code {restore.ExitCode}"); + } + } + + private void CreateTestDirectory() + { + Directory.CreateDirectory(WorkFolder); + var nugetConfigFilePath = FindNugetConfig(); + + var tempNugetConfigFile = Path.Combine(WorkFolder, NugetConfigFileName); + File.Copy(nugetConfigFilePath, tempNugetConfigFile); + } + + public Process ExecuteDotnet(string arguments, string workDir) + { + Console.WriteLine($"Running dotnet {arguments} in {workDir}"); + + var psi = new ProcessStartInfo("dotnet", arguments) + { + UseShellExecute = false, + WorkingDirectory = workDir + }; + + return Process.Start(psi); + } + + private string FindNugetConfig() + { + var currentDirPath = Directory.GetCurrentDirectory(); + + string nugetConfigFile; + while (true) + { + nugetConfigFile = Directory.GetFiles(currentDirPath).SingleOrDefault(f => Path.GetFileName(f).Equals(NugetConfigFileName, StringComparison.Ordinal)); + if (!string.IsNullOrEmpty(nugetConfigFile)) + { + break; + } + + currentDirPath = Path.GetDirectoryName(currentDirPath); + } + + return nugetConfigFile; + } + + public void Dispose() + { + try + { + Directory.Delete(TempFolder, recursive: true); + } + catch + { + Console.WriteLine($"Failed to delete {TempFolder}. Retrying..."); + Thread.Sleep(TimeSpan.FromSeconds(5)); + Directory.Delete(TempFolder, recursive: true); + } + } + } +} diff --git a/test/dotnet-watch.FunctionalTests/WaitForFileToChange.cs b/test/dotnet-watch.FunctionalTests/WaitForFileToChange.cs new file mode 100644 index 0000000..0105b74 --- /dev/null +++ b/test/dotnet-watch.FunctionalTests/WaitForFileToChange.cs @@ -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; + +namespace Microsoft.DotNet.Watcher.FunctionalTests +{ + public class WaitForFileToChange : IDisposable + { + private readonly FileSystemWatcher _watcher; + private readonly string _expectedFile; + + private readonly ManualResetEvent _changed = new ManualResetEvent(false); + + public WaitForFileToChange(string file) + { + _watcher = new FileSystemWatcher(Path.GetDirectoryName(file), "*" + Path.GetExtension(file)); + _expectedFile = file; + + _watcher.Changed += WatcherEvent; + _watcher.Created += WatcherEvent; + + _watcher.EnableRaisingEvents = true; + } + + private void WatcherEvent(object sender, FileSystemEventArgs e) + { + if (e.FullPath.Equals(_expectedFile, StringComparison.Ordinal)) + { + Waiters.WaitForFileToBeReadable(_expectedFile, TimeSpan.FromSeconds(10)); + _changed.Set(); + } + } + + public void Wait(TimeSpan timeout, bool expectedToChange, string errorMessage) + { + var changed = _changed.WaitOne(timeout); + if (changed != expectedToChange) + { + throw new Exception(errorMessage); + } + } + + public void Dispose() + { + _watcher.Dispose(); + _changed.Dispose(); + } + } +} diff --git a/test/dotnet-watch.FunctionalTests/Waiters.cs b/test/dotnet-watch.FunctionalTests/Waiters.cs new file mode 100644 index 0000000..b24a11e --- /dev/null +++ b/test/dotnet-watch.FunctionalTests/Waiters.cs @@ -0,0 +1,67 @@ +// 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.IO; +using System.Threading; + +namespace Microsoft.DotNet.Watcher.FunctionalTests +{ + public static class Waiters + { + public static void WaitForFileToBeReadable(string file, TimeSpan timeout) + { + var watch = new Stopwatch(); + + watch.Start(); + while (watch.Elapsed < timeout) + { + try + { + File.ReadAllText(file); + watch.Stop(); + return; + } + catch + { + } + Thread.Sleep(500); + } + watch.Stop(); + + throw new Exception($"{file} is not readable."); + } + + public static void WaitForProcessToStop(int processId, TimeSpan timeout, bool expectedToStop, string errorMessage) + { + Process process = null; + + try + { + process = Process.GetProcessById(processId); + } + catch + { + } + + var watch = new Stopwatch(); + watch.Start(); + while (watch.Elapsed < timeout) + { + if (process == null || process.HasExited) + { + break; + } + Thread.Sleep(500); + } + watch.Stop(); + + bool isStopped = process == null || process.HasExited; + if (isStopped != expectedToStop) + { + throw new Exception(errorMessage); + } + } + } +} diff --git a/test/dotnet-watch.FunctionalTests/dotnet-watch.FunctionalTests.xproj b/test/dotnet-watch.FunctionalTests/dotnet-watch.FunctionalTests.xproj new file mode 100644 index 0000000..7fd8ef1 --- /dev/null +++ b/test/dotnet-watch.FunctionalTests/dotnet-watch.FunctionalTests.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 16bade2f-1184-4518-8a70-b68a19d0805b + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/dotnet-watch.FunctionalTests/project.json b/test/dotnet-watch.FunctionalTests/project.json new file mode 100644 index 0000000..70c64e3 --- /dev/null +++ b/test/dotnet-watch.FunctionalTests/project.json @@ -0,0 +1,23 @@ +{ + "compilationOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk" + }, + "dependencies": { + "dotnet-test-xunit": "1.0.0-dev-*", + "Microsoft.Extensions.DependencyInjection": "1.0.0-*", + "Microsoft.DotNet.Watcher.Core": "1.0.0-*", + "Microsoft.NETCore.Platforms": "1.0.1-*", + "Newtonsoft.Json": "8.0.2", + "xunit": "2.1.0", + "System.Diagnostics.Process": "4.1.0-*", + "System.IO.FileSystem.Watcher": "4.0.0-*", + "System.Xml.XDocument": "4.0.10-*" + }, + "frameworks": { + "dnxcore50": { + "imports": "portable-net451+win8" + } + }, + "testRunner": "xunit" +} \ No newline at end of file