- E2E test
- File watcher that takes the globbing patterns into account - A big rewrite of the core algorithm
This commit is contained in:
Родитель
b62d90d7dc
Коммит
c43c37e7f9
|
@ -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"
|
||||
}
|
Загрузка…
Ссылка в новой задаче