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