- E2E test
- File watcher that takes the globbing patterns into account - A big rewrite of the core algorithm
This commit is contained in:
parent
b62d90d7dc
commit
c43c37e7f9
|
|
@ -15,6 +15,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||||
NuGet.Config = NuGet.Config
|
NuGet.Config = NuGet.Config
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{D3DA3BBB-E206-404F-AEE6-17FB9B6F1221}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -36,5 +70,11 @@ Global
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{8A8CEABC-AC47-43FF-A5DF-69224F7E1F46} = {66517987-2A5A-4330-B130-207039378FD4}
|
{8A8CEABC-AC47-43FF-A5DF-69224F7E1F46} = {66517987-2A5A-4330-B130-207039378FD4}
|
||||||
{D3DA3BBB-E206-404F-AEE6-17FB9B6F1221} = {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
|
EndGlobalSection
|
||||||
EndGlobal
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.DotNet.Watcher.Core.Internal;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Core
|
namespace Microsoft.DotNet.Watcher.Core
|
||||||
{
|
{
|
||||||
public class DotNetWatcher
|
public class DotNetWatcher
|
||||||
{
|
{
|
||||||
private readonly Func<string, IFileWatcher> _fileWatcherFactory;
|
private readonly Func<IFileWatcher> _fileWatcherFactory;
|
||||||
private readonly Func<IProcessWatcher> _processWatcherFactory;
|
private readonly Func<IProcessWatcher> _processWatcherFactory;
|
||||||
private readonly IProjectProvider _projectProvider;
|
private readonly IProjectProvider _projectProvider;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
|
@ -22,7 +22,7 @@ namespace Microsoft.DotNet.Watcher.Core
|
||||||
public bool ExitOnChange { get; set; }
|
public bool ExitOnChange { get; set; }
|
||||||
|
|
||||||
public DotNetWatcher(
|
public DotNetWatcher(
|
||||||
Func<string, IFileWatcher> fileWatcherFactory,
|
Func<IFileWatcher> fileWatcherFactory,
|
||||||
Func<IProcessWatcher> processWatcherFactory,
|
Func<IProcessWatcher> processWatcherFactory,
|
||||||
IProjectProvider projectProvider,
|
IProjectProvider projectProvider,
|
||||||
ILoggerFactory loggerFactory)
|
ILoggerFactory loggerFactory)
|
||||||
|
|
@ -86,7 +86,7 @@ namespace Microsoft.DotNet.Watcher.Core
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var project = await WaitForValidProjectJsonAsync(projectFile, cancellationToken);
|
await WaitForValidProjectJsonAsync(projectFile, cancellationToken);
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
using (var currentRunCancellationSource = new CancellationTokenSource())
|
using (var currentRunCancellationSource = new CancellationTokenSource())
|
||||||
|
|
@ -94,7 +94,7 @@ namespace Microsoft.DotNet.Watcher.Core
|
||||||
cancellationToken,
|
cancellationToken,
|
||||||
currentRunCancellationSource.Token))
|
currentRunCancellationSource.Token))
|
||||||
{
|
{
|
||||||
var fileWatchingTask = WaitForProjectFileToChangeAsync(project, combinedCancellationSource.Token);
|
var fileWatchingTask = WaitForProjectFileToChangeAsync(projectFile, combinedCancellationSource.Token);
|
||||||
var dotnetTask = WaitForDotnetToExitAsync(dotnetArgumentsAsString, workingDir, combinedCancellationSource.Token);
|
var dotnetTask = WaitForDotnetToExitAsync(dotnetArgumentsAsString, workingDir, combinedCancellationSource.Token);
|
||||||
|
|
||||||
var tasksToWait = new Task[] { dotnetTask, fileWatchingTask };
|
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...");
|
_logger.LogInformation("Waiting for a file to change before restarting dotnet...");
|
||||||
// Now wait 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
|
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 projectWatcher.WaitForChangeAsync(cancellationToken);
|
||||||
return await WatchForFileChangeAsync(fileWatcher, cancellationToken);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,37 +160,33 @@ namespace Microsoft.DotNet.Watcher.Core
|
||||||
|
|
||||||
var dotnetWatcher = _processWatcherFactory();
|
var dotnetWatcher = _processWatcherFactory();
|
||||||
int dotnetProcessId = dotnetWatcher.Start("dotnet", dotnetArguments, workingDir);
|
int dotnetProcessId = dotnetWatcher.Start("dotnet", dotnetArguments, workingDir);
|
||||||
_logger.LogInformation($"dotnet run process id: {dotnetProcessId}");
|
_logger.LogInformation($"dotnet process id: {dotnetProcessId}");
|
||||||
|
|
||||||
return dotnetWatcher.WaitForExitAsync(cancellationToken);
|
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)
|
while (true)
|
||||||
{
|
{
|
||||||
|
IProject project;
|
||||||
string errors;
|
string errors;
|
||||||
if (_projectProvider.TryReadProject(projectFile, out project, out errors))
|
if (_projectProvider.TryReadProject(projectFile, out project, out errors))
|
||||||
{
|
{
|
||||||
return project;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogError($"Error(s) reading project file '{projectFile}': ");
|
_logger.LogError($"Error(s) reading project file '{projectFile}': ");
|
||||||
_logger.LogError(errors);
|
_logger.LogError(errors);
|
||||||
_logger.LogInformation("Fix the error to continue.");
|
_logger.LogInformation("Fix the error to continue.");
|
||||||
|
|
||||||
using (var fileWatcher = _fileWatcherFactory(Path.GetDirectoryName(projectFile)))
|
using (var projectWatcher = CreateProjectWatcher(projectFile, watchProjectJsonOnly: true))
|
||||||
{
|
{
|
||||||
fileWatcher.WatchFile(projectFile);
|
await projectWatcher.WaitForChangeAsync(cancellationToken);
|
||||||
fileWatcher.WatchProject(projectFile);
|
|
||||||
|
|
||||||
await WatchForFileChangeAsync(fileWatcher, cancellationToken);
|
|
||||||
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation($"File changed: {projectFile}");
|
_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;
|
return new ProjectWatcher(projectFile, watchProjectJsonOnly, _fileWatcherFactory, _projectProvider);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DotNetWatcher CreateDefault(ILoggerFactory loggerFactory)
|
public static DotNetWatcher CreateDefault(ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
return new DotNetWatcher(
|
return new DotNetWatcher(
|
||||||
fileWatcherFactory: root => new FileWatcher(root),
|
fileWatcherFactory: () => new FileWatcher(),
|
||||||
processWatcherFactory: () => new ProcessWatcher(),
|
processWatcherFactory: () => new ProcessWatcher(),
|
||||||
projectProvider: new ProjectProvider(),
|
projectProvider: new ProjectProvider(),
|
||||||
loggerFactory: loggerFactory);
|
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;
|
using System;
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Core
|
namespace Microsoft.DotNet.Watcher.Core.Internal
|
||||||
{
|
{
|
||||||
public interface IFileWatcher : IDisposable
|
public interface IFileWatcher : IDisposable
|
||||||
{
|
{
|
||||||
event Action<string> OnChanged;
|
event Action<string> OnFileChange;
|
||||||
|
|
||||||
void WatchDirectory(string path, string extension);
|
void WatchDirectory(string directory);
|
||||||
|
|
||||||
bool WatchFile(string path);
|
|
||||||
|
|
||||||
void WatchProject(string path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Core
|
namespace Microsoft.DotNet.Watcher.Core.Internal
|
||||||
{
|
{
|
||||||
public interface IProcessWatcher
|
public interface IProcessWatcher
|
||||||
{
|
{
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Core
|
namespace Microsoft.DotNet.Watcher.Core.Internal
|
||||||
{
|
{
|
||||||
public interface IProject
|
public interface IProject
|
||||||
{
|
{
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Core
|
namespace Microsoft.DotNet.Watcher.Core.Internal
|
||||||
{
|
{
|
||||||
public interface IProjectProvider
|
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 System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.PlatformAbstractions;
|
using Microsoft.Extensions.PlatformAbstractions;
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Core
|
namespace Microsoft.DotNet.Watcher.Core.Internal
|
||||||
{
|
{
|
||||||
public class ProcessWatcher : IProcessWatcher
|
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;
|
private Process _runningProcess;
|
||||||
|
|
||||||
|
|
@ -39,7 +39,13 @@ namespace Microsoft.DotNet.Watcher.Core
|
||||||
|
|
||||||
public Task<int> WaitForExitAsync(CancellationToken cancellationToken)
|
public Task<int> WaitForExitAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
cancellationToken.Register(() => KillProcess(_runningProcess?.Id));
|
cancellationToken.Register(() =>
|
||||||
|
{
|
||||||
|
if (_runningProcess != null)
|
||||||
|
{
|
||||||
|
_runningProcess.KillTree(_processKillTimeout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return Task.Run(() =>
|
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 System.Linq;
|
||||||
using Microsoft.DotNet.ProjectModel.Graph;
|
using Microsoft.DotNet.ProjectModel.Graph;
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Core
|
namespace Microsoft.DotNet.Watcher.Core.Internal
|
||||||
{
|
{
|
||||||
internal class Project : IProject
|
internal class Project : IProject
|
||||||
{
|
{
|
||||||
|
|
@ -7,7 +7,7 @@ using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.DotNet.ProjectModel;
|
using Microsoft.DotNet.ProjectModel;
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Core
|
namespace Microsoft.DotNet.Watcher.Core.Internal
|
||||||
{
|
{
|
||||||
public class ProjectProvider : IProjectProvider
|
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.Logging.Abstractions": "1.0.0-*",
|
||||||
"Microsoft.Extensions.FileSystemGlobbing": "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": {
|
"frameworks": {
|
||||||
"dnxcore50": {
|
"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"
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue