From 3f40980d0200aa4491ec94042dd49987c52f3c3a Mon Sep 17 00:00:00 2001 From: Victor Hurdugaci Date: Thu, 17 Sep 2015 14:05:33 -0700 Subject: [PATCH] First version of dnx watch --- .gitattributes | 50 +++ .gitignore | 27 ++ .travis.yml | 9 + NuGet.config | 7 + appveyor.yml | 7 + build.cmd | 39 +++ build.sh | 41 +++ dnx-watch.sln | 49 +++ global.json | 3 + makefile.shade | 7 + .../Abstractions/IFileWatcher.cs | 18 + .../Abstractions/IProcessWatcher.cs | 15 + .../Abstractions/IProject.cs | 16 + .../Abstractions/IProjectProvider.cs | 10 + .../DictionaryExtensions.cs | 23 ++ src/Microsoft.Dnx.Watcher.Core/DnxWatcher.cs | 225 ++++++++++++ .../External/Runtime/Constants.cs | 34 ++ .../External/Runtime/FileFormatException.cs | 123 +++++++ .../External/Runtime/LockFile.cs | 16 + .../Runtime/LockFileProjectLibrary.cs | 12 + .../External/Runtime/LockFileReader.cs | 220 ++++++++++++ .../External/Runtime/NamedResourceReader.cs | 75 ++++ .../External/Runtime/PackIncludeEntry.cs | 45 +++ .../External/Runtime/PathUtility.cs | 196 +++++++++++ .../External/Runtime/PatternGroup.cs | 123 +++++++ .../Runtime/PatternsCollectionHelper.cs | 106 ++++++ .../External/Runtime/Project.cs | 126 +++++++ .../Runtime/ProjectFilesCollection.cs | 202 +++++++++++ .../External/Runtime/ProjectReader.cs | 144 ++++++++ .../Runtime/RuntimeEnvironmentHelper.cs | 50 +++ .../External/Runtime/SemanticVersion.cs | 330 ++++++++++++++++++ .../FileSystem/FileSystemWatcherRoot.cs | 30 ++ .../FileSystem/FileWatcher.cs | 217 ++++++++++++ .../FileSystem/IWatcherRoot.cs | 12 + .../Impl/ProcessWatcher.cs | 97 +++++ .../Impl/Project.cs | 45 +++ .../Impl/ProjectProvider.cs | 53 +++ .../Microsoft.Dnx.Watcher.Core.xproj | 20 ++ src/Microsoft.Dnx.Watcher.Core/project.json | 36 ++ .../CommandOutputLogger.cs | 64 ++++ .../CommandOutputProvider.cs | 30 ++ .../Microsoft.Dnx.Watcher.xproj | 20 ++ src/Microsoft.Dnx.Watcher/Program.cs | 133 +++++++ .../Properties/AssemblyInfo.cs | 6 + src/Microsoft.Dnx.Watcher/project.json | 23 ++ .../CommandLineParsingTests.cs | 48 +++ .../Microsoft.Dnx.Watcher.Tests.xproj | 21 ++ test/Microsoft.Dnx.Watcher.Tests/project.json | 15 + 48 files changed, 3218 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 NuGet.config create mode 100644 appveyor.yml create mode 100644 build.cmd create mode 100755 build.sh create mode 100644 dnx-watch.sln create mode 100644 global.json create mode 100644 makefile.shade create mode 100644 src/Microsoft.Dnx.Watcher.Core/Abstractions/IFileWatcher.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/Abstractions/IProcessWatcher.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/Abstractions/IProject.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/Abstractions/IProjectProvider.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/DictionaryExtensions.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/DnxWatcher.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/External/Runtime/Constants.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/External/Runtime/FileFormatException.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/External/Runtime/LockFile.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/External/Runtime/LockFileProjectLibrary.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/External/Runtime/LockFileReader.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/External/Runtime/NamedResourceReader.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/External/Runtime/PackIncludeEntry.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/External/Runtime/PathUtility.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/External/Runtime/PatternGroup.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/External/Runtime/PatternsCollectionHelper.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/External/Runtime/Project.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/External/Runtime/ProjectFilesCollection.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/External/Runtime/ProjectReader.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/External/Runtime/RuntimeEnvironmentHelper.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/External/Runtime/SemanticVersion.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/FileSystem/FileSystemWatcherRoot.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/FileSystem/FileWatcher.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/FileSystem/IWatcherRoot.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/Impl/ProcessWatcher.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/Impl/Project.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/Impl/ProjectProvider.cs create mode 100644 src/Microsoft.Dnx.Watcher.Core/Microsoft.Dnx.Watcher.Core.xproj create mode 100644 src/Microsoft.Dnx.Watcher.Core/project.json create mode 100644 src/Microsoft.Dnx.Watcher/CommandOutputLogger.cs create mode 100644 src/Microsoft.Dnx.Watcher/CommandOutputProvider.cs create mode 100644 src/Microsoft.Dnx.Watcher/Microsoft.Dnx.Watcher.xproj create mode 100644 src/Microsoft.Dnx.Watcher/Program.cs create mode 100644 src/Microsoft.Dnx.Watcher/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.Dnx.Watcher/project.json create mode 100644 test/Microsoft.Dnx.Watcher.Tests/CommandLineParsingTests.cs create mode 100644 test/Microsoft.Dnx.Watcher.Tests/Microsoft.Dnx.Watcher.Tests.xproj create mode 100644 test/Microsoft.Dnx.Watcher.Tests/project.json 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" + } +} +