diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..bdaa5ba982 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,50 @@ +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain + +*.jpg binary +*.png binary +*.gif binary + +*.cs text=auto diff=csharp +*.vb text=auto +*.resx text=auto +*.c text=auto +*.cpp text=auto +*.cxx text=auto +*.h text=auto +*.hxx text=auto +*.py text=auto +*.rb text=auto +*.java text=auto +*.html text=auto +*.htm text=auto +*.css text=auto +*.scss text=auto +*.sass text=auto +*.less text=auto +*.js text=auto +*.lisp text=auto +*.clj text=auto +*.sql text=auto +*.php text=auto +*.lua text=auto +*.m text=auto +*.asm text=auto +*.erl text=auto +*.fs text=auto +*.fsx text=auto +*.hs text=auto + +*.csproj text=auto +*.vbproj text=auto +*.fsproj text=auto +*.dbproj text=auto +*.sln text=auto eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..ac82da7568 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +[Oo]bj/ +[Bb]in/ +TestResults/ +.nuget/ +_ReSharper.*/ +packages/ +artifacts/ +PublishProfiles/ +*.user +*.suo +*.cache +*.docstates +_ReSharper.* +nuget.exe +*net45.csproj +*net451.csproj +*k10.csproj +*.psess +*.vsp +*.pidb +*.userprefs +*DS_Store +*.ncrunchsolution +*.*sdf +*.ipch +*.sln.ide +project.lock.json diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..f01ee5a79a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: csharp +sudo: false +env: + - MONO_THREADS_PER_CPU=2000 +os: + - linux + - osx +script: + - ./build.sh --quiet verify \ No newline at end of file diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000000..03704957e8 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000000..636a7618d3 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,7 @@ +init: + - git config --global core.autocrlf true +build_script: + - build.cmd --quiet verify +clone_depth: 1 +test: off +deploy: off \ No newline at end of file diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000000..177997c42e --- /dev/null +++ b/build.cmd @@ -0,0 +1,39 @@ +@echo off +cd %~dp0 + +SETLOCAL +SET NUGET_VERSION=latest +SET CACHED_NUGET=%LocalAppData%\NuGet\nuget.%NUGET_VERSION%.exe +SET BUILDCMD_KOREBUILD_VERSION="" +SET BUILDCMD_DNX_VERSION="" + +IF EXIST %CACHED_NUGET% goto copynuget +echo Downloading latest version of NuGet.exe... +IF NOT EXIST %LocalAppData%\NuGet md %LocalAppData%\NuGet +@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://dist.nuget.org/win-x86-commandline/%NUGET_VERSION%/nuget.exe' -OutFile '%CACHED_NUGET%'" + +:copynuget +IF EXIST .nuget\nuget.exe goto restore +md .nuget +copy %CACHED_NUGET% .nuget\nuget.exe > nul + +:restore +IF EXIST packages\KoreBuild goto run +IF %BUILDCMD_KOREBUILD_VERSION%=="" ( + .nuget\nuget.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre +) ELSE ( + .nuget\nuget.exe install KoreBuild -version %BUILDCMD_KOREBUILD_VERSION% -ExcludeVersion -o packages -nocache -pre +) +.nuget\nuget.exe install Sake -ExcludeVersion -Out packages + +IF "%SKIP_DNX_INSTALL%"=="1" goto run +IF %BUILDCMD_DNX_VERSION%=="" ( + CALL packages\KoreBuild\build\dnvm upgrade -runtime CLR -arch x86 +) ELSE ( + CALL packages\KoreBuild\build\dnvm install %BUILDCMD_DNX_VERSION% -runtime CLR -arch x86 -a default +) +CALL packages\KoreBuild\build\dnvm install default -runtime CoreCLR -arch x86 + +:run +CALL packages\KoreBuild\build\dnvm use default -runtime CLR -arch x86 +packages\Sake\tools\Sake.exe -I packages\KoreBuild\build -f makefile.shade %* \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000000..0c66139817 --- /dev/null +++ b/build.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +if test `uname` = Darwin; then + cachedir=~/Library/Caches/KBuild +else + if [ -z $XDG_DATA_HOME ]; then + cachedir=$HOME/.local/share + else + cachedir=$XDG_DATA_HOME; + fi +fi +mkdir -p $cachedir +nugetVersion=latest +cachePath=$cachedir/nuget.$nugetVersion.exe + +url=https://dist.nuget.org/win-x86-commandline/$nugetVersion/nuget.exe + +if test ! -f $cachePath; then + wget -O $cachePath $url 2>/dev/null || curl -o $cachePath --location $url /dev/null +fi + +if test ! -e .nuget; then + mkdir .nuget + cp $cachePath .nuget/nuget.exe +fi + +if test ! -d packages/KoreBuild; then + mono .nuget/nuget.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre + mono .nuget/nuget.exe install Sake -ExcludeVersion -Out packages +fi + +if ! type dnvm > /dev/null 2>&1; then + source packages/KoreBuild/build/dnvm.sh +fi + +if ! type dnx > /dev/null 2>&1; then + dnvm upgrade +fi + +mono packages/Sake/tools/Sake.exe -I packages/KoreBuild/build -f makefile.shade "$@" + diff --git a/dnx-watch.sln b/dnx-watch.sln new file mode 100644 index 0000000000..6b01701d71 --- /dev/null +++ b/dnx-watch.sln @@ -0,0 +1,49 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{66517987-2A5A-4330-B130-207039378FD4}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Dnx.Watcher", "src\Microsoft.Dnx.Watcher\Microsoft.Dnx.Watcher.xproj", "{8A8CEABC-AC47-43FF-A5DF-69224F7E1F46}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Dnx.Watcher.Core", "src\Microsoft.Dnx.Watcher.Core\Microsoft.Dnx.Watcher.Core.xproj", "{D3DA3BBB-E206-404F-AEE6-17FB9B6F1221}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8321E0D1-9A47-4D2F-AED8-3AE636D44E35}" + ProjectSection(SolutionItems) = preProject + global.json = global.json + NuGet.Config = NuGet.Config + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{442A6A17-4C5A-4E11-B547-A554063FD338}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Dnx.Watcher.Tests", "test\Microsoft.Dnx.Watcher.Tests\Microsoft.Dnx.Watcher.Tests.xproj", "{640D190B-26DB-4DDE-88EE-55814C86C43E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8A8CEABC-AC47-43FF-A5DF-69224F7E1F46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A8CEABC-AC47-43FF-A5DF-69224F7E1F46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A8CEABC-AC47-43FF-A5DF-69224F7E1F46}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A8CEABC-AC47-43FF-A5DF-69224F7E1F46}.Release|Any CPU.Build.0 = Release|Any CPU + {D3DA3BBB-E206-404F-AEE6-17FB9B6F1221}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU + {640D190B-26DB-4DDE-88EE-55814C86C43E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {640D190B-26DB-4DDE-88EE-55814C86C43E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {640D190B-26DB-4DDE-88EE-55814C86C43E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {640D190B-26DB-4DDE-88EE-55814C86C43E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {8A8CEABC-AC47-43FF-A5DF-69224F7E1F46} = {66517987-2A5A-4330-B130-207039378FD4} + {D3DA3BBB-E206-404F-AEE6-17FB9B6F1221} = {66517987-2A5A-4330-B130-207039378FD4} + {640D190B-26DB-4DDE-88EE-55814C86C43E} = {442A6A17-4C5A-4E11-B547-A554063FD338} + EndGlobalSection +EndGlobal diff --git a/global.json b/global.json new file mode 100644 index 0000000000..553b2244a3 --- /dev/null +++ b/global.json @@ -0,0 +1,3 @@ +{ + "projects": [ "src"] +} diff --git a/makefile.shade b/makefile.shade new file mode 100644 index 0000000000..562494d144 --- /dev/null +++ b/makefile.shade @@ -0,0 +1,7 @@ + +var VERSION='0.1' +var FULL_VERSION='0.1' +var AUTHORS='Microsoft Open Technologies, Inc.' + +use-standard-lifecycle +k-standard-goals diff --git a/src/Microsoft.Dnx.Watcher.Core/Abstractions/IFileWatcher.cs b/src/Microsoft.Dnx.Watcher.Core/Abstractions/IFileWatcher.cs new file mode 100644 index 0000000000..8dfd44f1e5 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/Abstractions/IFileWatcher.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Dnx.Watcher.Core +{ + public interface IFileWatcher : IDisposable + { + event Action OnChanged; + + void WatchDirectory(string path, string extension); + + bool WatchFile(string path); + + void WatchProject(string path); + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/Abstractions/IProcessWatcher.cs b/src/Microsoft.Dnx.Watcher.Core/Abstractions/IProcessWatcher.cs new file mode 100644 index 0000000000..d075bd519a --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/Abstractions/IProcessWatcher.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Dnx.Watcher.Core +{ + public interface IProcessWatcher + { + int Start(string executable, string arguments, string workingDir); + + Task WaitForExitAsync(CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/Abstractions/IProject.cs b/src/Microsoft.Dnx.Watcher.Core/Abstractions/IProject.cs new file mode 100644 index 0000000000..cad54e0997 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/Abstractions/IProject.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.Dnx.Watcher.Core +{ + public interface IProject + { + string ProjectFile { get; } + + IEnumerable Files { get; } + + IEnumerable ProjectDependencies { get; } + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/Abstractions/IProjectProvider.cs b/src/Microsoft.Dnx.Watcher.Core/Abstractions/IProjectProvider.cs new file mode 100644 index 0000000000..77abc7bbb1 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/Abstractions/IProjectProvider.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Dnx.Watcher.Core +{ + public interface IProjectProvider + { + bool TryReadProject(string projectFile, out IProject project, out string errors); + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/DictionaryExtensions.cs b/src/Microsoft.Dnx.Watcher.Core/DictionaryExtensions.cs new file mode 100644 index 0000000000..0b318da824 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/DictionaryExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace System.Collections.Generic +{ + internal static class DictionaryExtensions + { + public static TValue GetOrAdd(this IDictionary dictionary, TKey key, Func factory) + { + lock (dictionary) + { + TValue value; + if (!dictionary.TryGetValue(key, out value)) + { + value = factory(key); + dictionary[key] = value; + } + + return value; + } + } + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/DnxWatcher.cs b/src/Microsoft.Dnx.Watcher.Core/DnxWatcher.cs new file mode 100644 index 0000000000..b71de61d59 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/DnxWatcher.cs @@ -0,0 +1,225 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Framework.Logging; + +namespace Microsoft.Dnx.Watcher.Core +{ + public class DnxWatcher + { + private readonly Func _fileWatcherFactory; + private readonly Func _processWatcherFactory; + private readonly IProjectProvider _projectProvider; + private readonly ILoggerFactory _loggerFactory; + + private readonly ILogger _logger; + + public DnxWatcher( + Func fileWatcherFactory, + Func processWatcherFactory, + IProjectProvider projectProvider, + ILoggerFactory loggerFactory) + { + _fileWatcherFactory = fileWatcherFactory; + _processWatcherFactory = processWatcherFactory; + _projectProvider = projectProvider; + _loggerFactory = loggerFactory; + + _logger = _loggerFactory.CreateLogger(nameof(DnxWatcher)); + } + public async Task WatchAsync(string projectFile, string[] dnxArguments, string workingDir, CancellationToken cancellationToken) + { + dnxArguments = new string[] { "--project", projectFile } + .Concat(dnxArguments) + .Select(arg => + { + // If the argument has spaces, make sure we quote it + if (arg.Contains(" ") || arg.Contains("\t")) + { + return $"\"{arg}\""; + } + + return arg; + }) + .ToArray(); + + var dnxArgumentsAsString = string.Join(" ", dnxArguments); + + while (true) + { + var project = await WaitForValidProjectJsonAsync(projectFile, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + using (var currentRunCancellationSource = new CancellationTokenSource()) + using (var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + currentRunCancellationSource.Token)) + { + var fileWatchingTask = WaitForProjectFileToChangeAsync(project, combinedCancellationSource.Token); + var dnxTask = WaitForDnxToExitAsync(dnxArgumentsAsString, workingDir, combinedCancellationSource.Token); + + var tasksToWait = new Task[] { dnxTask, fileWatchingTask }; + + int finishedTaskIndex = Task.WaitAny(tasksToWait, cancellationToken); + + // Regardless of the outcome, make sure everything is cancelled + // and wait for dnx to exit. We don't want orphan processes + currentRunCancellationSource.Cancel(); + Task.WaitAll(tasksToWait); + + cancellationToken.ThrowIfCancellationRequested(); + + if (finishedTaskIndex == 0) + { + // This is the dnx task + var dnxExitCode = dnxTask.Result; + + if (dnxExitCode == 0) + { + _logger.LogInformation($"dnx exit code: {dnxExitCode}"); + } + else + { + _logger.LogError($"dnx exit code: {dnxExitCode}"); + } + + _logger.LogInformation("Waiting for a file to change before restarting dnx..."); + // Now wait for a file to change before restarting dnx + await WaitForProjectFileToChangeAsync(project, cancellationToken); + } + else + { + // This is a file watcher task + string changedFile = fileWatchingTask.Result; + _logger.LogInformation($"File changed: {fileWatchingTask.Result}"); + } + } + } + } + + private async Task WaitForProjectFileToChangeAsync(IProject project, CancellationToken cancellationToken) + { + using (var fileWatcher = _fileWatcherFactory(Path.GetDirectoryName(project.ProjectFile))) + { + AddProjectAndDependeciesToWatcher(project, fileWatcher); + return await WatchForFileChangeAsync(fileWatcher, cancellationToken); + } + } + + private Task WaitForDnxToExitAsync(string dnxArguments, string workingDir, CancellationToken cancellationToken) + { + _logger.LogInformation($"Running dnx with the following arguments: {dnxArguments}"); + + var dnxWatcher = _processWatcherFactory(); + int dnxProcessId = dnxWatcher.Start("dnx", dnxArguments, workingDir); + _logger.LogInformation($"dnx process id: {dnxProcessId}"); + + return dnxWatcher.WaitForExitAsync(cancellationToken); + } + + private async Task WaitForValidProjectJsonAsync(string projectFile, CancellationToken cancellationToken) + { + IProject project = null; + + while (true) + { + string errors; + if (_projectProvider.TryReadProject(projectFile, out project, out errors)) + { + return project; + } + + _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))) + { + fileWatcher.WatchFile(projectFile); + fileWatcher.WatchProject(projectFile); + + await WatchForFileChangeAsync(fileWatcher, cancellationToken); + + if (cancellationToken.IsCancellationRequested) + { + return null; + } + + _logger.LogInformation($"File changed: {projectFile}"); + } + } + } + + private void AddProjectAndDependeciesToWatcher(string projectFile, IFileWatcher fileWatcher) + { + 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) + { + //var fullProjectFilePath = Path.Combine(Path.GetDirectoryName(project.ProjectFile), projFile); + + AddProjectAndDependeciesToWatcher(projFile, fileWatcher); + } + } + + private Task WatchForFileChangeAsync(IFileWatcher fileWatcher, CancellationToken cancellationToken) + { + return Task.Run(() => + { + using (var fileChangeEvent = new ManualResetEvent(false)) + { + string changedFile = null; + + fileWatcher.OnChanged += path => + { + changedFile = path; + fileChangeEvent.Set(); + }; + + while (!cancellationToken.IsCancellationRequested && + !fileChangeEvent.WaitOne(500)) + { + } + + return changedFile; + } + }); + } + + public static DnxWatcher CreateDefault(ILoggerFactory loggerFactory) + { + return new DnxWatcher( + fileWatcherFactory: root => { return new FileWatcher(root); }, + processWatcherFactory: () => { return new ProcessWatcher(); }, + projectProvider: new ProjectProvider(), + loggerFactory: loggerFactory); + } + + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/External/Runtime/Constants.cs b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/Constants.cs new file mode 100644 index 0000000000..9c2e502b02 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/Constants.cs @@ -0,0 +1,34 @@ +// 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.Dnx.Runtime +{ + internal static class Constants + { + public const string BootstrapperExeName = "dnx"; + public const string BootstrapperFullName = "Microsoft .NET Execution environment"; + public const string DefaultLocalRuntimeHomeDir = ".dnx"; + public const string RuntimeShortName = "dnx"; + public const string RuntimeLongName = "Microsoft DNX"; + public const string RuntimeNamePrefix = RuntimeShortName + "-"; + public const string WebConfigRuntimeVersion = RuntimeNamePrefix + "version"; + public const string WebConfigRuntimeFlavor = RuntimeNamePrefix + "clr"; + public const string WebConfigRuntimeAppBase = RuntimeNamePrefix + "app-base"; + public const string WebConfigBootstrapperVersion = "bootstrapper-version"; + public const string WebConfigRuntimePath = "runtime-path"; + public const string BootstrapperHostName = RuntimeShortName + ".host"; + public const string BootstrapperClrName = RuntimeShortName + ".clr"; + + public const int LockFileVersion = 2; + + public static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10); + public static readonly string AppConfigurationFileName = "app.config"; + + public static readonly Version Version35 = new Version(3, 5); + public static readonly Version Version40 = new Version(4, 0); + public static readonly Version Version50 = new Version(5, 0); + public static readonly Version Version10_0 = new Version(10, 0); + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/External/Runtime/FileFormatException.cs b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/FileFormatException.cs new file mode 100644 index 0000000000..e895f724f7 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/FileFormatException.cs @@ -0,0 +1,123 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Dnx.Runtime.Json; + +namespace Microsoft.Dnx.Runtime +{ + internal sealed class FileFormatException : Exception + { + private FileFormatException(string message) : + base(message) + { + } + + private FileFormatException(string message, Exception innerException) : + base(message, innerException) + { + } + + public string Path { get; private set; } + public int Line { get; private set; } + public int Column { get; private set; } + + public override string ToString() + { + return $"{Path}({Line},{Column}): Error: {base.ToString()}"; + } + + internal static FileFormatException Create(Exception exception, string filePath) + { + if (exception is JsonDeserializerException) + { + return new FileFormatException(exception.Message, exception) + .WithFilePath(filePath) + .WithLineInfo((JsonDeserializerException)exception); + } + else + { + return new FileFormatException(exception.Message, exception) + .WithFilePath(filePath); + } + } + + internal static FileFormatException Create(Exception exception, JsonValue jsonValue, string filePath) + { + var result = Create(exception, jsonValue) + .WithFilePath(filePath); + + return result; + } + + internal static FileFormatException Create(Exception exception, JsonValue jsonValue) + { + var result = new FileFormatException(exception.Message, exception) + .WithLineInfo(jsonValue); + + return result; + } + + internal static FileFormatException Create(string message, JsonValue jsonValue, string filePath) + { + var result = Create(message, jsonValue) + .WithFilePath(filePath); + + return result; + } + + internal static FileFormatException Create(string message, string filePath) + { + var result = new FileFormatException(message) + .WithFilePath(filePath); + + return result; + } + + internal static FileFormatException Create(string message, JsonValue jsonValue) + { + var result = new FileFormatException(message) + .WithLineInfo(jsonValue); + + return result; + } + + internal FileFormatException WithFilePath(string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Path = path; + + return this; + } + + private FileFormatException WithLineInfo(JsonValue value) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + Line = value.Line; + Column = value.Column; + + return this; + } + + private FileFormatException WithLineInfo(JsonDeserializerException exception) + { + if (exception == null) + { + throw new ArgumentNullException(nameof(exception)); + } + + Line = exception.Line; + Column = exception.Column; + + return this; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Dnx.Watcher.Core/External/Runtime/LockFile.cs b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/LockFile.cs new file mode 100644 index 0000000000..8f07a5f54c --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/LockFile.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Dnx.Runtime +{ + public class LockFile + { + public int Version { get; set; } + + public IList ProjectLibraries { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/src/Microsoft.Dnx.Watcher.Core/External/Runtime/LockFileProjectLibrary.cs b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/LockFileProjectLibrary.cs new file mode 100644 index 0000000000..a131fa32ce --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/LockFileProjectLibrary.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Dnx.Runtime +{ + public class LockFileProjectLibrary + { + public string Name { get; set; } + + public string Path { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.Dnx.Watcher.Core/External/Runtime/LockFileReader.cs b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/LockFileReader.cs new file mode 100644 index 0000000000..f55b198ace --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/LockFileReader.cs @@ -0,0 +1,220 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Versioning; +using System.Threading; +using Microsoft.Dnx.Runtime.Json; +using NuGet; + +namespace Microsoft.Dnx.Runtime +{ + internal class LockFileReader + { + public const string LockFileName = "project.lock.json"; + + public LockFile Read(string filePath) + { + using (var stream = OpenFileStream(filePath)) + { + try + { + return Read(stream); + } + catch (FileFormatException ex) + { + throw ex.WithFilePath(filePath); + } + catch (Exception ex) + { + throw FileFormatException.Create(ex, filePath); + } + } + } + + private static FileStream OpenFileStream(string filePath) + { + // Retry 3 times before re-throw the exception. + // It mitigates the race condition when DTH read lock file while VS is restoring projects. + + int retry = 3; + while (true) + { + try + { + return new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + } + catch (Exception) + { + if (retry > 0) + { + retry--; + Thread.Sleep(100); + } + else + { + throw; + } + } + } + + } + + internal LockFile Read(Stream stream) + { + try + { + var reader = new StreamReader(stream); + var jobject = JsonDeserializer.Deserialize(reader) as JsonObject; + + if (jobject != null) + { + return ReadLockFile(jobject); + } + else + { + throw new InvalidDataException(); + } + } + catch + { + // Ran into parsing errors, mark it as unlocked and out-of-date + return new LockFile + { + Version = int.MinValue + }; + } + } + + private LockFile ReadLockFile(JsonObject cursor) + { + var lockFile = new LockFile(); + lockFile.Version = ReadInt(cursor, "version", defaultValue: int.MinValue); + ReadLibrary(cursor.ValueAsJsonObject("libraries"), lockFile); + + return lockFile; + } + + private void ReadLibrary(JsonObject json, LockFile lockFile) + { + if (json == null) + { + return; + } + + foreach (var key in json.Keys) + { + var value = json.ValueAsJsonObject(key); + if (value == null) + { + throw FileFormatException.Create("The value type is not object.", json.Value(key)); + } + + var parts = key.Split(new[] { '/' }, 2); + var name = parts[0]; + var version = parts.Length == 2 ? SemanticVersion.Parse(parts[1]) : null; + + var type = value.ValueAsString("type")?.Value; + + if (type == "project") + { + lockFile.ProjectLibraries.Add(new LockFileProjectLibrary + { + Name = name, + Path = ReadString(value.Value("path")) + }); + } + } + } + + private string ReadFrameworkAssemblyReference(JsonValue json) + { + return ReadString(json); + } + + private IList ReadArray(JsonValue json, Func readItem) + { + if (json == null) + { + return new List(); + } + + var jarray = json as JsonArray; + if (jarray == null) + { + throw FileFormatException.Create("The value type is not array.", json); + } + + var items = new List(); + for (int i = 0; i < jarray.Length; ++i) + { + items.Add(readItem(jarray[i])); + } + return items; + } + + private IList ReadObject(JsonObject json, Func readItem) + { + if (json == null) + { + return new List(); + } + var items = new List(); + foreach (var childKey in json.Keys) + { + items.Add(readItem(childKey, json.Value(childKey))); + } + return items; + } + + private bool ReadBool(JsonObject cursor, string property, bool defaultValue) + { + var valueToken = cursor.Value(property) as JsonBoolean; + if (valueToken == null) + { + return defaultValue; + } + + return valueToken.Value; + } + + private int ReadInt(JsonObject cursor, string property, int defaultValue) + { + var number = cursor.Value(property) as JsonNumber; + if (number == null) + { + return defaultValue; + } + + try + { + var resultInInt = Convert.ToInt32(number.Raw); + return resultInInt; + } + catch (Exception ex) + { + // FormatException or OverflowException + throw FileFormatException.Create(ex, cursor); + } + } + + private string ReadString(JsonValue json) + { + if (json is JsonString) + { + return (json as JsonString).Value; + } + else if (json is JsonNull) + { + return null; + } + else + { + throw FileFormatException.Create("The value type is not string.", json); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Dnx.Watcher.Core/External/Runtime/NamedResourceReader.cs b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/NamedResourceReader.cs new file mode 100644 index 0000000000..1970cd4cb7 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/NamedResourceReader.cs @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using Microsoft.Dnx.Runtime.Json; + +namespace Microsoft.Dnx.Runtime +{ + internal static class NamedResourceReader + { + public static IDictionary ReadNamedResources(JsonObject rawProject, string projectFilePath) + { + if (!rawProject.Keys.Contains("namedResource")) + { + return new Dictionary(); + } + + var namedResourceToken = rawProject.ValueAsJsonObject("namedResource"); + if (namedResourceToken == null) + { + throw FileFormatException.Create("Value must be object.", rawProject.Value("namedResource"), projectFilePath); + } + + var namedResources = new Dictionary(); + + foreach (var namedResourceKey in namedResourceToken.Keys) + { + var resourcePath = namedResourceToken.ValueAsString(namedResourceKey); + if (resourcePath == null) + { + throw FileFormatException.Create("Value must be string.", namedResourceToken.Value(namedResourceKey), projectFilePath); + } + + if (resourcePath.Value.Contains("*")) + { + throw FileFormatException.Create("Value cannot contain wildcards.", resourcePath, projectFilePath); + } + + var resourceFileFullPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(projectFilePath), resourcePath)); + + if (namedResources.ContainsKey(namedResourceKey)) + { + throw FileFormatException.Create( + string.Format("The named resource {0} already exists.", namedResourceKey), + resourcePath, + projectFilePath); + } + + namedResources.Add( + namedResourceKey, + resourceFileFullPath); + } + + return namedResources; + } + + public static void ApplyNamedResources(IDictionary namedResources, IDictionary resources) + { + foreach (var namedResource in namedResources) + { + // The named resources dictionary is like the project file + // key = name, value = path to resource + if (resources.ContainsKey(namedResource.Value)) + { + resources[namedResource.Value] = namedResource.Key; + } + else + { + resources.Add(namedResource.Value, namedResource.Key); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Dnx.Watcher.Core/External/Runtime/PackIncludeEntry.cs b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/PackIncludeEntry.cs new file mode 100644 index 0000000000..0819ffed03 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/PackIncludeEntry.cs @@ -0,0 +1,45 @@ +// 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.Linq; +using Microsoft.Dnx.Runtime.Json; + +namespace Microsoft.Dnx.Runtime +{ + public class PackIncludeEntry + { + public string Target { get; } + public string[] SourceGlobs { get; } + public int Line { get; } + public int Column { get; } + + internal PackIncludeEntry(string target, JsonValue json) + : this(target, ExtractValues(json), json.Line, json.Column) + { + } + + public PackIncludeEntry(string target, string[] sourceGlobs, int line, int column) + { + Target = target; + SourceGlobs = sourceGlobs; + Line = line; + Column = column; + } + + private static string[] ExtractValues(JsonValue json) + { + var valueAsString = json as JsonString; + if (valueAsString != null) + { + return new string[] { valueAsString.Value }; + } + + var valueAsArray = json as JsonArray; + if(valueAsArray != null) + { + return valueAsArray.Values.Select(v => v.ToString()).ToArray(); + } + return new string[0]; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Dnx.Watcher.Core/External/Runtime/PathUtility.cs b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/PathUtility.cs new file mode 100644 index 0000000000..9546881690 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/PathUtility.cs @@ -0,0 +1,196 @@ +// 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; + +namespace NuGet +{ + internal static class PathUtility + { + public static bool IsChildOfDirectory(string dir, string candidate) + { + if (dir == null) + { + throw new ArgumentNullException(nameof(dir)); + } + if (candidate == null) + { + throw new ArgumentNullException(nameof(candidate)); + } + dir = Path.GetFullPath(dir); + dir = EnsureTrailingSlash(dir); + candidate = Path.GetFullPath(candidate); + return candidate.StartsWith(dir, StringComparison.OrdinalIgnoreCase); + } + + public static string EnsureTrailingSlash(string path) + { + return EnsureTrailingCharacter(path, Path.DirectorySeparatorChar); + } + + public static string EnsureTrailingForwardSlash(string path) + { + return EnsureTrailingCharacter(path, '/'); + } + + private static string EnsureTrailingCharacter(string path, char trailingCharacter) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + // if the path is empty, we want to return the original string instead of a single trailing character. + if (path.Length == 0 || path[path.Length - 1] == trailingCharacter) + { + return path; + } + + return path + trailingCharacter; + } + + public static void EnsureParentDirectory(string filePath) + { + string directory = Path.GetDirectoryName(filePath); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + } + + /// + /// Returns path2 relative to path1, with Path.DirectorySeparatorChar as separator + /// + public static string GetRelativePath(string path1, string path2) + { + return GetRelativePath(path1, path2, Path.DirectorySeparatorChar); + } + + /// + /// Returns path2 relative to path1, with given path separator + /// + public static string GetRelativePath(string path1, string path2, char separator) + { + if (string.IsNullOrEmpty(path1)) + { + throw new ArgumentException("Path must have a value", nameof(path1)); + } + + if (string.IsNullOrEmpty(path2)) + { + throw new ArgumentException("Path must have a value", nameof(path2)); + } + + StringComparison compare; + if (Microsoft.Dnx.Runtime.RuntimeEnvironmentHelper.IsWindows) + { + compare = StringComparison.OrdinalIgnoreCase; + // check if paths are on the same volume + if (!string.Equals(Path.GetPathRoot(path1), Path.GetPathRoot(path2))) + { + // on different volumes, "relative" path is just path2 + return path2; + } + } + else + { + compare = StringComparison.Ordinal; + } + + var index = 0; + var path1Segments = path1.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + var path2Segments = path2.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + // if path1 does not end with / it is assumed the end is not a directory + // we will assume that is isn't a directory by ignoring the last split + var len1 = path1Segments.Length - 1; + var len2 = path2Segments.Length; + + // find largest common absolute path between both paths + var min = Math.Min(len1, len2); + while (min > index) + { + if (!string.Equals(path1Segments[index], path2Segments[index], compare)) + { + break; + } + // Handle scenarios where folder and file have same name (only if os supports same name for file and directory) + // e.g. /file/name /file/name/app + else if ((len1 == index && len2 > index + 1) || (len1 > index && len2 == index + 1)) + { + break; + } + ++index; + } + + var path = ""; + + // check if path2 ends with a non-directory separator and if path1 has the same non-directory at the end + if (len1 + 1 == len2 && !string.IsNullOrEmpty(path1Segments[index]) && + string.Equals(path1Segments[index], path2Segments[index], compare)) + { + return path; + } + + for (var i = index; len1 > i; ++i) + { + path += ".." + separator; + } + for (var i = index; len2 - 1 > i; ++i) + { + path += path2Segments[i] + separator; + } + // if path2 doesn't end with an empty string it means it ended with a non-directory name, so we add it back + if (!string.IsNullOrEmpty(path2Segments[len2 - 1])) + { + path += path2Segments[len2 - 1]; + } + + return path; + } + + public static string GetAbsolutePath(string basePath, string relativePath) + { + if (basePath == null) + { + throw new ArgumentNullException(nameof(basePath)); + } + + if (relativePath == null) + { + throw new ArgumentNullException(nameof(relativePath)); + } + + Uri resultUri = new Uri(new Uri(basePath), new Uri(relativePath, UriKind.Relative)); + return resultUri.LocalPath; + } + + public static string GetDirectoryName(string path) + { + path = path.TrimEnd(Path.DirectorySeparatorChar); + return path.Substring(Path.GetDirectoryName(path).Length).Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } + + public static string GetPathWithForwardSlashes(string path) + { + return path.Replace('\\', '/'); + } + + public static string GetPathWithBackSlashes(string path) + { + return path.Replace('/', '\\'); + } + + public static string GetPathWithDirectorySeparator(string path) + { + if (Path.DirectorySeparatorChar == '/') + { + return GetPathWithForwardSlashes(path); + } + else + { + return GetPathWithBackSlashes(path); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Dnx.Watcher.Core/External/Runtime/PatternGroup.cs b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/PatternGroup.cs new file mode 100644 index 0000000000..50012a7a35 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/PatternGroup.cs @@ -0,0 +1,123 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Framework.FileSystemGlobbing; +using Microsoft.Dnx.Runtime.Json; + +namespace Microsoft.Dnx.Runtime +{ + public class PatternGroup + { + private readonly List _excludeGroups = new List(); + private readonly Matcher _matcher = new Matcher(); + + internal PatternGroup(IEnumerable includePatterns) + { + IncludeLiterals = Enumerable.Empty(); + IncludePatterns = includePatterns; + ExcludePatterns = Enumerable.Empty(); + _matcher.AddIncludePatterns(IncludePatterns); + } + + internal PatternGroup(IEnumerable includePatterns, IEnumerable excludePatterns, IEnumerable includeLiterals) + { + IncludeLiterals = includeLiterals; + IncludePatterns = includePatterns; + ExcludePatterns = excludePatterns; + + _matcher.AddIncludePatterns(IncludePatterns); + _matcher.AddExcludePatterns(ExcludePatterns); + } + + internal static PatternGroup Build(JsonObject rawProject, + string projectDirectory, + string projectFilePath, + string name, + IEnumerable fallbackIncluding = null, + IEnumerable additionalIncluding = null, + IEnumerable additionalExcluding = null, + bool includePatternsOnly = false, + ICollection warnings = null) + { + string includePropertyName = name; + additionalIncluding = additionalIncluding ?? Enumerable.Empty(); + var includePatterns = PatternsCollectionHelper.GetPatternsCollection(rawProject, projectDirectory, projectFilePath, includePropertyName, defaultPatterns: fallbackIncluding) + .Concat(additionalIncluding) + .Distinct(); + + if (includePatternsOnly) + { + return new PatternGroup(includePatterns); + } + + additionalExcluding = additionalExcluding ?? Enumerable.Empty(); + var excludePatterns = PatternsCollectionHelper.GetPatternsCollection(rawProject, projectDirectory, projectFilePath, propertyName: name + "Exclude") + .Concat(additionalExcluding) + .Distinct(); + + var includeLiterals = PatternsCollectionHelper.GetPatternsCollection(rawProject, projectDirectory, projectFilePath, propertyName: name + "Files", literalPath: true) + .Distinct(); + + return new PatternGroup(includePatterns, excludePatterns, includeLiterals); + } + + public IEnumerable IncludeLiterals { get; } + + public IEnumerable IncludePatterns { get; } + + public IEnumerable ExcludePatterns { get; } + + public IEnumerable ExcludePatternsGroup { get { return _excludeGroups; } } + + public PatternGroup ExcludeGroup(PatternGroup group) + { + _excludeGroups.Add(group); + + return this; + } + + public IEnumerable SearchFiles(string rootPath) + { + // literal included files are added at the last, but the search happens early + // so as to make the process fail early in case there is missing file. fail early + // helps to avoid unnecessary globing for performance optimization + var literalIncludedFiles = new List(); + foreach (var literalRelativePath in IncludeLiterals) + { + var fullPath = Path.GetFullPath(Path.Combine(rootPath, literalRelativePath)); + + if (!File.Exists(fullPath)) + { + throw new InvalidOperationException(string.Format("Can't find file {0}", literalRelativePath)); + } + + // TODO: extract utility like NuGet.PathUtility.GetPathWithForwardSlashes() + literalIncludedFiles.Add(fullPath.Replace('\\', '/')); + } + + // globing files + var globbingResults = _matcher.GetResultsInFullPath(rootPath); + + // if there is no results generated in globing, skip excluding other groups + // for performance optimization. + if (globbingResults.Any()) + { + foreach (var group in _excludeGroups) + { + globbingResults = globbingResults.Except(group.SearchFiles(rootPath)); + } + } + + return globbingResults.Concat(literalIncludedFiles).Distinct(); + } + + public override string ToString() + { + return string.Format("Pattern group: Literals [{0}] Includes [{1}] Excludes [{2}]", string.Join(", ", IncludeLiterals), string.Join(", ", IncludePatterns), string.Join(", ", ExcludePatterns)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Dnx.Watcher.Core/External/Runtime/PatternsCollectionHelper.cs b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/PatternsCollectionHelper.cs new file mode 100644 index 0000000000..33cb2f12de --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/PatternsCollectionHelper.cs @@ -0,0 +1,106 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Dnx.Runtime.Json; + +namespace Microsoft.Dnx.Runtime +{ + internal static class PatternsCollectionHelper + { + private static readonly char[] PatternSeparator = new[] { ';' }; + + public static IEnumerable GetPatternsCollection(JsonObject rawProject, + string projectDirectory, + string projectFilePath, + string propertyName, + IEnumerable defaultPatterns = null, + bool literalPath = false) + { + defaultPatterns = defaultPatterns ?? Enumerable.Empty(); + + try + { + if (!rawProject.Keys.Contains(propertyName)) + { + return CreateCollection(projectDirectory, propertyName, defaultPatterns, literalPath); + } + + var valueInString = rawProject.ValueAsString(propertyName); + if (valueInString != null) + { + return CreateCollection(projectDirectory, propertyName, new string[] { valueInString }, literalPath); + } + + var valuesInArray = rawProject.ValueAsStringArray(propertyName); + if (valuesInArray != null) + { + return CreateCollection(projectDirectory, propertyName, valuesInArray.Select(s => s.ToString()), literalPath); + } + } + catch (Exception ex) + { + throw FileFormatException.Create(ex, rawProject.Value(propertyName), projectFilePath); + } + + throw FileFormatException.Create("Value must be either string or array.", rawProject.Value(propertyName), projectFilePath); + } + + private static IEnumerable CreateCollection(string projectDirectory, string propertyName, IEnumerable patternsStrings, bool literalPath) + { + var patterns = patternsStrings.SelectMany(patternsString => GetSourcesSplit(patternsString)) + .Select(patternString => patternString.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar)); + + foreach (var pattern in patterns) + { + if (Path.IsPathRooted(pattern)) + { + throw new InvalidOperationException($"The '{propertyName}' property cannot be a rooted path."); + } + + if (literalPath && pattern.Contains('*')) + { + throw new InvalidOperationException($"The '{propertyName}' property cannot contain wildcard characters."); + } + } + + return new List(patterns.Select(pattern => FolderToPattern(pattern, projectDirectory))); + } + + private static IEnumerable GetSourcesSplit(string sourceDescription) + { + if (string.IsNullOrEmpty(sourceDescription)) + { + return Enumerable.Empty(); + } + + return sourceDescription.Split(PatternSeparator, StringSplitOptions.RemoveEmptyEntries); + } + + private static string FolderToPattern(string candidate, string projectDir) + { + // This conversion is needed to support current template + + // If it's already a pattern, no change is needed + if (candidate.Contains('*')) + { + return candidate; + } + + // If the given string ends with a path separator, or it is an existing directory + // we convert this folder name to a pattern matching all files in the folder + if (candidate.EndsWith(@"\") || + candidate.EndsWith("/") || + Directory.Exists(Path.Combine(projectDir, candidate))) + { + return Path.Combine(candidate, "**", "*"); + } + + // Otherwise, it represents a single file + return candidate; + } + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/External/Runtime/Project.cs b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/Project.cs new file mode 100644 index 0000000000..e9a610c29c --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/Project.cs @@ -0,0 +1,126 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Versioning; +using NuGet; + +namespace Microsoft.Dnx.Runtime +{ + public class Project + { + public const string ProjectFileName = "project.json"; + + public Project() + { + } + + public string ProjectFilePath { get; set; } + + public string ProjectDirectory + { + get + { + return Path.GetDirectoryName(ProjectFilePath); + } + } + + public string Name { get; set; } + + public string Title { get; set; } + + public string Description { get; set; } + + public string Copyright { get; set; } + + public string Summary { get; set; } + + public string Language { get; set; } + + public string ReleaseNotes { get; set; } + + public string[] Authors { get; set; } + + public string[] Owners { get; set; } + + public bool EmbedInteropTypes { get; set; } + + public Version AssemblyFileVersion { get; set; } + public string WebRoot { get; set; } + + public string EntryPoint { get; set; } + + public string ProjectUrl { get; set; } + + public string LicenseUrl { get; set; } + + public string IconUrl { get; set; } + + public bool RequireLicenseAcceptance { get; set; } + + public string[] Tags { get; set; } + + public bool IsLoadable { get; set; } + + public ProjectFilesCollection Files { get; set; } + + public IDictionary Commands { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public IDictionary> Scripts { get; } = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + public static bool HasProjectFile(string path) + { + string projectPath = Path.Combine(path, ProjectFileName); + + return File.Exists(projectPath); + } + + public static bool TryGetProject(string path, out Project project, ICollection diagnostics = null) + { + project = null; + + string projectPath = null; + + if (string.Equals(Path.GetFileName(path), ProjectFileName, StringComparison.OrdinalIgnoreCase)) + { + projectPath = path; + path = Path.GetDirectoryName(path); + } + else if (!HasProjectFile(path)) + { + return false; + } + else + { + projectPath = Path.Combine(path, ProjectFileName); + } + + // Assume the directory name is the project name if none was specified + var projectName = PathUtility.GetDirectoryName(path); + projectPath = Path.GetFullPath(projectPath); + + if (!File.Exists(projectPath)) + { + return false; + } + + try + { + using (var stream = File.OpenRead(projectPath)) + { + var reader = new ProjectReader(); + project = reader.ReadProject(stream, projectName, projectPath, diagnostics); + } + } + catch (Exception ex) + { + throw FileFormatException.Create(ex, projectPath); + } + + return true; + } + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/External/Runtime/ProjectFilesCollection.cs b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/ProjectFilesCollection.cs new file mode 100644 index 0000000000..9ee0056e5c --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/ProjectFilesCollection.cs @@ -0,0 +1,202 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.Dnx.Runtime.Json; + +namespace Microsoft.Dnx.Runtime +{ + public class ProjectFilesCollection + { + public static readonly string[] DefaultCompileBuiltInPatterns = new[] { @"**/*.cs" }; + public static readonly string[] DefaultPublishExcludePatterns = new[] { @"obj/**/*.*", @"bin/**/*.*", @"**/.*/**", @"**/global.json" }; + public static readonly string[] DefaultPreprocessPatterns = new[] { @"compiler/preprocess/**/*.cs" }; + public static readonly string[] DefaultSharedPatterns = new[] { @"compiler/shared/**/*.cs" }; + public static readonly string[] DefaultResourcesBuiltInPatterns = new[] { @"compiler/resources/**/*", "**/*.resx" }; + public static readonly string[] DefaultContentsBuiltInPatterns = new[] { @"**/*" }; + + public static readonly string[] DefaultBuiltInExcludePatterns = new[] { "bin/**", "obj/**", "**/*.xproj" }; + + public static readonly string PackIncludePropertyName = "packInclude"; + + private PatternGroup _sharedPatternsGroup; + private PatternGroup _resourcePatternsGroup; + private PatternGroup _preprocessPatternsGroup; + private PatternGroup _compilePatternsGroup; + private PatternGroup _contentPatternsGroup; + private IDictionary _namedResources; + private IEnumerable _publishExcludePatterns; + private IEnumerable _packInclude; + + private readonly string _projectDirectory; + private readonly string _projectFilePath; + + private JsonObject _rawProject; + private bool _initialized; + + internal ProjectFilesCollection(JsonObject rawProject, string projectDirectory, string projectFilePath) + { + _projectDirectory = projectDirectory; + _projectFilePath = projectFilePath; + _rawProject = rawProject; + } + + internal void EnsureInitialized() + { + if (_initialized) + { + return; + } + + var excludeBuiltIns = PatternsCollectionHelper.GetPatternsCollection(_rawProject, _projectDirectory, _projectFilePath, "excludeBuiltIn", DefaultBuiltInExcludePatterns); + var excludePatterns = PatternsCollectionHelper.GetPatternsCollection(_rawProject, _projectDirectory, _projectFilePath, "exclude") + .Concat(excludeBuiltIns); + var contentBuiltIns = PatternsCollectionHelper.GetPatternsCollection(_rawProject, _projectDirectory, _projectFilePath, "contentBuiltIn", DefaultContentsBuiltInPatterns); + var compileBuiltIns = PatternsCollectionHelper.GetPatternsCollection(_rawProject, _projectDirectory, _projectFilePath, "compileBuiltIn", DefaultCompileBuiltInPatterns); + var resourceBuiltIns = PatternsCollectionHelper.GetPatternsCollection(_rawProject, _projectDirectory, _projectFilePath, "resourceBuiltIn", DefaultResourcesBuiltInPatterns); + + _publishExcludePatterns = PatternsCollectionHelper.GetPatternsCollection(_rawProject, _projectDirectory, _projectFilePath, "publishExclude", DefaultPublishExcludePatterns); + + _sharedPatternsGroup = PatternGroup.Build(_rawProject, _projectDirectory, _projectFilePath, "shared", fallbackIncluding: DefaultSharedPatterns, additionalExcluding: excludePatterns); + + _resourcePatternsGroup = PatternGroup.Build(_rawProject, _projectDirectory, _projectFilePath, "resource", additionalIncluding: resourceBuiltIns, additionalExcluding: excludePatterns); + + _preprocessPatternsGroup = PatternGroup.Build(_rawProject, _projectDirectory, _projectFilePath, "preprocess", fallbackIncluding: DefaultPreprocessPatterns, additionalExcluding: excludePatterns) + .ExcludeGroup(_sharedPatternsGroup) + .ExcludeGroup(_resourcePatternsGroup); + + _compilePatternsGroup = PatternGroup.Build(_rawProject, _projectDirectory, _projectFilePath, "compile", additionalIncluding: compileBuiltIns, additionalExcluding: excludePatterns) + .ExcludeGroup(_sharedPatternsGroup) + .ExcludeGroup(_preprocessPatternsGroup) + .ExcludeGroup(_resourcePatternsGroup); + + _contentPatternsGroup = PatternGroup.Build(_rawProject, _projectDirectory, _projectFilePath, "content", additionalIncluding: contentBuiltIns, additionalExcluding: excludePatterns.Concat(_publishExcludePatterns)) + .ExcludeGroup(_compilePatternsGroup) + .ExcludeGroup(_preprocessPatternsGroup) + .ExcludeGroup(_sharedPatternsGroup) + .ExcludeGroup(_resourcePatternsGroup); + + _namedResources = NamedResourceReader.ReadNamedResources(_rawProject, _projectFilePath); + + // Files to be packed along with the project + var packIncludeJson = _rawProject.ValueAsJsonObject(PackIncludePropertyName); + if (packIncludeJson != null) + { + _packInclude = packIncludeJson + .Keys + .Select(k => new PackIncludeEntry(k, packIncludeJson.Value(k))) + .ToList(); + } + else + { + _packInclude = new List(); + } + + _initialized = true; + _rawProject = null; + } + + public IEnumerable PackInclude + { + get + { + EnsureInitialized(); + return _packInclude; + } + } + + public IEnumerable SourceFiles + { + get { return CompilePatternsGroup.SearchFiles(_projectDirectory).Distinct(); } + } + + public IEnumerable PreprocessSourceFiles + { + get { return PreprocessPatternsGroup.SearchFiles(_projectDirectory).Distinct(); } + } + + public IDictionary ResourceFiles + { + get + { + var resources = ResourcePatternsGroup + .SearchFiles(_projectDirectory) + .Distinct() + .ToDictionary(res => res, res => (string)null); + + NamedResourceReader.ApplyNamedResources(_namedResources, resources); + + return resources; + } + } + + public IEnumerable SharedFiles + { + get { return SharedPatternsGroup.SearchFiles(_projectDirectory).Distinct(); } + } + + public IEnumerable GetFilesForBundling(bool includeSource, IEnumerable additionalExcludePatterns) + { + var patternGroup = new PatternGroup(ContentPatternsGroup.IncludePatterns, + ContentPatternsGroup.ExcludePatterns.Concat(additionalExcludePatterns), + ContentPatternsGroup.IncludeLiterals); + if (!includeSource) + { + foreach (var excludedGroup in ContentPatternsGroup.ExcludePatternsGroup) + { + patternGroup.ExcludeGroup(excludedGroup); + } + } + + return patternGroup.SearchFiles(_projectDirectory); + } + + internal PatternGroup CompilePatternsGroup + { + get + { + EnsureInitialized(); + return _compilePatternsGroup; + } + } + + internal PatternGroup SharedPatternsGroup + { + get + { + EnsureInitialized(); + return _sharedPatternsGroup; + } + } + + internal PatternGroup ResourcePatternsGroup + { + get + { + EnsureInitialized(); + return _resourcePatternsGroup; + } + } + + internal PatternGroup PreprocessPatternsGroup + { + get + { + EnsureInitialized(); + return _preprocessPatternsGroup; + } + } + + internal PatternGroup ContentPatternsGroup + { + get + { + EnsureInitialized(); + return _contentPatternsGroup; + } + } + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/External/Runtime/ProjectReader.cs b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/ProjectReader.cs new file mode 100644 index 0000000000..b8aeb4734a --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/ProjectReader.cs @@ -0,0 +1,144 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Dnx.Runtime.Json; +using NuGet; + +namespace Microsoft.Dnx.Runtime +{ + public class ProjectReader + { + public Project ReadProject(Stream stream, string projectName, string projectPath, ICollection diagnostics) + { + var project = new Project(); + + var reader = new StreamReader(stream); + var rawProject = JsonDeserializer.Deserialize(reader) as JsonObject; + if (rawProject == null) + { + throw FileFormatException.Create( + "The JSON file can't be deserialized to a JSON object.", + projectPath); + } + + // Meta-data properties + project.Name = projectName; + project.ProjectFilePath = Path.GetFullPath(projectPath); + + var version = rawProject.Value("version") as JsonString; + + project.Description = rawProject.ValueAsString("description"); + project.Summary = rawProject.ValueAsString("summary"); + project.Copyright = rawProject.ValueAsString("copyright"); + project.Title = rawProject.ValueAsString("title"); + project.WebRoot = rawProject.ValueAsString("webroot"); + project.EntryPoint = rawProject.ValueAsString("entryPoint"); + project.ProjectUrl = rawProject.ValueAsString("projectUrl"); + project.LicenseUrl = rawProject.ValueAsString("licenseUrl"); + project.IconUrl = rawProject.ValueAsString("iconUrl"); + + project.Authors = rawProject.ValueAsStringArray("authors") ?? new string[] { }; + project.Owners = rawProject.ValueAsStringArray("owners") ?? new string[] { }; + project.Tags = rawProject.ValueAsStringArray("tags") ?? new string[] { }; + + project.Language = rawProject.ValueAsString("language"); + project.ReleaseNotes = rawProject.ValueAsString("releaseNotes"); + + project.RequireLicenseAcceptance = rawProject.ValueAsBoolean("requireLicenseAcceptance", defaultValue: false); + project.IsLoadable = rawProject.ValueAsBoolean("loadable", defaultValue: true); + // TODO: Move this to the dependencies node + project.EmbedInteropTypes = rawProject.ValueAsBoolean("embedInteropTypes", defaultValue: false); + + // Project files + project.Files = new ProjectFilesCollection(rawProject, project.ProjectDirectory, project.ProjectFilePath); + + var commands = rawProject.Value("commands") as JsonObject; + if (commands != null) + { + foreach (var key in commands.Keys) + { + var value = commands.ValueAsString(key); + if (value != null) + { + project.Commands[key] = value; + } + } + } + + var scripts = rawProject.Value("scripts") as JsonObject; + if (scripts != null) + { + foreach (var key in scripts.Keys) + { + var stringValue = scripts.ValueAsString(key); + if (stringValue != null) + { + project.Scripts[key] = new string[] { stringValue }; + continue; + } + + var arrayValue = scripts.ValueAsStringArray(key); + if (arrayValue != null) + { + project.Scripts[key] = arrayValue; + continue; + } + + throw FileFormatException.Create( + string.Format("The value of a script in {0} can only be a string or an array of strings", Project.ProjectFileName), + scripts.Value(key), + project.ProjectFilePath); + } + } + + return project; + } + + private static SemanticVersion SpecifySnapshot(string version, string snapshotValue) + { + if (version.EndsWith("-*")) + { + if (string.IsNullOrEmpty(snapshotValue)) + { + version = version.Substring(0, version.Length - 2); + } + else + { + version = version.Substring(0, version.Length - 1) + snapshotValue; + } + } + + return new SemanticVersion(version); + } + + private static bool TryGetStringEnumerable(JsonObject parent, string property, out IEnumerable result) + { + var collection = new List(); + var valueInString = parent.ValueAsString(property); + if (valueInString != null) + { + collection.Add(valueInString); + } + else + { + var valueInArray = parent.ValueAsStringArray(property); + if (valueInArray != null) + { + collection.AddRange(valueInArray); + } + else + { + result = null; + return false; + } + } + + result = collection.SelectMany(value => value.Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries)); + return true; + } + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/External/Runtime/RuntimeEnvironmentHelper.cs b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/RuntimeEnvironmentHelper.cs new file mode 100644 index 0000000000..4003c3fee4 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/RuntimeEnvironmentHelper.cs @@ -0,0 +1,50 @@ +// 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.Dnx.Runtime +{ + internal static class RuntimeEnvironmentHelper + { + private static Lazy _isMono = new Lazy(() => + _runtimeEnv.Value.RuntimeType == "Mono"); + + private static Lazy _isWindows = new Lazy(() => + _runtimeEnv.Value.OperatingSystem == "Windows"); + + private static Lazy _runtimeEnv = new Lazy(() => + GetRuntimeEnvironment()); + + private static IRuntimeEnvironment GetRuntimeEnvironment() + { + var provider = Infrastructure.CallContextServiceLocator.Locator.ServiceProvider; + var environment = (IRuntimeEnvironment)provider?.GetService(typeof(IRuntimeEnvironment)); + + if (environment == null) + { + throw new InvalidOperationException("Failed to resolve IRuntimeEnvironment"); + } + + return environment; + } + + public static IRuntimeEnvironment RuntimeEnvironment + { + get + { + return _runtimeEnv.Value; + } + } + + public static bool IsWindows + { + get { return _isWindows.Value; } + } + + public static bool IsMono + { + get { return _isMono.Value; } + } + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/External/Runtime/SemanticVersion.cs b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/SemanticVersion.cs new file mode 100644 index 0000000000..59846a2cf9 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/External/Runtime/SemanticVersion.cs @@ -0,0 +1,330 @@ +// 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.Text; + +namespace NuGet +{ + /// + /// A hybrid implementation of SemVer that supports semantic versioning as described at http://semver.org while not strictly enforcing it to + /// allow older 4-digit versioning schemes to continue working. + /// + internal sealed class SemanticVersion : IComparable, IComparable, IEquatable + { + private string _normalizedVersionString; + + public SemanticVersion(string version) + : this(Parse(version)) + { + } + + public SemanticVersion(int major, int minor, int build, int revision) + : this(new Version(major, minor, build, revision)) + { + } + + public SemanticVersion(int major, int minor, int build, string specialVersion) + : this(new Version(major, minor, build), specialVersion) + { + } + + public SemanticVersion(Version version) + : this(version, string.Empty) + { + } + + public SemanticVersion(Version version, string specialVersion) + { + if (version == null) + { + throw new ArgumentNullException(nameof(version)); + } + Version = NormalizeVersionValue(version); + SpecialVersion = specialVersion ?? string.Empty; + } + + internal SemanticVersion(SemanticVersion semVer) + { + Version = semVer.Version; + SpecialVersion = semVer.SpecialVersion; + } + + /// + /// Gets the normalized version portion. + /// + public Version Version + { + get; + private set; + } + + /// + /// Gets the optional special version. + /// + public string SpecialVersion + { + get; + private set; + } + + private static string[] SplitAndPadVersionString(string version) + { + string[] a = version.Split('.'); + if (a.Length == 4) + { + return a; + } + else + { + // if 'a' has less than 4 elements, we pad the '0' at the end + // to make it 4. + var b = new string[4] { "0", "0", "0", "0" }; + Array.Copy(a, 0, b, 0, a.Length); + return b; + } + } + + /// + /// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version. + /// + public static SemanticVersion Parse(string version) + { + if (string.IsNullOrEmpty(version)) + { + throw new ArgumentNullException(nameof(version)); + } + + SemanticVersion semVer; + if (!TryParse(version, out semVer)) + { + throw new ArgumentException(nameof(version)); + } + return semVer; + } + + /// + /// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version. + /// + public static bool TryParse(string version, out SemanticVersion value) + { + return TryParseInternal(version, strict: false, semVer: out value); + } + + /// + /// Parses a version string using strict semantic versioning rules that allows exactly 3 components and an optional special version. + /// + public static bool TryParseStrict(string version, out SemanticVersion value) + { + return TryParseInternal(version, strict: true, semVer: out value); + } + + private static bool TryParseInternal(string version, bool strict, out SemanticVersion semVer) + { + semVer = null; + if (string.IsNullOrEmpty(version)) + { + return false; + } + + version = version.Trim(); + var versionPart = version; + + string specialVersion = string.Empty; + if (version.IndexOf('-') != -1) + { + var parts = version.Split(new char[] { '-' }, 2, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 2) + { + return false; + } + + versionPart = parts[0]; + specialVersion = parts[1]; + } + + Version versionValue; + if (!Version.TryParse(versionPart, out versionValue)) + { + return false; + } + + if (strict) + { + // Must have major, minor and build only. + if (versionValue.Major == -1 || + versionValue.Minor == -1 || + versionValue.Build == -1 || + versionValue.Revision != -1) + { + return false; + } + } + + semVer = new SemanticVersion(NormalizeVersionValue(versionValue), specialVersion); + return true; + } + + /// + /// Attempts to parse the version token as a SemanticVersion. + /// + /// An instance of SemanticVersion if it parses correctly, null otherwise. + public static SemanticVersion ParseOptionalVersion(string version) + { + SemanticVersion semVer; + TryParse(version, out semVer); + return semVer; + } + + private static Version NormalizeVersionValue(Version version) + { + return new Version(version.Major, + version.Minor, + Math.Max(version.Build, 0), + Math.Max(version.Revision, 0)); + } + + public int CompareTo(object obj) + { + if (Object.ReferenceEquals(obj, null)) + { + return 1; + } + SemanticVersion other = obj as SemanticVersion; + if (other == null) + { + throw new ArgumentException(nameof(obj)); + } + return CompareTo(other); + } + + public int CompareTo(SemanticVersion other) + { + if (Object.ReferenceEquals(other, null)) + { + return 1; + } + + int result = Version.CompareTo(other.Version); + + if (result != 0) + { + return result; + } + + bool empty = string.IsNullOrEmpty(SpecialVersion); + bool otherEmpty = string.IsNullOrEmpty(other.SpecialVersion); + if (empty && otherEmpty) + { + return 0; + } + else if (empty) + { + return 1; + } + else if (otherEmpty) + { + return -1; + } + return StringComparer.OrdinalIgnoreCase.Compare(SpecialVersion, other.SpecialVersion); + } + + public static bool operator ==(SemanticVersion version1, SemanticVersion version2) + { + if (Object.ReferenceEquals(version1, null)) + { + return Object.ReferenceEquals(version2, null); + } + return version1.Equals(version2); + } + + public static bool operator !=(SemanticVersion version1, SemanticVersion version2) + { + return !(version1 == version2); + } + + public static bool operator <(SemanticVersion version1, SemanticVersion version2) + { + if (version1 == null) + { + throw new ArgumentNullException(nameof(version1)); + } + return version1.CompareTo(version2) < 0; + } + + public static bool operator <=(SemanticVersion version1, SemanticVersion version2) + { + return (version1 == version2) || (version1 < version2); + } + + public static bool operator >(SemanticVersion version1, SemanticVersion version2) + { + if (version1 == null) + { + throw new ArgumentNullException(nameof(version1)); + } + return version2 < version1; + } + + public static bool operator >=(SemanticVersion version1, SemanticVersion version2) + { + return (version1 == version2) || (version1 > version2); + } + + public override string ToString() + { + if (_normalizedVersionString == null) + { + var builder = new StringBuilder(); + builder + .Append(Version.Major) + .Append('.') + .Append(Version.Minor) + .Append('.') + .Append(Math.Max(0, Version.Build)); + + if (Version.Revision > 0) + { + builder + .Append('.') + .Append(Version.Revision); + } + + if (!string.IsNullOrEmpty(SpecialVersion)) + { + builder + .Append('-') + .Append(SpecialVersion); + } + + _normalizedVersionString = builder.ToString(); + } + + return _normalizedVersionString; + } + + public bool Equals(SemanticVersion other) + { + return !Object.ReferenceEquals(null, other) && + Version.Equals(other.Version) && + SpecialVersion.Equals(other.SpecialVersion, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object obj) + { + SemanticVersion semVer = obj as SemanticVersion; + return !Object.ReferenceEquals(null, semVer) && Equals(semVer); + } + + public override int GetHashCode() + { + int hashCode = Version.GetHashCode(); + if (SpecialVersion != null) + { + hashCode = hashCode * 4567 + SpecialVersion.GetHashCode(); + } + + return hashCode; + } + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/FileSystem/FileSystemWatcherRoot.cs b/src/Microsoft.Dnx.Watcher.Core/FileSystem/FileSystemWatcherRoot.cs new file mode 100644 index 0000000000..3c4fb517c1 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/FileSystem/FileSystemWatcherRoot.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; + +namespace Microsoft.Dnx.Watcher.Core +{ + internal class FileSystemWatcherRoot : IWatcherRoot + { + private readonly FileSystemWatcher _watcher; + + public FileSystemWatcherRoot(FileSystemWatcher watcher) + { + _watcher = watcher; + } + + public string Path + { + get + { + return _watcher.Path; + } + } + + public void Dispose() + { + _watcher.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Dnx.Watcher.Core/FileSystem/FileWatcher.cs b/src/Microsoft.Dnx.Watcher.Core/FileSystem/FileWatcher.cs new file mode 100644 index 0000000000..fe9921336e --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/FileSystem/FileWatcher.cs @@ -0,0 +1,217 @@ +// 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.Dnx.Watcher.Core +{ + public class FileWatcher : IFileWatcher + { + private readonly HashSet _files = new HashSet(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> _directories = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + private readonly List _watchers = new List(); + + internal FileWatcher() + { + } + + public FileWatcher(string path) + { + AddWatcher(path); + } + + public event Action OnChanged; + + public void WatchDirectory(string path, string extension) + { + var extensions = _directories.GetOrAdd(path, _ => new HashSet(StringComparer.OrdinalIgnoreCase)); + + extensions.Add(extension); + } + + public bool WatchFile(string path) + { + return _files.Add(path); + } + + public void WatchProject(string projectPath) + { + if (string.IsNullOrEmpty(projectPath)) + { + return; + } + + // If any watchers already handle this path then noop + if (!IsAlreadyWatched(projectPath)) + { + // To reduce the number of watchers we have we add a watcher to the root + // of this project so that we'll be notified if anything we care + // about changes + var rootPath = ResolveRootDirectory(projectPath); + AddWatcher(rootPath); + } + } + + // For testing + internal bool IsAlreadyWatched(string projectPath) + { + if (string.IsNullOrEmpty(projectPath)) + { + return false; + } + + bool anyWatchers = false; + + foreach (var watcher in _watchers) + { + // REVIEW: This needs to work x-platform, should this be case + // sensitive? + if (EnsureTrailingSlash(projectPath).StartsWith(EnsureTrailingSlash(watcher.Path), StringComparison.OrdinalIgnoreCase)) + { + anyWatchers = true; + } + } + + return anyWatchers; + } + + public void Dispose() + { + foreach (var w in _watchers) + { + w.Dispose(); + } + + _watchers.Clear(); + } + + public bool ReportChange(string newPath, WatcherChangeTypes changeType) + { + return ReportChange(oldPath: null, newPath: newPath, changeType: changeType); + } + + public bool ReportChange(string oldPath, string newPath, WatcherChangeTypes changeType) + { + if (HasChanged(oldPath, newPath, changeType)) + { + if (OnChanged != null) + { + OnChanged(oldPath ?? newPath); + } + + return true; + } + + return false; + } + + private static string EnsureTrailingSlash(string path) + { + if (string.IsNullOrEmpty(path)) + { + return path; + } + + if (path[path.Length - 1] != Path.DirectorySeparatorChar) + { + return path + Path.DirectorySeparatorChar; + } + + return path; + } + + // For testing only + internal void AddWatcher(IWatcherRoot watcherRoot) + { + _watchers.Add(watcherRoot); + } + + private void AddWatcher(string path) + { + var watcher = new FileSystemWatcher(path); + watcher.IncludeSubdirectories = true; + watcher.EnableRaisingEvents = true; + + watcher.Changed += OnWatcherChanged; + watcher.Renamed += OnRenamed; + watcher.Deleted += OnWatcherChanged; + watcher.Created += OnWatcherChanged; + + _watchers.Add(new FileSystemWatcherRoot(watcher)); + } + + private void OnRenamed(object sender, RenamedEventArgs e) + { + ReportChange(e.OldFullPath, e.FullPath, e.ChangeType); + } + + private void OnWatcherChanged(object sender, FileSystemEventArgs e) + { + ReportChange(e.FullPath, e.ChangeType); + } + + private bool HasChanged(string oldPath, string newPath, WatcherChangeTypes changeType) + { + // File changes + if (_files.Contains(newPath) || + (oldPath != null && _files.Contains(oldPath))) + { + return true; + } + + HashSet extensions; + if (_directories.TryGetValue(newPath, out extensions) || + _directories.TryGetValue(Path.GetDirectoryName(newPath), out extensions)) + { + string extension = Path.GetExtension(newPath); + + if (String.IsNullOrEmpty(extension)) + { + // Assume it's a directory + if (changeType == WatcherChangeTypes.Created || + changeType == WatcherChangeTypes.Renamed) + { + foreach (var e in extensions) + { + WatchDirectory(newPath, e); + } + } + else if (changeType == WatcherChangeTypes.Deleted) + { + return true; + } + + // Ignore anything else + return false; + } + + return extensions.Contains(extension); + } + + return false; + } + + private static string ResolveRootDirectory(string projectPath) + { + var di = new DirectoryInfo(projectPath); + + while (di.Parent != null) + { + var globalJsonPath = Path.Combine(di.FullName, "global.json"); + + if (File.Exists(globalJsonPath)) + { + return di.FullName; + } + + di = di.Parent; + } + + // If we don't find any files then make the project folder the root + return projectPath; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Dnx.Watcher.Core/FileSystem/IWatcherRoot.cs b/src/Microsoft.Dnx.Watcher.Core/FileSystem/IWatcherRoot.cs new file mode 100644 index 0000000000..ffc7a86f6b --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/FileSystem/IWatcherRoot.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Dnx.Watcher.Core +{ + internal interface IWatcherRoot : IDisposable + { + string Path { get; } + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/Impl/ProcessWatcher.cs b/src/Microsoft.Dnx.Watcher.Core/Impl/ProcessWatcher.cs new file mode 100644 index 0000000000..86b3cc13d7 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/Impl/ProcessWatcher.cs @@ -0,0 +1,97 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Dnx.Watcher.Core +{ + public class ProcessWatcher : IProcessWatcher + { + private Process _runningProcess; + + public int Start(string executable, string arguments, string workingDir) + { + // This is not thread safe but it will not run in a multithreaded environment so don't worry + if (_runningProcess != null) + { + throw new InvalidOperationException("The previous process is still running"); + } + + _runningProcess = new Process(); + _runningProcess.StartInfo = new ProcessStartInfo() + { + FileName = executable, + Arguments = arguments, + UseShellExecute = false, + WorkingDirectory = workingDir + }; + + RemoveCompilationPortEnvironmentVariable(_runningProcess.StartInfo); + + _runningProcess.Start(); + + return _runningProcess.Id; + } + + public async Task WaitForExitAsync(CancellationToken cancellationToken) + { + try + { + await Task.Run(() => + { + while (!cancellationToken.IsCancellationRequested) + { + if (_runningProcess.WaitForExit(500)) + { + break; + } + } + + if (!_runningProcess.HasExited) + { + _runningProcess.Kill(); + } + + }); + + return _runningProcess.ExitCode; + } + finally + { + _runningProcess = null; + } + } + + private static void RemoveCompilationPortEnvironmentVariable(ProcessStartInfo procStartInfo) + { + string[] _environmentVariablesToRemove = new string[] + { + "DNX_COMPILATION_SERVER_PORT", + }; + +#if DNX451 + var environmentVariables = procStartInfo.EnvironmentVariables.Keys.Cast(); +#else + var environmentVariables = procStartInfo.Environment.Keys; +#endif + + var envVarsToRemove = environmentVariables + .Where(envVar => _environmentVariablesToRemove.Contains(envVar, StringComparer.OrdinalIgnoreCase)) + .ToArray(); + + // Workaround for the DNX start issue (it passes some environment variables that it shouldn't) + foreach (var envVar in envVarsToRemove) + { +#if DNX451 + procStartInfo.EnvironmentVariables.Remove(envVar); +#else + procStartInfo.Environment.Remove(envVar); +#endif + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Dnx.Watcher.Core/Impl/Project.cs b/src/Microsoft.Dnx.Watcher.Core/Impl/Project.cs new file mode 100644 index 0000000000..8df5bd2ec8 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/Impl/Project.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Dnx.Runtime; + +namespace Microsoft.Dnx.Watcher.Core +{ + internal class Project : IProject + { + public Project(Runtime.Project runtimeProject) + { + ProjectFile = runtimeProject.ProjectFilePath; + + Files = runtimeProject.Files.SourceFiles.Concat( + runtimeProject.Files.ResourceFiles.Values.Concat( + runtimeProject.Files.PreprocessSourceFiles.Concat( + runtimeProject.Files.SharedFiles))).Concat( + new string[] { runtimeProject.ProjectFilePath }) + .ToList(); + + var projectLockJsonPath = Path.Combine(runtimeProject.ProjectDirectory, LockFileReader.LockFileName); + var lockFileReader = new LockFileReader(); + + if (File.Exists(projectLockJsonPath)) + { + var lockFile = lockFileReader.Read(projectLockJsonPath); + ProjectDependencies = lockFile.ProjectLibraries.Select(dep => dep.Path).ToList(); + } + else + { + ProjectDependencies = new string[0]; + } + } + + public IEnumerable ProjectDependencies { get; private set; } + + public IEnumerable Files { get; private set; } + + public string ProjectFile { get; private set; } + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/Impl/ProjectProvider.cs b/src/Microsoft.Dnx.Watcher.Core/Impl/ProjectProvider.cs new file mode 100644 index 0000000000..f7efad6d78 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/Impl/ProjectProvider.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Dnx.Runtime; + +namespace Microsoft.Dnx.Watcher.Core +{ + public class ProjectProvider : IProjectProvider + { + public bool TryReadProject(string projectFile, out IProject project, out string errors) + { + Runtime.Project runtimeProject; + if (!TryGetProject(projectFile, out runtimeProject, out errors)) + { + project = null; + return false; + } + + errors = null; + project = new Project(runtimeProject); + + return true; + } + + // Same as TryGetProject but it doesn't throw + private bool TryGetProject(string projectFile, out Runtime.Project project, out string errorMessage) + { + try + { + var errors = new List(); + if (!Runtime.Project.TryGetProject(projectFile, out project, errors)) + { + errorMessage = string.Join(Environment.NewLine, errors.Select(e => e.ToString())); + } + else + { + errorMessage = null; + return true; + } + } + catch (Exception ex) + { + errorMessage = ex.Message; + } + + project = null; + return false; + } + } +} diff --git a/src/Microsoft.Dnx.Watcher.Core/Microsoft.Dnx.Watcher.Core.xproj b/src/Microsoft.Dnx.Watcher.Core/Microsoft.Dnx.Watcher.Core.xproj new file mode 100644 index 0000000000..27b8206579 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/Microsoft.Dnx.Watcher.Core.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + d3da3bbb-e206-404f-aee6-17fb9b6f1221 + Microsoft.Dnx.Watcher.Core + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + diff --git a/src/Microsoft.Dnx.Watcher.Core/project.json b/src/Microsoft.Dnx.Watcher.Core/project.json new file mode 100644 index 0000000000..a8fbd226df --- /dev/null +++ b/src/Microsoft.Dnx.Watcher.Core/project.json @@ -0,0 +1,36 @@ +{ + "version": "1.0.0-*", + "compilationOptions": { "warningsAsErrors": true }, + "dependencies": { + "Microsoft.AspNet.FileProviders.Abstractions": "1.0.0-*", + "Microsoft.AspNet.FileProviders.Physical": "1.0.0-*", + "Microsoft.Dnx.Runtime.Abstractions": "1.0.0-*", + "Microsoft.Dnx.Runtime.Json.Sources": { + "type": "build", + "version": "1.0.0-*" + }, + "Microsoft.Framework.Logging.Abstractions": "1.0.0-*", + "Microsoft.Framework.FileSystemGlobbing": "1.0.0-*" + }, + "frameworks": { + "dnx451": { + "frameworkAssemblies": { + "System.Collections": "", + "System.Runtime": "" + } + }, + "dnxcore50": { + "dependencies": { + "System.Diagnostics.Process": "4.1.0-beta-*", + "System.Linq": "4.0.1-beta-*", + "System.Runtime.Extensions": "4.0.11-beta-*", + "System.Threading.Thread": "4.0.0-beta-*" + } + } + }, + + "scripts": { + "postbuild": [ + ] + } +} diff --git a/src/Microsoft.Dnx.Watcher/CommandOutputLogger.cs b/src/Microsoft.Dnx.Watcher/CommandOutputLogger.cs new file mode 100644 index 0000000000..934f8e272a --- /dev/null +++ b/src/Microsoft.Dnx.Watcher/CommandOutputLogger.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Dnx.Runtime.Common.CommandLine; +using Microsoft.Framework.Logging; + +namespace Microsoft.Dnx.Watcher +{ + /// + /// Logger to print formatted command output. + /// + public class CommandOutputLogger : ILogger + { + private readonly CommandOutputProvider _provider; + private readonly AnsiConsole _outConsole; + private readonly string _loggerName; + + public CommandOutputLogger(CommandOutputProvider commandOutputProvider, string loggerName, bool useConsoleColor) + { + _provider = commandOutputProvider; + _outConsole = AnsiConsole.GetOutput(useConsoleColor); + _loggerName = loggerName; + } + + public IDisposable BeginScopeImpl(object state) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) + { + if (logLevel < _provider.LogLevel) + { + return false; + } + + return true; + } + + public void Log(LogLevel logLevel, int eventId, object state, Exception exception, Func formatter) + { + if (IsEnabled(logLevel)) + { + _outConsole.WriteLine($"[{_loggerName}] {Caption(logLevel)}: {formatter(state, exception)}"); + } + } + + private string Caption(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Debug: return "\x1b[35mdebug\x1b[39m"; + case LogLevel.Verbose: return "\x1b[35mverbose\x1b[39m"; + case LogLevel.Information: return "\x1b[32minfo\x1b[39m"; + case LogLevel.Warning: return "\x1b[33mwarn\x1b[39m"; + case LogLevel.Error: return "\x1b[31mfail\x1b[39m"; + case LogLevel.Critical: return "\x1b[31mcritical\x1b[39m"; + } + + throw new Exception("Unknown LogLevel"); + } + } +} diff --git a/src/Microsoft.Dnx.Watcher/CommandOutputProvider.cs b/src/Microsoft.Dnx.Watcher/CommandOutputProvider.cs new file mode 100644 index 0000000000..3a98395b53 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher/CommandOutputProvider.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Dnx.Runtime; +using Microsoft.Framework.Logging; + +namespace Microsoft.Dnx.Watcher +{ + public class CommandOutputProvider : ILoggerProvider + { + private readonly bool _isWindows; + + public CommandOutputProvider(IRuntimeEnvironment runtimeEnv) + { + _isWindows = runtimeEnv.OperatingSystem == "Windows"; + } + + public ILogger CreateLogger(string name) + { + return new CommandOutputLogger(this, name, useConsoleColor: _isWindows); + } + + public void Dispose() + { + } + + public LogLevel LogLevel { get; set; } = LogLevel.Information; + } +} diff --git a/src/Microsoft.Dnx.Watcher/Microsoft.Dnx.Watcher.xproj b/src/Microsoft.Dnx.Watcher/Microsoft.Dnx.Watcher.xproj new file mode 100644 index 0000000000..c3c42491f3 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher/Microsoft.Dnx.Watcher.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 8a8ceabc-ac47-43ff-a5df-69224f7e1f46 + Microsoft.Dnx.Watcher + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + diff --git a/src/Microsoft.Dnx.Watcher/Program.cs b/src/Microsoft.Dnx.Watcher/Program.cs new file mode 100644 index 0000000000..ec4e43f6d7 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher/Program.cs @@ -0,0 +1,133 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Dnx.Runtime; +using Microsoft.Dnx.Runtime.Common.CommandLine; +using Microsoft.Dnx.Watcher.Core; +using Microsoft.Framework.Logging; + +namespace Microsoft.Dnx.Watcher +{ + public class Program + { + private const string DnxWatchArgumentSeparator = "--dnx-args"; + + private readonly ILoggerFactory _loggerFactory; + + public Program(IRuntimeEnvironment runtimeEnvironment) + { + _loggerFactory = new LoggerFactory(); + + var commandProvider = new CommandOutputProvider(runtimeEnvironment); + _loggerFactory.AddProvider(commandProvider); + } + + public int Main(string[] args) + { + using (CancellationTokenSource ctrlCTokenSource = new CancellationTokenSource()) + { + Console.CancelKeyPress += (sender, ev) => + { + ctrlCTokenSource.Cancel(); + ev.Cancel = false; + }; + + string[] watchArgs, dnxArgs; + SeparateWatchArguments(args, out watchArgs, out dnxArgs); + + return MainInternal(watchArgs, dnxArgs, ctrlCTokenSource.Token); + } + } + + internal static void SeparateWatchArguments(string[] args, out string[] watchArgs, out string[] dnxArgs) + { + int argsIndex = -1; + watchArgs = args.TakeWhile((arg, idx) => + { + argsIndex = idx; + return !string.Equals(arg, DnxWatchArgumentSeparator, StringComparison.OrdinalIgnoreCase); + }).ToArray(); + + dnxArgs = args.Skip(argsIndex + 1).ToArray(); + + if (dnxArgs.Length == 0) + { + // If no explicit dnx arguments then all arguments get passed to dnx + dnxArgs = watchArgs; + watchArgs = new string[0]; + } + } + + private int MainInternal(string[] watchArgs, string[] dnxArgs, CancellationToken cancellationToken) + { + var app = new CommandLineApplication(); + app.Name = "dnx-watch"; + app.FullName = "Microsoft .NET DNX File Watcher"; + + app.HelpOption("-?|-h|--help"); + + // Show help information if no subcommand/option was specified + app.OnExecute(() => + { + app.ShowHelp(); + return 2; + }); + + var projectArg = app.Option( + "--project ", + "Path to the project.json file or the application folder. Defaults to the current folder if not provided. Will be passed to DNX.", + CommandOptionType.SingleValue); + + var workingDirArg = app.Option( + "--workingDir ", + "The working directory for DNX. Defaults to the current directory.", + CommandOptionType.SingleValue); + + // This option is here just to be displayed in help + // it will not be parsed because it is removed before the code is executed + app.Option( + $"{DnxWatchArgumentSeparator} ", + "Marks the arguments that will be passed to DNX. Anything following this option is passed. If not specified, all the arguments are passed to DNX.", + CommandOptionType.SingleValue); + + app.OnExecute(() => + { + var projectToRun = projectArg.HasValue() ? + projectArg.Value() : + Directory.GetCurrentDirectory(); + + if (!projectToRun.EndsWith("project.json", StringComparison.Ordinal)) + { + projectToRun = Path.Combine(projectToRun, "project.json"); + } + + var workingDir = workingDirArg.HasValue() ? + workingDirArg.Value() : + Directory.GetCurrentDirectory(); + + var watcher = DnxWatcher.CreateDefault(_loggerFactory); + try + { + watcher.WatchAsync(projectToRun, dnxArgs, workingDir, cancellationToken).Wait(); + } + catch (AggregateException ex) + { + if (ex.InnerExceptions.Count != 1 || !(ex.InnerException is TaskCanceledException)) + { + throw; + } + } + + + return 1; + }); + + return app.Execute(watchArgs); + } + } +} diff --git a/src/Microsoft.Dnx.Watcher/Properties/AssemblyInfo.cs b/src/Microsoft.Dnx.Watcher/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..2cd8231774 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("Microsoft.Dnx.Watcher.Tests")] diff --git a/src/Microsoft.Dnx.Watcher/project.json b/src/Microsoft.Dnx.Watcher/project.json new file mode 100644 index 0000000000..cbe70ae529 --- /dev/null +++ b/src/Microsoft.Dnx.Watcher/project.json @@ -0,0 +1,23 @@ +{ + "version": "1.0.0-*", + "compilationOptions": { "warningsAsErrors": true }, + "dependencies": { + "Microsoft.Dnx.Watcher.Core": "1.0.0-*", + "Microsoft.Framework.CommandLineUtils.Sources": { "version": "1.0.0-*", "type": "build" }, + "Microsoft.Framework.Logging": "1.0.0-*", + "Microsoft.Framework.Logging.Console": "1.0.0-*" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + }, + + "commands": { + "dnx-watch": "Microsoft.Dnx.Watcher" + }, + + "scripts": { + "postbuild": [ + ] + } +} diff --git a/test/Microsoft.Dnx.Watcher.Tests/CommandLineParsingTests.cs b/test/Microsoft.Dnx.Watcher.Tests/CommandLineParsingTests.cs new file mode 100644 index 0000000000..c4ac14008b --- /dev/null +++ b/test/Microsoft.Dnx.Watcher.Tests/CommandLineParsingTests.cs @@ -0,0 +1,48 @@ +// 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 Xunit; + +namespace Microsoft.Dnx.Watcher.Tests +{ + // This project can output the Class library as a NuGet Package. + // To enable this option, right-click on the project and select the Properties menu item. In the Build tab select "Produce outputs on build". + public class CommandLineParsingTests + { + [Fact] + public void NoWatcherArgs() + { + var args = "--arg1 v1 --arg2 v2".Split(' '); + + string[] watcherArgs, dnxArgs; + Program.SeparateWatchArguments(args, out watcherArgs, out dnxArgs); + + Assert.Empty(watcherArgs); + Assert.Equal(args, dnxArgs); + } + + [Fact] + public void ArgsForBothDnxAndWatcher() + { + var args = "--arg1 v1 --arg2 v2 --dnx-args --arg3 --arg4 v4".Split(' '); + + string[] watcherArgs, dnxArgs; + Program.SeparateWatchArguments(args, out watcherArgs, out dnxArgs); + + Assert.Equal(new string[] {"--arg1", "v1", "--arg2", "v2" }, watcherArgs); + Assert.Equal(new string[] { "--arg3", "--arg4", "v4" }, dnxArgs); + } + + [Fact] + public void MultipleSeparators() + { + var args = "--arg1 v1 --arg2 v2 --dnx-args --arg3 --dnxArgs --arg4 v4".Split(' '); + + string[] watcherArgs, dnxArgs; + Program.SeparateWatchArguments(args, out watcherArgs, out dnxArgs); + + Assert.Equal(new string[] { "--arg1", "v1", "--arg2", "v2" }, watcherArgs); + Assert.Equal(new string[] { "--arg3", "--dnxArgs", "--arg4", "v4" }, dnxArgs); + } + } +} diff --git a/test/Microsoft.Dnx.Watcher.Tests/Microsoft.Dnx.Watcher.Tests.xproj b/test/Microsoft.Dnx.Watcher.Tests/Microsoft.Dnx.Watcher.Tests.xproj new file mode 100644 index 0000000000..7c8ac87ba2 --- /dev/null +++ b/test/Microsoft.Dnx.Watcher.Tests/Microsoft.Dnx.Watcher.Tests.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 640d190b-26db-4dde-88ee-55814c86c43e + Microsoft.Dnx.Watcher.Tests + ..\artifacts\obj\$(MSBuildProjectName) + ..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.Dnx.Watcher.Tests/project.json b/test/Microsoft.Dnx.Watcher.Tests/project.json new file mode 100644 index 0000000000..fc449ab49d --- /dev/null +++ b/test/Microsoft.Dnx.Watcher.Tests/project.json @@ -0,0 +1,15 @@ +{ + "dependencies": { + "Microsoft.Dnx.Runtime.Abstractions": "1.0.0-*", + "Microsoft.Dnx.Watcher": "1.0.0-*", + "xunit.runner.aspnet": "2.0.0-aspnet-*" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + }, + "commands": { + "test": "xunit.runner.aspnet" + } +} +