- File watcher that takes the globbing patterns into account
- A big rewrite of the core algorithm
This commit is contained in:
Victor Hurdugaci 2016-02-19 14:51:08 -08:00
Родитель b62d90d7dc
Коммит c43c37e7f9
40 изменённых файлов: 1463 добавлений и 406 удалений

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

@ -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

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

@ -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<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TValue> factory)
{
lock (dictionary)
{
TValue value;
if (!dictionary.TryGetValue(key, out value))
{
value = factory(key);
dictionary[key] = value;
}
return value;
}
}
}
}

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

@ -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<string, IFileWatcher> _fileWatcherFactory;
private readonly Func<IFileWatcher> _fileWatcherFactory;
private readonly Func<IProcessWatcher> _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<string, IFileWatcher> fileWatcherFactory,
Func<IFileWatcher> fileWatcherFactory,
Func<IProcessWatcher> 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<string> WaitForProjectFileToChangeAsync(IProject project, CancellationToken cancellationToken)
private async Task<string> 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<IProject> 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<string> WatchForFileChangeAsync(IFileWatcher fileWatcher, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<string>();
cancellationToken.Register(() => tcs.TrySetResult(null));
Action<string> 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);
}
}
}

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

@ -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();
}
}
}

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

@ -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<string> _files = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, HashSet<string>> _directories = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
private readonly List<IWatcherRoot> _watchers = new List<IWatcherRoot>();
internal FileWatcher()
{
}
public FileWatcher(string path)
{
AddWatcher(path);
}
public event Action<string> OnChanged;
public void WatchDirectory(string path, string extension)
{
var extensions = _directories.GetOrAdd(path, _ => new HashSet<string>(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<string> 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;
}
}
}

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

@ -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; }
}
}

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

@ -3,16 +3,12 @@
using System;
namespace Microsoft.DotNet.Watcher.Core
namespace Microsoft.DotNet.Watcher.Core.Internal
{
public interface IFileWatcher : IDisposable
{
event Action<string> OnChanged;
event Action<string> OnFileChange;
void WatchDirectory(string path, string extension);
bool WatchFile(string path);
void WatchProject(string path);
void WatchDirectory(string directory);
}
}

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

@ -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
{

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

@ -3,7 +3,7 @@
using System.Collections.Generic;
namespace Microsoft.DotNet.Watcher.Core
namespace Microsoft.DotNet.Watcher.Core.Internal
{
public interface IProject
{

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

@ -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
{

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

@ -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<string, FileSystemWatcher> _watchers = new Dictionary<string, FileSystemWatcher>();
public event Action<string> 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));
}
}
}
}

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

@ -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<int> 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();
}
}
}
}

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

@ -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
{

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

@ -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
{

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

@ -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<int> GetAllChildIdsUnix(Process process, TimeSpan timeout)
{
var children = new HashSet<int>();
GetAllChildIdsUnix(process.Id, children, timeout);
return children;
}
private static void GetAllChildIdsUnix(int parentId, ISet<int> 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;
}
}
}

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

@ -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<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);
}
}
}
}
}

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

@ -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": {

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

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0.23107" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.23107</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>f7734e61-f510-41e0-ad15-301a64081cd1</ProjectGuid>
<RootNamespace>AppWithDeps</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

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

@ -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();
}
}
}

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

@ -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-*"
}
}
}
}

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

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0.23107" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.23107</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>2f48041a-f7d1-478f-9c38-d41f0f05e8ca</ProjectGuid>
<RootNamespace>Dependency</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

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

@ -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
{
}
}

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

@ -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-*"
}
}
}
}

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

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0.23107" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.23107</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>2ab1a28b-2022-49ea-af77-ac8a875915cc</ProjectGuid>
<RootNamespace>GlobbingApp</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

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

@ -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();
}
}
}

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

@ -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"
}
}

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

@ -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
{
}
}

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

@ -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-*"
}
}
}
}

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

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0.23107" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.23107</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>4f0d8a80-221f-4bcb-822e-44a0655f537e</ProjectGuid>
<RootNamespace>NoDepsApp</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

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

@ -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();
}
}
}
}

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

@ -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-*"
}
}
}
}

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

@ -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; }
}
}
}

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

@ -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; }
}
}
}

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

@ -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));
}
}
}
}

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

@ -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;
}
}
}

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

@ -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);
}
}
}
}

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

@ -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();
}
}
}

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

@ -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);
}
}
}
}

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

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<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)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>16bade2f-1184-4518-8a70-b68a19d0805b</ProjectGuid>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

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

@ -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"
}