Merge branch 'release/2.1' into release/2.2
This commit is contained in:
commit
27acef3d46
|
|
@ -30,10 +30,6 @@
|
||||||
path = modules/Diagnostics
|
path = modules/Diagnostics
|
||||||
url = https://github.com/aspnet/Diagnostics.git
|
url = https://github.com/aspnet/Diagnostics.git
|
||||||
branch = release/2.2
|
branch = release/2.2
|
||||||
[submodule "modules/DotNetTools"]
|
|
||||||
path = modules/DotNetTools
|
|
||||||
url = https://github.com/aspnet/DotNetTools.git
|
|
||||||
branch = release/2.2
|
|
||||||
[submodule "modules/EntityFrameworkCore"]
|
[submodule "modules/EntityFrameworkCore"]
|
||||||
path = modules/EntityFrameworkCore
|
path = modules/EntityFrameworkCore
|
||||||
url = https://github.com/aspnet/EntityFrameworkCore.git
|
url = https://github.com/aspnet/EntityFrameworkCore.git
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<RepositoryBuildOrder Include="DotNetTools" Order="1" />
|
|
||||||
<RepositoryBuildOrder Include="Razor" Order="6" />
|
<RepositoryBuildOrder Include="Razor" Order="6" />
|
||||||
<RepositoryBuildOrder Include="HttpAbstractions" Order="6" />
|
<RepositoryBuildOrder Include="HttpAbstractions" Order="6" />
|
||||||
<RepositoryBuildOrder Include="HttpClientFactory" Order="6" />
|
<RepositoryBuildOrder Include="HttpClientFactory" Order="6" />
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,16 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectToExclude Include="$(RepositoryRoot)src\Middleware\WebSockets\samples\**\*.csproj" />
|
<ProjectToExclude Include="
|
||||||
|
$(RepositoryRoot)src\Middleware\WebSockets\samples\**\*.csproj;
|
||||||
|
$(RepositoryRoot)src\Tools\dotnet-watch\test\TestProjects\**\*.csproj;
|
||||||
|
" />
|
||||||
|
|
||||||
<ProjectToBuild Include="
|
<ProjectToBuild Include="
|
||||||
$(RepositoryRoot)src\Features\JsonPatch\**\*.*proj;
|
$(RepositoryRoot)src\Features\JsonPatch\**\*.*proj;
|
||||||
$(RepositoryRoot)src\DataProtection\**\*.*proj;
|
$(RepositoryRoot)src\DataProtection\**\*.*proj;
|
||||||
$(RepositoryRoot)src\Html\**\*.*proj;
|
$(RepositoryRoot)src\Html\**\*.*proj;
|
||||||
|
$(RepositoryRoot)src\Tools\**\*.*proj;
|
||||||
$(RepositoryRoot)src\Middleware\**\*.*proj;
|
$(RepositoryRoot)src\Middleware\**\*.*proj;
|
||||||
"
|
"
|
||||||
Exclude="@(ProjectToExclude)" />
|
Exclude="@(ProjectToExclude)" />
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@
|
||||||
<ShippedRepository Include="BrowserLink" />
|
<ShippedRepository Include="BrowserLink" />
|
||||||
<ShippedRepository Include="CORS" />
|
<ShippedRepository Include="CORS" />
|
||||||
<ShippedRepository Include="Diagnostics" />
|
<ShippedRepository Include="Diagnostics" />
|
||||||
<ShippedRepository Include="DotNetTools" />
|
|
||||||
<ShippedRepository Include="EntityFrameworkCore" />
|
<ShippedRepository Include="EntityFrameworkCore" />
|
||||||
<ShippedRepository Include="Hosting" />
|
<ShippedRepository Include="Hosting" />
|
||||||
<ShippedRepository Include="HttpAbstractions" />
|
<ShippedRepository Include="HttpAbstractions" />
|
||||||
|
|
|
||||||
|
|
@ -7,23 +7,29 @@
|
||||||
|
|
||||||
<ItemGroup Label="ProdCon dependencies">
|
<ItemGroup Label="ProdCon dependencies">
|
||||||
<!-- These dependencies must use version variables because they may be overriden by ProdCon builds. -->
|
<!-- These dependencies must use version variables because they may be overriden by ProdCon builds. -->
|
||||||
|
<LatestPackageReference Include="Microsoft.AspNetCore.Certificates.Generation.Sources" Version="$(MicrosoftAspNetCoreCertificatesGenerationSourcesPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.CSharp" Version="$(MicrosoftCSharpPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.CSharp" Version="$(MicrosoftCSharpPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="$(MicrosoftEntityFrameworkCoreInMemoryPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="$(MicrosoftEntityFrameworkCoreInMemoryPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.EntityFrameworkCore" Version="$(MicrosoftEntityFrameworkCorePackageVersion)" />
|
<LatestPackageReference Include="Microsoft.EntityFrameworkCore" Version="$(MicrosoftEntityFrameworkCorePackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.Extensions.ClosedGenericMatcher.Sources" Version="$(MicrosoftExtensionsClosedGenericMatcherSourcesPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.Extensions.ClosedGenericMatcher.Sources" Version="$(MicrosoftExtensionsClosedGenericMatcherSourcesPackageVersion)" />
|
||||||
|
<LatestPackageReference Include="Microsoft.Extensions.CommandLineUtils.Sources" Version="$(MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="$(MicrosoftExtensionsConfigurationCommandLinePackageVersion)" />
|
<LatestPackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="$(MicrosoftExtensionsConfigurationCommandLinePackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="$(MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="$(MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(MicrosoftExtensionsConfigurationJsonPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(MicrosoftExtensionsConfigurationJsonPackageVersion)" />
|
||||||
|
<LatestPackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="$(MicrosoftExtensionsConfigurationUserSecretsPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.Extensions.Configuration" Version="$(MicrosoftExtensionsConfigurationPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.Extensions.Configuration" Version="$(MicrosoftExtensionsConfigurationPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
|
<LatestPackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
|
||||||
|
<LatestPackageReference Include="Microsoft.Extensions.Process.Sources" Version="$(MicrosoftExtensionsProcessSourcesPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.Extensions.WebEncoders.Sources" Version="$(MicrosoftExtensionsWebEncodersSourcesPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.Extensions.WebEncoders.Sources" Version="$(MicrosoftExtensionsWebEncodersSourcesPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.Extensions.WebEncoders" Version="$(MicrosoftExtensionsWebEncodersPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.Extensions.WebEncoders" Version="$(MicrosoftExtensionsWebEncodersPackageVersion)" />
|
||||||
|
<LatestPackageReference Include="System.Data.SqlClient" Version="$(SystemDataSqlClientPackageVersion)" />
|
||||||
<LatestPackageReference Include="System.Net.WebSockets.WebSocketProtocol" Version="$(SystemNetWebSocketsWebSocketProtocolPackageVersion)" />
|
<LatestPackageReference Include="System.Net.WebSockets.WebSocketProtocol" Version="$(SystemNetWebSocketsWebSocketProtocolPackageVersion)" />
|
||||||
|
<LatestPackageReference Include="System.Security.Cryptography.Cng" Version="$(SystemSecurityCryptographyCngPackageVersion)" />
|
||||||
<LatestPackageReference Include="System.Text.Encodings.Web" Version="$(SystemTextEncodingsWebPackageVersion)" />
|
<LatestPackageReference Include="System.Text.Encodings.Web" Version="$(SystemTextEncodingsWebPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
{
|
||||||
|
"adx": {
|
||||||
|
"rules": [
|
||||||
|
"AdxVerificationCompositeRule"
|
||||||
|
],
|
||||||
|
"packages": {
|
||||||
|
"dotnet-watch": {
|
||||||
|
"packageTypes": [
|
||||||
|
"DotnetTool"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dotnet-sql-cache": {
|
||||||
|
"packageTypes": [
|
||||||
|
"DotnetTool"
|
||||||
|
],
|
||||||
|
"Exclusions": {
|
||||||
|
"NEUTRAL_RESOURCES_LANGUAGE": {
|
||||||
|
"tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
|
||||||
|
},
|
||||||
|
"WRONG_PUBLICKEYTOKEN": {
|
||||||
|
"tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.0/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/runtimes/unix/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg."
|
||||||
|
},
|
||||||
|
"ASSEMBLY_INFORMATIONAL_VERSION_MISMATCH": {
|
||||||
|
"tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.0/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/runtimes/unix/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg."
|
||||||
|
},
|
||||||
|
"ASSEMBLY_FILE_VERSION_MISMATCH": {
|
||||||
|
"tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.0/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/runtimes/unix/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg."
|
||||||
|
},
|
||||||
|
"ASSEMBLY_VERSION_MISMATCH": {
|
||||||
|
"tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.0/System.Text.Encoding.CodePages.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/runtimes/unix/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/runtimes/win/lib/netcoreapp2.1/System.Data.SqlClient.dll": "Assembly is built by another project but bundled in our nupkg."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dotnet-user-secrets": {
|
||||||
|
"packageTypes": [
|
||||||
|
"DotnetTool"
|
||||||
|
],
|
||||||
|
"Exclusions": {
|
||||||
|
"NEUTRAL_RESOURCES_LANGUAGE": {
|
||||||
|
"tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
|
||||||
|
},
|
||||||
|
"SERVICING_ATTRIBUTE": {
|
||||||
|
"tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Assembly is built by another project but bundled in our nupkg."
|
||||||
|
},
|
||||||
|
"WRONG_PUBLICKEYTOKEN": {
|
||||||
|
"tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
|
||||||
|
},
|
||||||
|
"ASSEMBLY_INFORMATIONAL_VERSION_MISMATCH": {
|
||||||
|
"tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
|
||||||
|
},
|
||||||
|
"ASSEMBLY_FILE_VERSION_MISMATCH": {
|
||||||
|
"tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
|
||||||
|
},
|
||||||
|
"ASSEMBLY_VERSION_MISMATCH": {
|
||||||
|
"tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Assembly is built by another project but bundled in our nupkg.",
|
||||||
|
"tools/netcoreapp2.1/any/System.Runtime.CompilerServices.Unsafe.dll": "Assembly is built by another project but bundled in our nupkg."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dotnet-dev-certs": {
|
||||||
|
"packageTypes": [
|
||||||
|
"DotnetTool"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Microsoft.AspNetCore.DeveloperCertificates.XPlat": {
|
||||||
|
"Exclusions": {
|
||||||
|
"DOC_MISSING": {
|
||||||
|
"lib/netcoreapp2.1/Microsoft.AspNetCore.DeveloperCertificates.XPlat.dll": "Docs not required to shipoob package"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Default": {
|
||||||
|
"rules": [
|
||||||
|
"DefaultCompositeRule"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,11 @@
|
||||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" ProjectPath="$(RepositoryRoot)src\DataProtection\StackExchangeRedis\src\Microsoft.AspNetCore.DataProtection.StackExchangeRedis.csproj" />
|
<ProjectReferenceProvider Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" ProjectPath="$(RepositoryRoot)src\DataProtection\StackExchangeRedis\src\Microsoft.AspNetCore.DataProtection.StackExchangeRedis.csproj" />
|
||||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.DataProtection.SystemWeb" ProjectPath="$(RepositoryRoot)src\DataProtection\SystemWeb\src\Microsoft.AspNetCore.DataProtection.SystemWeb.csproj" />
|
<ProjectReferenceProvider Include="Microsoft.AspNetCore.DataProtection.SystemWeb" ProjectPath="$(RepositoryRoot)src\DataProtection\SystemWeb\src\Microsoft.AspNetCore.DataProtection.SystemWeb.csproj" />
|
||||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Html.Abstractions" ProjectPath="$(RepositoryRoot)src\Html\Abstractions\src\Microsoft.AspNetCore.Html.Abstractions.csproj" />
|
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Html.Abstractions" ProjectPath="$(RepositoryRoot)src\Html\Abstractions\src\Microsoft.AspNetCore.Html.Abstractions.csproj" />
|
||||||
|
<ProjectReferenceProvider Include="dotnet-dev-certs" ProjectPath="$(RepositoryRoot)src\Tools\dotnet-dev-certs\src\dotnet-dev-certs.csproj" />
|
||||||
|
<ProjectReferenceProvider Include="dotnet-sql-cache" ProjectPath="$(RepositoryRoot)src\Tools\dotnet-sql-cache\src\dotnet-sql-cache.csproj" />
|
||||||
|
<ProjectReferenceProvider Include="dotnet-user-secrets" ProjectPath="$(RepositoryRoot)src\Tools\dotnet-user-secrets\src\dotnet-user-secrets.csproj" />
|
||||||
|
<ProjectReferenceProvider Include="dotnet-watch" ProjectPath="$(RepositoryRoot)src\Tools\dotnet-watch\src\dotnet-watch.csproj" />
|
||||||
|
<ProjectReferenceProvider Include="Microsoft.AspNetCore.DeveloperCertificates.XPlat" ProjectPath="$(RepositoryRoot)src\Tools\FirstRunCertGenerator\src\Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj" />
|
||||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.WebSockets" ProjectPath="$(RepositoryRoot)src\Middleware\WebSockets\src\Microsoft.AspNetCore.WebSockets.csproj" />
|
<ProjectReferenceProvider Include="Microsoft.AspNetCore.WebSockets" ProjectPath="$(RepositoryRoot)src\Middleware\WebSockets\src\Microsoft.AspNetCore.WebSockets.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
<Baseline Version="2.2.0">
|
<Baseline Version="2.2.0">
|
||||||
|
<Package Id="dotnet-dev-certs" Version="2.2.0" />
|
||||||
|
<Package Id="dotnet-sql-cache" Version="2.2.0" />
|
||||||
|
<Package Id="dotnet-user-secrets" Version="2.2.0" />
|
||||||
|
<Package Id="dotnet-watch" Version="2.2.0" />
|
||||||
<Package Id="Microsoft.AspNetCore.Cryptography.Internal" Version="2.2.0" />
|
<Package Id="Microsoft.AspNetCore.Cryptography.Internal" Version="2.2.0" />
|
||||||
<Package Id="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="2.2.0" />
|
<Package Id="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="2.2.0" />
|
||||||
<Package Id="Microsoft.AspNetCore.DataProtection" Version="2.2.0" />
|
<Package Id="Microsoft.AspNetCore.DataProtection" Version="2.2.0" />
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit d745b8c161d42665ceb51597888062336fec7764
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<Project>
|
||||||
|
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<BaseIntermediateOutputPath>$(RepositoryRoot)obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
|
||||||
|
<BaseOutputPath>$(RepositoryRoot)bin\$(MSBuildProjectName)\</BaseOutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<Project>
|
||||||
|
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.targets))\Directory.Build.targets" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<UseLatestPackageReferences Condition=" '$(PackAsTool)' == 'true' ">true</UseLatestPackageReferences>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="CleanPublishDir" AfterTargets="CoreClean">
|
||||||
|
<RemoveDir Directories="$(PublishDir)" />
|
||||||
|
</Target>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Certificates.Generation;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.DeveloperCertificates.XPlat
|
||||||
|
{
|
||||||
|
public static class CertificateGenerator
|
||||||
|
{
|
||||||
|
public static void GenerateAspNetHttpsCertificate()
|
||||||
|
{
|
||||||
|
var manager = new CertificateManager();
|
||||||
|
var now = DateTimeOffset.Now;
|
||||||
|
manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<Description>Package for the CLI first run experience.</Description>
|
||||||
|
<DefineConstants>$(DefineConstants);XPLAT</DefineConstants>
|
||||||
|
<PackageTags>aspnet;cli</PackageTags>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Certificates.Generation.Sources" PrivateAssets="All" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
DotNetTools
|
||||||
|
===========
|
||||||
|
|
||||||
|
## Projects
|
||||||
|
|
||||||
|
The folder contains command-line tools for ASP.NET Core that are bundled* in the .NET Core CLI. Follow the links below for more details on each tool.
|
||||||
|
|
||||||
|
- [dotnet-watch](dotnet-watch/README.md)
|
||||||
|
- [dotnet-user-secrets](dotnet-user-secrets/README.md)
|
||||||
|
- [dotnet-sql-cache](dotnet-sql-cache/README.md)
|
||||||
|
- [dotnet-dev-certs](dotnet-dev-certs/README.md)
|
||||||
|
|
||||||
|
*\*This applies to .NET Core CLI 2.1.300-preview2 and up. For earlier versions of the CLI, these tools must be installed separately.*
|
||||||
|
|
||||||
|
*For 2.0 CLI and earlier, see <https://github.com/aspnet/DotNetTools/tree/rel/2.0.0/README.md> for details.*
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The command line tools can be invoked as a subcommand of `dotnet`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dotnet watch
|
||||||
|
dotnet user-secrets
|
||||||
|
dotnet sql-cache
|
||||||
|
dotnet dev-certs
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `--help` to see more details. For example,
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet watch --help
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
dotnet-dev-certs
|
||||||
|
================
|
||||||
|
|
||||||
|
`dotnet-dev-certs` is a command line tool to generate certificates used in ASP.NET Core during development.
|
||||||
|
|
||||||
|
### How To Use
|
||||||
|
|
||||||
|
Run `dotnet dev-certs --help` for more information about usage.
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
// 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.Runtime.InteropServices;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using Microsoft.AspNetCore.Certificates.Generation;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
||||||
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
private const int CriticalError = -1;
|
||||||
|
private const int Success = 0;
|
||||||
|
private const int ErrorCreatingTheCertificate = 1;
|
||||||
|
private const int ErrorSavingTheCertificate = 2;
|
||||||
|
private const int ErrorExportingTheCertificate = 3;
|
||||||
|
private const int ErrorTrustingTheCertificate = 4;
|
||||||
|
private const int ErrorUserCancelledTrustPrompt = 5;
|
||||||
|
private const int ErrorNoValidCertificateFound = 6;
|
||||||
|
private const int ErrorCertificateNotTrusted = 7;
|
||||||
|
private const int ErrorCleaningUpCertificates = 8;
|
||||||
|
|
||||||
|
public static readonly TimeSpan HttpsCertificateValidity = TimeSpan.FromDays(365);
|
||||||
|
|
||||||
|
public static int Main(string[] args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication
|
||||||
|
{
|
||||||
|
Name = "dotnet dev-certs"
|
||||||
|
};
|
||||||
|
|
||||||
|
app.Command("https", c =>
|
||||||
|
{
|
||||||
|
var exportPath = c.Option("-ep|--export-path",
|
||||||
|
"Full path to the exported certificate",
|
||||||
|
CommandOptionType.SingleValue);
|
||||||
|
|
||||||
|
var password = c.Option("-p|--password",
|
||||||
|
"Password to use when exporting the certificate with the private key into a pfx file",
|
||||||
|
CommandOptionType.SingleValue);
|
||||||
|
|
||||||
|
var check = c.Option(
|
||||||
|
"-c|--check",
|
||||||
|
"Check for the existence of the certificate but do not perform any action",
|
||||||
|
CommandOptionType.NoValue);
|
||||||
|
|
||||||
|
var clean = c.Option(
|
||||||
|
"--clean",
|
||||||
|
"Cleans all HTTPS development certificates from the machine.",
|
||||||
|
CommandOptionType.NoValue);
|
||||||
|
|
||||||
|
CommandOption trust = null;
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
{
|
||||||
|
trust = c.Option("-t|--trust",
|
||||||
|
"Trust the certificate on the current platform",
|
||||||
|
CommandOptionType.NoValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
var verbose = c.Option("-v|--verbose",
|
||||||
|
"Display more debug information.",
|
||||||
|
CommandOptionType.NoValue);
|
||||||
|
|
||||||
|
var quiet = c.Option("-q|--quiet",
|
||||||
|
"Display warnings and errors only.",
|
||||||
|
CommandOptionType.NoValue);
|
||||||
|
|
||||||
|
c.HelpOption("-h|--help");
|
||||||
|
|
||||||
|
c.OnExecute(() =>
|
||||||
|
{
|
||||||
|
var reporter = new ConsoleReporter(PhysicalConsole.Singleton, verbose.HasValue(), quiet.HasValue());
|
||||||
|
if ((clean.HasValue() && (exportPath.HasValue() || password.HasValue() || trust?.HasValue() == true)) ||
|
||||||
|
(check.HasValue() && (exportPath.HasValue() || password.HasValue() || clean.HasValue())))
|
||||||
|
{
|
||||||
|
reporter.Error(@"Incompatible set of flags. Sample usages
|
||||||
|
'dotnet dev-certs https --clean'
|
||||||
|
'dotnet dev-certs https --check --trust'
|
||||||
|
'dotnet dev-certs https -ep ./certificate.pfx -p password --trust'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (check.HasValue())
|
||||||
|
{
|
||||||
|
return CheckHttpsCertificate(trust, reporter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clean.HasValue())
|
||||||
|
{
|
||||||
|
return CleanHttpsCertificates(reporter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return EnsureHttpsCertificate(exportPath, password, trust, reporter);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.HelpOption("-h|--help");
|
||||||
|
|
||||||
|
app.OnExecute(() =>
|
||||||
|
{
|
||||||
|
app.ShowHelp();
|
||||||
|
return Success;
|
||||||
|
});
|
||||||
|
|
||||||
|
return app.Execute(args);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return CriticalError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CleanHttpsCertificates(IReporter reporter)
|
||||||
|
{
|
||||||
|
var manager = new CertificateManager();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
reporter.Output("Cleaning HTTPS development certificates from the machine. A prompt might get " +
|
||||||
|
"displayed to confirm the removal of some of the certificates.");
|
||||||
|
}
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
{
|
||||||
|
reporter.Output("Cleaning HTTPS development certificates from the machine. This operation might " +
|
||||||
|
"require elevated privileges. If that is the case, a prompt for credentials will be displayed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.CleanupHttpsCertificates();
|
||||||
|
reporter.Verbose("HTTPS development certificates successfully removed from the machine.");
|
||||||
|
return Success;
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
reporter.Error("There was an error trying to clean HTTPS development certificates on this machine.");
|
||||||
|
reporter.Error(e.Message);
|
||||||
|
|
||||||
|
return ErrorCleaningUpCertificates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CheckHttpsCertificate(CommandOption trust, IReporter reporter)
|
||||||
|
{
|
||||||
|
var now = DateTimeOffset.Now;
|
||||||
|
var certificateManager = new CertificateManager();
|
||||||
|
var certificates = certificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
|
||||||
|
if (certificates.Count == 0)
|
||||||
|
{
|
||||||
|
reporter.Verbose("No valid certificate found.");
|
||||||
|
return ErrorNoValidCertificateFound;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reporter.Verbose("A valid certificate was found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trust != null && trust.HasValue())
|
||||||
|
{
|
||||||
|
var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root;
|
||||||
|
var trustedCertificates = certificateManager.ListCertificates(CertificatePurpose.HTTPS, store, StoreLocation.CurrentUser, isValid: true);
|
||||||
|
if (!certificates.Any(c => certificateManager.IsTrusted(c)))
|
||||||
|
{
|
||||||
|
reporter.Verbose($@"The following certificates were found, but none of them is trusted:
|
||||||
|
{string.Join(Environment.NewLine, certificates.Select(c => $"{c.Subject} - {c.Thumbprint}"))}");
|
||||||
|
return ErrorCertificateNotTrusted;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reporter.Verbose("A trusted certificate was found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption trust, IReporter reporter)
|
||||||
|
{
|
||||||
|
var now = DateTimeOffset.Now;
|
||||||
|
var manager = new CertificateManager();
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && trust?.HasValue() == true)
|
||||||
|
{
|
||||||
|
reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " +
|
||||||
|
"already trusted we will run the following command:" + Environment.NewLine +
|
||||||
|
"'sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <<certificate>>'" +
|
||||||
|
Environment.NewLine + "This command might prompt you for your password to install the certificate " +
|
||||||
|
"on the system keychain.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && trust?.HasValue() == true)
|
||||||
|
{
|
||||||
|
reporter.Warn("Trusting the HTTPS development certificate was requested. A confirmation prompt will be displayed " +
|
||||||
|
"if the certificate was not previously trusted. Click yes on the prompt to trust the certificate.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(
|
||||||
|
now,
|
||||||
|
now.Add(HttpsCertificateValidity),
|
||||||
|
exportPath.Value(),
|
||||||
|
trust == null ? false : trust.HasValue(),
|
||||||
|
password.HasValue(),
|
||||||
|
password.Value());
|
||||||
|
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case EnsureCertificateResult.Succeeded:
|
||||||
|
reporter.Output("The HTTPS developer certificate was generated successfully.");
|
||||||
|
if (exportPath.Value() != null)
|
||||||
|
{
|
||||||
|
reporter.Verbose($"The certificate was exported to {Path.GetFullPath(exportPath.Value())}");
|
||||||
|
}
|
||||||
|
return Success;
|
||||||
|
case EnsureCertificateResult.ValidCertificatePresent:
|
||||||
|
reporter.Output("A valid HTTPS certificate is already present.");
|
||||||
|
if (exportPath.Value() != null)
|
||||||
|
{
|
||||||
|
reporter.Verbose($"The certificate was exported to {Path.GetFullPath(exportPath.Value())}");
|
||||||
|
}
|
||||||
|
return Success;
|
||||||
|
case EnsureCertificateResult.ErrorCreatingTheCertificate:
|
||||||
|
reporter.Error("There was an error creating the HTTPS developer certificate.");
|
||||||
|
return ErrorCreatingTheCertificate;
|
||||||
|
case EnsureCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore:
|
||||||
|
reporter.Error("There was an error saving the HTTPS developer certificate to the current user personal certificate store.");
|
||||||
|
return ErrorSavingTheCertificate;
|
||||||
|
case EnsureCertificateResult.ErrorExportingTheCertificate:
|
||||||
|
reporter.Warn("There was an error exporting HTTPS developer certificate to a file.");
|
||||||
|
return ErrorExportingTheCertificate;
|
||||||
|
case EnsureCertificateResult.FailedToTrustTheCertificate:
|
||||||
|
reporter.Warn("There was an error trusting HTTPS developer certificate.");
|
||||||
|
return ErrorTrustingTheCertificate;
|
||||||
|
case EnsureCertificateResult.UserCancelledTrustStep:
|
||||||
|
reporter.Warn("The user cancelled the trust step.");
|
||||||
|
return ErrorUserCancelledTrustPrompt;
|
||||||
|
default:
|
||||||
|
reporter.Error("Something went wrong. The HTTPS developer certificate could not be created.");
|
||||||
|
return CriticalError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<OutputType>exe</OutputType>
|
||||||
|
<Description>Command line tool to generate certificates used in ASP.NET Core during development.</Description>
|
||||||
|
<RootNamespace>Microsoft.AspNetCore.DeveloperCertificates.Tools</RootNamespace>
|
||||||
|
<PackageTags>dotnet;developercertificates</PackageTags>
|
||||||
|
<PackAsTool>true</PackAsTool>
|
||||||
|
<!-- This is a requirement for Microsoft tool packages only. -->
|
||||||
|
<PackAsToolShimRuntimeIdentifiers>win-x64;win-x86</PackAsToolShimRuntimeIdentifiers>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\..\shared\src\CliContext.cs" Link="CliContext.cs" />
|
||||||
|
<Compile Include="..\..\shared\src\CommandLineApplicationExtensions.cs" Link="CommandLineApplicationExtensions.cs" />
|
||||||
|
<Compile Include="..\..\shared\src\ConsoleReporter.cs" Link="ConsoleReporter.cs" />
|
||||||
|
<Compile Include="..\..\shared\src\DebugHelper.cs" Link="DebugHelper.cs" />
|
||||||
|
<Compile Include="..\..\shared\src\Ensure.cs" Link="Ensure.cs" />
|
||||||
|
<Compile Include="..\..\shared\src\IConsole.cs" Link="IConsole.cs" />
|
||||||
|
<Compile Include="..\..\shared\src\IReporter.cs" Link="IReporter.cs" />
|
||||||
|
<Compile Include="..\..\shared\src\NullReporter.cs" Link="NullReporter.cs" />
|
||||||
|
<Compile Include="..\..\shared\src\PhysicalConsole.cs" Link="PhysicalConsole.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Certificates.Generation.Sources" PrivateAssets="All" />
|
||||||
|
<Reference Include="Microsoft.Extensions.CommandLineUtils.Sources" PrivateAssets="All" />
|
||||||
|
<Reference Include="System.Security.Cryptography.Cng" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
dotnet-sql-cache
|
||||||
|
================
|
||||||
|
|
||||||
|
`dotnet-sql-cache` is a command line tool that creates table and indexes in Microsoft SQL Server database to be used for distributed caching
|
||||||
|
|
||||||
|
### How To Use
|
||||||
|
|
||||||
|
Run `dotnet sql-cache --help` for more information about usage.
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
// 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.Data;
|
||||||
|
using System.Data.SqlClient;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Caching.SqlConfig.Tools
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
private string _connectionString = null;
|
||||||
|
private string _schemaName = null;
|
||||||
|
private string _tableName = null;
|
||||||
|
private readonly IConsole _console;
|
||||||
|
|
||||||
|
public Program(IConsole console)
|
||||||
|
{
|
||||||
|
Ensure.NotNull(console, nameof(console));
|
||||||
|
|
||||||
|
_console = console;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int Main(string[] args)
|
||||||
|
{
|
||||||
|
return new Program(PhysicalConsole.Singleton).Run(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Run(string[] args)
|
||||||
|
{
|
||||||
|
DebugHelper.HandleDebugSwitch(ref args);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication
|
||||||
|
{
|
||||||
|
Name = "dotnet sql-cache",
|
||||||
|
FullName = "SQL Server Cache Command Line Tool",
|
||||||
|
Description =
|
||||||
|
"Creates table and indexes in Microsoft SQL Server database to be used for distributed caching",
|
||||||
|
};
|
||||||
|
|
||||||
|
app.HelpOption();
|
||||||
|
app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly);
|
||||||
|
var verbose = app.VerboseOption();
|
||||||
|
|
||||||
|
app.Command("create", command =>
|
||||||
|
{
|
||||||
|
command.Description = app.Description;
|
||||||
|
|
||||||
|
var connectionStringArg = command.Argument(
|
||||||
|
"[connectionString]", "The connection string to connect to the database.");
|
||||||
|
|
||||||
|
var schemaNameArg = command.Argument(
|
||||||
|
"[schemaName]", "Name of the table schema.");
|
||||||
|
|
||||||
|
var tableNameArg = command.Argument(
|
||||||
|
"[tableName]", "Name of the table to be created.");
|
||||||
|
|
||||||
|
command.HelpOption();
|
||||||
|
|
||||||
|
command.OnExecute(() =>
|
||||||
|
{
|
||||||
|
var reporter = CreateReporter(verbose.HasValue());
|
||||||
|
if (string.IsNullOrEmpty(connectionStringArg.Value)
|
||||||
|
|| string.IsNullOrEmpty(schemaNameArg.Value)
|
||||||
|
|| string.IsNullOrEmpty(tableNameArg.Value))
|
||||||
|
{
|
||||||
|
reporter.Error("Invalid input");
|
||||||
|
app.ShowHelp();
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
_connectionString = connectionStringArg.Value;
|
||||||
|
_schemaName = schemaNameArg.Value;
|
||||||
|
_tableName = tableNameArg.Value;
|
||||||
|
|
||||||
|
return CreateTableAndIndexes(reporter);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show help information if no subcommand/option was specified.
|
||||||
|
app.OnExecute(() =>
|
||||||
|
{
|
||||||
|
app.ShowHelp();
|
||||||
|
return 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
return app.Execute(args);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
CreateReporter(verbose: false).Error($"An error occurred. {exception.Message}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReporter CreateReporter(bool verbose)
|
||||||
|
=> new ConsoleReporter(_console, verbose, quiet: false);
|
||||||
|
private int CreateTableAndIndexes(IReporter reporter)
|
||||||
|
{
|
||||||
|
ValidateConnectionString();
|
||||||
|
|
||||||
|
using (var connection = new SqlConnection(_connectionString))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
var sqlQueries = new SqlQueries(_schemaName, _tableName);
|
||||||
|
var command = new SqlCommand(sqlQueries.TableInfo, connection);
|
||||||
|
|
||||||
|
using (var reader = command.ExecuteReader(CommandBehavior.SingleRow))
|
||||||
|
{
|
||||||
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
reporter.Warn(
|
||||||
|
$"Table with schema '{_schemaName}' and name '{_tableName}' already exists. " +
|
||||||
|
"Provide a different table name and try again.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var transaction = connection.BeginTransaction())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
command = new SqlCommand(sqlQueries.CreateTable, connection, transaction);
|
||||||
|
|
||||||
|
reporter.Verbose($"Executing {command.CommandText}");
|
||||||
|
command.ExecuteNonQuery();
|
||||||
|
|
||||||
|
command = new SqlCommand(
|
||||||
|
sqlQueries.CreateNonClusteredIndexOnExpirationTime,
|
||||||
|
connection,
|
||||||
|
transaction);
|
||||||
|
|
||||||
|
reporter.Verbose($"Executing {command.CommandText}");
|
||||||
|
command.ExecuteNonQuery();
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
|
||||||
|
reporter.Output("Table and index were created successfully.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
reporter.Error(
|
||||||
|
$"An error occurred while trying to create the table and index. {ex.Message}");
|
||||||
|
transaction.Rollback();
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateConnectionString()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
new SqlConnectionStringBuilder(_connectionString);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"Invalid SQL Server connection string '{_connectionString}'. {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Caching.SqlConfig.Tools
|
||||||
|
{
|
||||||
|
internal class SqlQueries
|
||||||
|
{
|
||||||
|
private const string CreateTableFormat = "CREATE TABLE {0}(" +
|
||||||
|
// Maximum size of primary key column is 900 bytes (898 bytes from key + 2 additional bytes used by the
|
||||||
|
// Sql Server). In the case where the key is greater than 898 bytes, then it gets truncated.
|
||||||
|
// - Add collation to the key column to make it case-sensitive
|
||||||
|
"Id nvarchar(449) COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL, " +
|
||||||
|
"Value varbinary(MAX) NOT NULL, " +
|
||||||
|
"ExpiresAtTime datetimeoffset NOT NULL, " +
|
||||||
|
"SlidingExpirationInSeconds bigint NULL," +
|
||||||
|
"AbsoluteExpiration datetimeoffset NULL, " +
|
||||||
|
"PRIMARY KEY (Id))";
|
||||||
|
|
||||||
|
private const string CreateNonClusteredIndexOnExpirationTimeFormat
|
||||||
|
= "CREATE NONCLUSTERED INDEX Index_ExpiresAtTime ON {0}(ExpiresAtTime)";
|
||||||
|
|
||||||
|
private const string TableInfoFormat =
|
||||||
|
"SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE " +
|
||||||
|
"FROM INFORMATION_SCHEMA.TABLES " +
|
||||||
|
"WHERE TABLE_SCHEMA = '{0}' " +
|
||||||
|
"AND TABLE_NAME = '{1}'";
|
||||||
|
|
||||||
|
public SqlQueries(string schemaName, string tableName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(schemaName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Schema name cannot be empty or null");
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(tableName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Table name cannot be empty or null");
|
||||||
|
}
|
||||||
|
|
||||||
|
var tableNameWithSchema = string.Format(
|
||||||
|
"{0}.{1}", DelimitIdentifier(schemaName), DelimitIdentifier(tableName));
|
||||||
|
CreateTable = string.Format(CreateTableFormat, tableNameWithSchema);
|
||||||
|
CreateNonClusteredIndexOnExpirationTime = string.Format(
|
||||||
|
CreateNonClusteredIndexOnExpirationTimeFormat,
|
||||||
|
tableNameWithSchema);
|
||||||
|
TableInfo = string.Format(TableInfoFormat, EscapeLiteral(schemaName), EscapeLiteral(tableName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CreateTable { get; }
|
||||||
|
|
||||||
|
public string CreateNonClusteredIndexOnExpirationTime { get; }
|
||||||
|
|
||||||
|
public string TableInfo { get; }
|
||||||
|
|
||||||
|
// From EF's SqlServerQuerySqlGenerator
|
||||||
|
private string DelimitIdentifier(string identifier)
|
||||||
|
{
|
||||||
|
return "[" + identifier.Replace("]", "]]") + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string EscapeLiteral(string literal)
|
||||||
|
{
|
||||||
|
return literal.Replace("'", "''");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<OutputType>exe</OutputType>
|
||||||
|
<Description>Command line tool to create tables and indexes in a Microsoft SQL Server database for distributed caching.</Description>
|
||||||
|
<PackageTags>cache;distributedcache;sqlserver</PackageTags>
|
||||||
|
<PackAsTool>true</PackAsTool>
|
||||||
|
<!-- This is a requirement for Microsoft tool packages only. -->
|
||||||
|
<PackAsToolShimRuntimeIdentifiers>win-x64;win-x86</PackAsToolShimRuntimeIdentifiers>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\..\shared\src\**\*.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.Extensions.CommandLineUtils.Sources" PrivateAssets="All" />
|
||||||
|
<Reference Include="System.Data.SqlClient" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- These files should be signed by corefx -->
|
||||||
|
<ExcludePackageFileFromSigning Include="sni.dll" />
|
||||||
|
<ExcludePackageFileFromSigning Include="System.Data.SqlClient.dll" />
|
||||||
|
<ExcludePackageFileFromSigning Include="System.Runtime.CompilerServices.Unsafe.dll" />
|
||||||
|
<ExcludePackageFileFromSigning Include="System.Text.Encoding.CodePages.dll" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
dotnet-user-secrets
|
||||||
|
===================
|
||||||
|
|
||||||
|
`dotnet-user-secrets` is a command line tool for managing the secrets in a user secret store.
|
||||||
|
|
||||||
|
### How To Use
|
||||||
|
|
||||||
|
Run `dotnet user-secrets --help` for more information about usage.
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
// 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.Reflection;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.SecretManager.Tools.Internal;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools
|
||||||
|
{
|
||||||
|
public class CommandLineOptions
|
||||||
|
{
|
||||||
|
public ICommand Command { get; set; }
|
||||||
|
public string Configuration { get; private set; }
|
||||||
|
public string Id { get; private set; }
|
||||||
|
public bool IsHelp { get; private set; }
|
||||||
|
public bool IsVerbose { get; private set; }
|
||||||
|
public string Project { get; private set; }
|
||||||
|
|
||||||
|
public static CommandLineOptions Parse(string[] args, IConsole console)
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication()
|
||||||
|
{
|
||||||
|
Out = console.Out,
|
||||||
|
Error = console.Error,
|
||||||
|
Name = "dotnet user-secrets",
|
||||||
|
FullName = "User Secrets Manager",
|
||||||
|
Description = "Manages user secrets"
|
||||||
|
};
|
||||||
|
|
||||||
|
app.HelpOption();
|
||||||
|
app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly);
|
||||||
|
|
||||||
|
var optionVerbose = app.VerboseOption();
|
||||||
|
|
||||||
|
var optionProject = app.Option("-p|--project <PROJECT>", "Path to project. Defaults to searching the current directory.",
|
||||||
|
CommandOptionType.SingleValue, inherited: true);
|
||||||
|
|
||||||
|
var optionConfig = app.Option("-c|--configuration <CONFIGURATION>", $"The project configuration to use. Defaults to 'Debug'.",
|
||||||
|
CommandOptionType.SingleValue, inherited: true);
|
||||||
|
|
||||||
|
// the escape hatch if project evaluation fails, or if users want to alter a secret store other than the one
|
||||||
|
// in the current project
|
||||||
|
var optionId = app.Option("--id", "The user secret ID to use.",
|
||||||
|
CommandOptionType.SingleValue, inherited: true);
|
||||||
|
|
||||||
|
var options = new CommandLineOptions();
|
||||||
|
|
||||||
|
app.Command("set", c => SetCommand.Configure(c, options, console));
|
||||||
|
app.Command("remove", c => RemoveCommand.Configure(c, options));
|
||||||
|
app.Command("list", c => ListCommand.Configure(c, options));
|
||||||
|
app.Command("clear", c => ClearCommand.Configure(c, options));
|
||||||
|
|
||||||
|
// Show help information if no subcommand/option was specified.
|
||||||
|
app.OnExecute(() => app.ShowHelp());
|
||||||
|
|
||||||
|
if (app.Execute(args) != 0)
|
||||||
|
{
|
||||||
|
// when command line parsing error in subcommand
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.Configuration = optionConfig.Value();
|
||||||
|
options.Id = optionId.Value();
|
||||||
|
options.IsHelp = app.IsShowingInformation;
|
||||||
|
options.IsVerbose = optionVerbose.HasValue();
|
||||||
|
options.Project = optionProject.Value();
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
|
{
|
||||||
|
internal class ClearCommand : ICommand
|
||||||
|
{
|
||||||
|
public static void Configure(CommandLineApplication command, CommandLineOptions options)
|
||||||
|
{
|
||||||
|
command.Description = "Deletes all the application secrets";
|
||||||
|
command.HelpOption();
|
||||||
|
|
||||||
|
command.OnExecute(() =>
|
||||||
|
{
|
||||||
|
options.Command = new ClearCommand();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(CommandContext context)
|
||||||
|
{
|
||||||
|
context.SecretStore.Clear();
|
||||||
|
context.SecretStore.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
// 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 Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
|
{
|
||||||
|
public class CommandContext
|
||||||
|
{
|
||||||
|
public CommandContext(
|
||||||
|
SecretsStore store,
|
||||||
|
IReporter reporter,
|
||||||
|
IConsole console)
|
||||||
|
{
|
||||||
|
SecretStore = store;
|
||||||
|
Reporter = reporter;
|
||||||
|
Console = console;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConsole Console { get; }
|
||||||
|
public IReporter Reporter { get; }
|
||||||
|
public SecretsStore SecretStore { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.Extensions.SecretManager.Tools.Internal
|
||||||
|
{
|
||||||
|
public interface ICommand
|
||||||
|
{
|
||||||
|
void Execute(CommandContext context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
|
{
|
||||||
|
internal class ListCommand : ICommand
|
||||||
|
{
|
||||||
|
private readonly bool _jsonOutput;
|
||||||
|
|
||||||
|
public static void Configure(CommandLineApplication command, CommandLineOptions options)
|
||||||
|
{
|
||||||
|
command.Description = "Lists all the application secrets";
|
||||||
|
command.HelpOption();
|
||||||
|
|
||||||
|
var optJson = command.Option("--json", "Use json output. JSON is wrapped by '//BEGIN' and '//END'",
|
||||||
|
CommandOptionType.NoValue);
|
||||||
|
|
||||||
|
command.OnExecute(() =>
|
||||||
|
{
|
||||||
|
options.Command = new ListCommand(optJson.HasValue());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListCommand(bool jsonOutput)
|
||||||
|
{
|
||||||
|
_jsonOutput = jsonOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(CommandContext context)
|
||||||
|
{
|
||||||
|
if (_jsonOutput)
|
||||||
|
{
|
||||||
|
ReportJson(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.SecretStore.Count == 0)
|
||||||
|
{
|
||||||
|
context.Reporter.Output(Resources.Error_No_Secrets_Found);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var secret in context.SecretStore.AsEnumerable())
|
||||||
|
{
|
||||||
|
context.Reporter.Output(Resources.FormatMessage_Secret_Value_Format(secret.Key, secret.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReportJson(CommandContext context)
|
||||||
|
{
|
||||||
|
var jObject = new JObject();
|
||||||
|
foreach(var item in context.SecretStore.AsEnumerable())
|
||||||
|
{
|
||||||
|
jObject[item.Key] = item.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Reporter.Output("//BEGIN");
|
||||||
|
context.Reporter.Output(jObject.ToString(Formatting.Indented));
|
||||||
|
context.Reporter.Output("//END");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
// 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 Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
|
{
|
||||||
|
internal class MsBuildProjectFinder
|
||||||
|
{
|
||||||
|
private readonly string _directory;
|
||||||
|
|
||||||
|
public MsBuildProjectFinder(string directory)
|
||||||
|
{
|
||||||
|
Ensure.NotNullOrEmpty(directory, nameof(directory));
|
||||||
|
|
||||||
|
_directory = directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string FindMsBuildProject(string project)
|
||||||
|
{
|
||||||
|
var projectPath = project ?? _directory;
|
||||||
|
|
||||||
|
if (!Path.IsPathRooted(projectPath))
|
||||||
|
{
|
||||||
|
projectPath = Path.Combine(_directory, projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists(projectPath))
|
||||||
|
{
|
||||||
|
var projects = Directory.EnumerateFileSystemEntries(projectPath, "*.*proj", SearchOption.TopDirectoryOnly)
|
||||||
|
.Where(f => !".xproj".Equals(Path.GetExtension(f), StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (projects.Count > 1)
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException(Resources.FormatError_MultipleProjectsFound(projectPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projects.Count == 0)
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException(Resources.FormatError_NoProjectsFound(projectPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(projectPath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException(Resources.FormatError_ProjectPath_NotFound(projectPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
|
{
|
||||||
|
public class ProjectIdResolver
|
||||||
|
{
|
||||||
|
private const string DefaultConfig = "Debug";
|
||||||
|
private readonly IReporter _reporter;
|
||||||
|
private readonly string _targetsFile;
|
||||||
|
private readonly string _workingDirectory;
|
||||||
|
|
||||||
|
public ProjectIdResolver(IReporter reporter, string workingDirectory)
|
||||||
|
{
|
||||||
|
_workingDirectory = workingDirectory;
|
||||||
|
_reporter = reporter;
|
||||||
|
_targetsFile = FindTargetsFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Resolve(string project, string configuration)
|
||||||
|
{
|
||||||
|
var finder = new MsBuildProjectFinder(_workingDirectory);
|
||||||
|
var projectFile = finder.FindMsBuildProject(project);
|
||||||
|
|
||||||
|
_reporter.Verbose(Resources.FormatMessage_Project_File_Path(projectFile));
|
||||||
|
|
||||||
|
configuration = !string.IsNullOrEmpty(configuration)
|
||||||
|
? configuration
|
||||||
|
: DefaultConfig;
|
||||||
|
|
||||||
|
var outputFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var args = new[]
|
||||||
|
{
|
||||||
|
"msbuild",
|
||||||
|
projectFile,
|
||||||
|
"/nologo",
|
||||||
|
"/t:_ExtractUserSecretsMetadata", // defined in SecretManager.targets
|
||||||
|
"/p:_UserSecretsMetadataFile=" + outputFile,
|
||||||
|
"/p:Configuration=" + configuration,
|
||||||
|
"/p:CustomAfterMicrosoftCommonTargets=" + _targetsFile,
|
||||||
|
"/p:CustomAfterMicrosoftCommonCrossTargetingTargets=" + _targetsFile,
|
||||||
|
};
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = DotNetMuxer.MuxerPathOrDefault(),
|
||||||
|
Arguments = ArgumentEscaper.EscapeAndConcatenate(args),
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true
|
||||||
|
};
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
_reporter.Verbose($"Invoking '{psi.FileName} {psi.Arguments}'");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var process = Process.Start(psi);
|
||||||
|
process.WaitForExit();
|
||||||
|
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
_reporter.Verbose(process.StandardOutput.ReadToEnd());
|
||||||
|
_reporter.Verbose(process.StandardError.ReadToEnd());
|
||||||
|
throw new InvalidOperationException(Resources.FormatError_ProjectFailedToLoad(projectFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = File.ReadAllText(outputFile)?.Trim();
|
||||||
|
if (string.IsNullOrEmpty(id))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(Resources.FormatError_ProjectMissingId(projectFile));
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
TryDelete(outputFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FindTargetsFile()
|
||||||
|
{
|
||||||
|
var assemblyDir = Path.GetDirectoryName(typeof(ProjectIdResolver).Assembly.Location);
|
||||||
|
var searchPaths = new[]
|
||||||
|
{
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "assets"),
|
||||||
|
Path.Combine(assemblyDir, "assets"),
|
||||||
|
AppContext.BaseDirectory,
|
||||||
|
assemblyDir,
|
||||||
|
};
|
||||||
|
|
||||||
|
var targetPath = searchPaths.Select(p => Path.Combine(p, "SecretManager.targets")).FirstOrDefault(File.Exists);
|
||||||
|
if (targetPath == null)
|
||||||
|
{
|
||||||
|
_reporter.Error("Fatal error: could not find SecretManager.targets");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return targetPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDelete(string file)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(file))
|
||||||
|
{
|
||||||
|
File.Delete(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// whatever
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.Collections.Generic;
|
||||||
|
using Microsoft.Extensions.Configuration.Json;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
|
{
|
||||||
|
public class ReadableJsonConfigurationProvider : JsonConfigurationProvider
|
||||||
|
{
|
||||||
|
public ReadableJsonConfigurationProvider()
|
||||||
|
: base(new JsonConfigurationSource())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDictionary<string, string> CurrentData => Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 Microsoft.Extensions.CommandLineUtils;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
|
{
|
||||||
|
internal class RemoveCommand : ICommand
|
||||||
|
{
|
||||||
|
private readonly string _keyName;
|
||||||
|
|
||||||
|
public static void Configure(CommandLineApplication command, CommandLineOptions options)
|
||||||
|
{
|
||||||
|
command.Description = "Removes the specified user secret";
|
||||||
|
command.HelpOption();
|
||||||
|
|
||||||
|
var keyArg = command.Argument("[name]", "Name of the secret");
|
||||||
|
command.OnExecute(() =>
|
||||||
|
{
|
||||||
|
if (keyArg.Value == null)
|
||||||
|
{
|
||||||
|
throw new CommandParsingException(command, Resources.FormatError_MissingArgument("name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
options.Command = new RemoveCommand(keyArg.Value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public RemoveCommand(string keyName)
|
||||||
|
{
|
||||||
|
_keyName = keyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(CommandContext context)
|
||||||
|
{
|
||||||
|
if (!context.SecretStore.ContainsKey(_keyName))
|
||||||
|
{
|
||||||
|
context.Reporter.Warn(Resources.FormatError_Missing_Secret(_keyName));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.SecretStore.Remove(_keyName);
|
||||||
|
context.SecretStore.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
// 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.Text;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Configuration.UserSecrets;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
|
{
|
||||||
|
public class SecretsStore
|
||||||
|
{
|
||||||
|
private readonly string _secretsFilePath;
|
||||||
|
private IDictionary<string, string> _secrets;
|
||||||
|
|
||||||
|
public SecretsStore(string userSecretsId, IReporter reporter)
|
||||||
|
{
|
||||||
|
Ensure.NotNull(userSecretsId, nameof(userSecretsId));
|
||||||
|
|
||||||
|
_secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId);
|
||||||
|
|
||||||
|
// workaround bug in configuration
|
||||||
|
var secretDir = Path.GetDirectoryName(_secretsFilePath);
|
||||||
|
Directory.CreateDirectory(secretDir);
|
||||||
|
|
||||||
|
reporter.Verbose(Resources.FormatMessage_Secret_File_Path(_secretsFilePath));
|
||||||
|
_secrets = Load(userSecretsId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string this[string key]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _secrets[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => _secrets.Count;
|
||||||
|
|
||||||
|
public bool ContainsKey(string key) => _secrets.ContainsKey(key);
|
||||||
|
|
||||||
|
public IEnumerable<KeyValuePair<string, string>> AsEnumerable() => _secrets;
|
||||||
|
|
||||||
|
public void Clear() => _secrets.Clear();
|
||||||
|
|
||||||
|
public void Set(string key, string value) => _secrets[key] = value;
|
||||||
|
|
||||||
|
public void Remove(string key)
|
||||||
|
{
|
||||||
|
if (_secrets.ContainsKey(key))
|
||||||
|
{
|
||||||
|
_secrets.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Save()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(_secretsFilePath));
|
||||||
|
|
||||||
|
var contents = new JObject();
|
||||||
|
if (_secrets != null)
|
||||||
|
{
|
||||||
|
foreach (var secret in _secrets.AsEnumerable())
|
||||||
|
{
|
||||||
|
contents[secret.Key] = secret.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(_secretsFilePath, contents.ToString(), Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual IDictionary<string, string> Load(string userSecretsId)
|
||||||
|
{
|
||||||
|
return new ConfigurationBuilder()
|
||||||
|
.AddJsonFile(_secretsFilePath, optional: true)
|
||||||
|
.Build()
|
||||||
|
.AsEnumerable()
|
||||||
|
.Where(i => i.Value != null)
|
||||||
|
.ToDictionary(i => i.Key, i => i.Value, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
|
{
|
||||||
|
internal class SetCommand
|
||||||
|
{
|
||||||
|
public static void Configure(CommandLineApplication command, CommandLineOptions options, IConsole console)
|
||||||
|
{
|
||||||
|
command.Description = "Sets the user secret to the specified value";
|
||||||
|
command.ExtendedHelpText = @"
|
||||||
|
Additional Info:
|
||||||
|
This command will also handle piped input. Piped input is expected to be a valid JSON format.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dotnet user-secrets set ConnStr ""User ID=bob;Password=***""
|
||||||
|
";
|
||||||
|
|
||||||
|
var catCmd = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||||
|
? @"type .\secrets.json"
|
||||||
|
: "cat ./secrets.json";
|
||||||
|
|
||||||
|
command.ExtendedHelpText += $@" {catCmd} | dotnet user-secrets set";
|
||||||
|
|
||||||
|
command.HelpOption();
|
||||||
|
|
||||||
|
var nameArg = command.Argument("[name]", "Name of the secret");
|
||||||
|
var valueArg = command.Argument("[value]", "Value of the secret");
|
||||||
|
|
||||||
|
command.OnExecute(() =>
|
||||||
|
{
|
||||||
|
if (console.IsInputRedirected && nameArg.Value == null)
|
||||||
|
{
|
||||||
|
options.Command = new FromStdInStrategy();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(nameArg.Value))
|
||||||
|
{
|
||||||
|
throw new CommandParsingException(command, Resources.FormatError_MissingArgument("name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueArg.Value == null)
|
||||||
|
{
|
||||||
|
throw new CommandParsingException(command, Resources.FormatError_MissingArgument("value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
options.Command = new ForOneValueStrategy(nameArg.Value, valueArg.Value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FromStdInStrategy : ICommand
|
||||||
|
{
|
||||||
|
public void Execute(CommandContext context)
|
||||||
|
{
|
||||||
|
// parses stdin with the same parser that Microsoft.Extensions.Configuration.Json would use
|
||||||
|
var provider = new ReadableJsonConfigurationProvider();
|
||||||
|
using (var stream = new MemoryStream())
|
||||||
|
{
|
||||||
|
using (var writer = new StreamWriter(stream, Encoding.Unicode, 1024, true))
|
||||||
|
{
|
||||||
|
writer.Write(context.Console.In.ReadToEnd()); // TODO buffer?
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
provider.Load(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var k in provider.CurrentData)
|
||||||
|
{
|
||||||
|
context.SecretStore.Set(k.Key, k.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Reporter.Output(Resources.FormatMessage_Saved_Secrets(provider.CurrentData.Count));
|
||||||
|
|
||||||
|
context.SecretStore.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ForOneValueStrategy : ICommand
|
||||||
|
{
|
||||||
|
private readonly string _keyName;
|
||||||
|
private readonly string _keyValue;
|
||||||
|
|
||||||
|
public ForOneValueStrategy(string keyName, string keyValue)
|
||||||
|
{
|
||||||
|
_keyName = keyName;
|
||||||
|
_keyValue = keyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(CommandContext context)
|
||||||
|
{
|
||||||
|
context.SecretStore.Set(_keyName, _keyValue);
|
||||||
|
context.SecretStore.Save();
|
||||||
|
context.Reporter.Output(Resources.FormatMessage_Saved_Secret(_keyName, _keyValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
// 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 Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.SecretManager.Tools.Internal;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
private readonly IConsole _console;
|
||||||
|
private readonly string _workingDirectory;
|
||||||
|
|
||||||
|
public static int Main(string[] args)
|
||||||
|
{
|
||||||
|
DebugHelper.HandleDebugSwitch(ref args);
|
||||||
|
|
||||||
|
int rc;
|
||||||
|
new Program(PhysicalConsole.Singleton, Directory.GetCurrentDirectory()).TryRun(args, out rc);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Program(IConsole console, string workingDirectory)
|
||||||
|
{
|
||||||
|
_console = console;
|
||||||
|
_workingDirectory = workingDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryRun(string[] args, out int returnCode)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
returnCode = RunInternal(args);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
var reporter = CreateReporter(verbose: true);
|
||||||
|
reporter.Verbose(exception.ToString());
|
||||||
|
reporter.Error(Resources.FormatError_Command_Failed(exception.Message));
|
||||||
|
returnCode = 1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal int RunInternal(params string[] args)
|
||||||
|
{
|
||||||
|
CommandLineOptions options;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
options = CommandLineOptions.Parse(args, _console);
|
||||||
|
}
|
||||||
|
catch (CommandParsingException ex)
|
||||||
|
{
|
||||||
|
CreateReporter(verbose: false).Error(ex.Message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.IsHelp)
|
||||||
|
{
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reporter = CreateReporter(options.IsVerbose);
|
||||||
|
|
||||||
|
string userSecretsId;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
userSecretsId = ResolveId(options, reporter);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is InvalidOperationException || ex is FileNotFoundException)
|
||||||
|
{
|
||||||
|
reporter.Error(ex.Message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var store = new SecretsStore(userSecretsId, reporter);
|
||||||
|
var context = new Internal.CommandContext(store, reporter, _console);
|
||||||
|
options.Command.Execute(context);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReporter CreateReporter(bool verbose)
|
||||||
|
=> new ConsoleReporter(_console, verbose, quiet: false);
|
||||||
|
|
||||||
|
internal string ResolveId(CommandLineOptions options, IReporter reporter)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(options.Id))
|
||||||
|
{
|
||||||
|
return options.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolver = new ProjectIdResolver(reporter, _workingDirectory);
|
||||||
|
return resolver.Resolve(options.Project, options.Configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.Extensions.SecretManager.Tools.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
// <auto-generated />
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools
|
||||||
|
{
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Resources;
|
||||||
|
|
||||||
|
internal static class Resources
|
||||||
|
{
|
||||||
|
private static readonly ResourceManager _resourceManager
|
||||||
|
= new ResourceManager("Microsoft.Extensions.SecretManager.Tools.Resources", typeof(Resources).GetTypeInfo().Assembly);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command failed : {message}
|
||||||
|
/// </summary>
|
||||||
|
internal static string Error_Command_Failed
|
||||||
|
{
|
||||||
|
get { return GetString("Error_Command_Failed"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command failed : {message}
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatError_Command_Failed(object message)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Error_Command_Failed", "message"), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Missing parameter value for '{name}'.
|
||||||
|
/// Use the '--help' flag to see info.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Error_MissingArgument
|
||||||
|
{
|
||||||
|
get { return GetString("Error_MissingArgument"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Missing parameter value for '{name}'.
|
||||||
|
/// Use the '--help' flag to see info.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatError_MissingArgument(object name)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Error_MissingArgument", "name"), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cannot find '{key}' in the secret store.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Error_Missing_Secret
|
||||||
|
{
|
||||||
|
get { return GetString("Error_Missing_Secret"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cannot find '{key}' in the secret store.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatError_Missing_Secret(object key)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Error_Missing_Secret", "key"), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Error_MultipleProjectsFound
|
||||||
|
{
|
||||||
|
get { return GetString("Error_MultipleProjectsFound"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatError_MultipleProjectsFound(object projectPath)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Error_MultipleProjectsFound", "projectPath"), projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No secrets configured for this application.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Error_No_Secrets_Found
|
||||||
|
{
|
||||||
|
get { return GetString("Error_No_Secrets_Found"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No secrets configured for this application.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatError_No_Secrets_Found()
|
||||||
|
{
|
||||||
|
return GetString("Error_No_Secrets_Found");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Error_NoProjectsFound
|
||||||
|
{
|
||||||
|
get { return GetString("Error_NoProjectsFound"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatError_NoProjectsFound(object projectPath)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Error_NoProjectsFound", "projectPath"), projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Could not find the global property 'UserSecretsId' in MSBuild project '{project}'. Ensure this property is set in the project or use the '--id' command line option.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Error_ProjectMissingId
|
||||||
|
{
|
||||||
|
get { return GetString("Error_ProjectMissingId"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Could not find the global property 'UserSecretsId' in MSBuild project '{project}'. Ensure this property is set in the project or use the '--id' command line option.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatError_ProjectMissingId(object project)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectMissingId", "project"), project);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The project file '{path}' does not exist.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Error_ProjectPath_NotFound
|
||||||
|
{
|
||||||
|
get { return GetString("Error_ProjectPath_NotFound"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The project file '{path}' does not exist.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatError_ProjectPath_NotFound(object path)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectPath_NotFound", "path"), path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Could not load the MSBuild project '{project}'.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Error_ProjectFailedToLoad
|
||||||
|
{
|
||||||
|
get { return GetString("Error_ProjectFailedToLoad"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Could not load the MSBuild project '{project}'.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatError_ProjectFailedToLoad(object project)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectFailedToLoad", "project"), project);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Project file path {project}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Message_Project_File_Path
|
||||||
|
{
|
||||||
|
get { return GetString("Message_Project_File_Path"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Project file path {project}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatMessage_Project_File_Path(object project)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Message_Project_File_Path", "project"), project);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Successfully saved {key} = {value} to the secret store.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Message_Saved_Secret
|
||||||
|
{
|
||||||
|
get { return GetString("Message_Saved_Secret"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Successfully saved {key} = {value} to the secret store.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatMessage_Saved_Secret(object key, object value)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secret", "key", "value"), key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Successfully saved {number} secrets to the secret store.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Message_Saved_Secrets
|
||||||
|
{
|
||||||
|
get { return GetString("Message_Saved_Secrets"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Successfully saved {number} secrets to the secret store.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatMessage_Saved_Secrets(object number)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secrets", "number"), number);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Secrets file path {secretsFilePath}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Message_Secret_File_Path
|
||||||
|
{
|
||||||
|
get { return GetString("Message_Secret_File_Path"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Secrets file path {secretsFilePath}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatMessage_Secret_File_Path(object secretsFilePath)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Message_Secret_File_Path", "secretsFilePath"), secretsFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// {key} = {value}
|
||||||
|
/// </summary>
|
||||||
|
internal static string Message_Secret_Value_Format
|
||||||
|
{
|
||||||
|
get { return GetString("Message_Secret_Value_Format"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// {key} = {value}
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatMessage_Secret_Value_Format(object key, object value)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Message_Secret_Value_Format", "key", "value"), key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
|
{
|
||||||
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
||||||
|
System.Diagnostics.Debug.Assert(value != null);
|
||||||
|
|
||||||
|
if (formatterNames != null)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < formatterNames.Length; i++)
|
||||||
|
{
|
||||||
|
value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="Error_Command_Failed" xml:space="preserve">
|
||||||
|
<value>Command failed : {message}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Error_MissingArgument" xml:space="preserve">
|
||||||
|
<value>Missing parameter value for '{name}'.
|
||||||
|
Use the '--help' flag to see info.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Error_Missing_Secret" xml:space="preserve">
|
||||||
|
<value>Cannot find '{key}' in the secret store.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Error_MultipleProjectsFound" xml:space="preserve">
|
||||||
|
<value>Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Error_No_Secrets_Found" xml:space="preserve">
|
||||||
|
<value>No secrets configured for this application.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Error_NoProjectsFound" xml:space="preserve">
|
||||||
|
<value>Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Error_ProjectMissingId" xml:space="preserve">
|
||||||
|
<value>Could not find the global property 'UserSecretsId' in MSBuild project '{project}'. Ensure this property is set in the project or use the '--id' command line option.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Error_ProjectPath_NotFound" xml:space="preserve">
|
||||||
|
<value>The project file '{path}' does not exist.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Error_ProjectFailedToLoad" xml:space="preserve">
|
||||||
|
<value>Could not load the MSBuild project '{project}'.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_Project_File_Path" xml:space="preserve">
|
||||||
|
<value>Project file path {project}.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_Saved_Secret" xml:space="preserve">
|
||||||
|
<value>Successfully saved {key} = {value} to the secret store.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_Saved_Secrets" xml:space="preserve">
|
||||||
|
<value>Successfully saved {number} secrets to the secret store.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_Secret_File_Path" xml:space="preserve">
|
||||||
|
<value>Secrets file path {secretsFilePath}.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_Secret_Value_Format" xml:space="preserve">
|
||||||
|
<value>{key} = {value}</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<Project>
|
||||||
|
<Target Name="_ExtractUserSecretsMetadata">
|
||||||
|
<WriteLinesToFile File="$(_UserSecretsMetadataFile)" Lines="$(UserSecretsId)" />
|
||||||
|
</Target>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<OutputType>exe</OutputType>
|
||||||
|
<Description>Command line tool to manage user secrets for Microsoft.Extensions.Configuration.</Description>
|
||||||
|
<GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute>
|
||||||
|
<RootNamespace>Microsoft.Extensions.SecretManager.Tools</RootNamespace>
|
||||||
|
<PackageTags>configuration;secrets;usersecrets</PackageTags>
|
||||||
|
<PackAsTool>true</PackAsTool>
|
||||||
|
<!-- This is a requirement for Microsoft tool packages only. -->
|
||||||
|
<PackAsToolShimRuntimeIdentifiers>win-x64;win-x86</PackAsToolShimRuntimeIdentifiers>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\..\shared\src\**\*.cs" />
|
||||||
|
<None Include="assets\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.Extensions.CommandLineUtils.Sources" PrivateAssets="All" />
|
||||||
|
<Reference Include="Microsoft.Extensions.Configuration.UserSecrets" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- 3rd party binary -->
|
||||||
|
<SignedPackageFile Include="Newtonsoft.Json.dll" PackagePath="tools/$(TargetFramework)/any/Newtonsoft.Json.dll" Certificate="$(AssemblySigning3rdPartyCertName)" />
|
||||||
|
|
||||||
|
<!-- Exclude files that should already be signed -->
|
||||||
|
<ExcludePackageFileFromSigning Include="Microsoft.Extensions.Configuration.dll" />
|
||||||
|
<ExcludePackageFileFromSigning Include="Microsoft.Extensions.Configuration.Abstractions.dll" />
|
||||||
|
<ExcludePackageFileFromSigning Include="Microsoft.Extensions.Configuration.FileExtensions.dll" />
|
||||||
|
<ExcludePackageFileFromSigning Include="Microsoft.Extensions.Configuration.Json.dll" />
|
||||||
|
<ExcludePackageFileFromSigning Include="Microsoft.Extensions.Configuration.UserSecrets.dll" />
|
||||||
|
<ExcludePackageFileFromSigning Include="Microsoft.Extensions.FileProviders.Abstractions.dll" />
|
||||||
|
<ExcludePackageFileFromSigning Include="Microsoft.Extensions.FileProviders.Physical.dll" />
|
||||||
|
<ExcludePackageFileFromSigning Include="Microsoft.Extensions.FileSystemGlobbing.dll" />
|
||||||
|
<ExcludePackageFileFromSigning Include="Microsoft.Extensions.Primitives.dll" />
|
||||||
|
<ExcludePackageFileFromSigning Include="System.Runtime.CompilerServices.Unsafe.dll" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
// 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;
|
||||||
|
using Microsoft.Extensions.SecretManager.Tools.Internal;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Tests
|
||||||
|
{
|
||||||
|
public class MsBuildProjectFinderTest
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(".csproj")]
|
||||||
|
[InlineData(".vbproj")]
|
||||||
|
[InlineData(".fsproj")]
|
||||||
|
public void FindsSingleProject(string extension)
|
||||||
|
{
|
||||||
|
using (var files = new TemporaryFileProvider())
|
||||||
|
{
|
||||||
|
var filename = "TestProject" + extension;
|
||||||
|
files.Add(filename, "");
|
||||||
|
|
||||||
|
var finder = new MsBuildProjectFinder(files.Root);
|
||||||
|
|
||||||
|
Assert.Equal(Path.Combine(files.Root, filename), finder.FindMsBuildProject(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ThrowsWhenNoFile()
|
||||||
|
{
|
||||||
|
using (var files = new TemporaryFileProvider())
|
||||||
|
{
|
||||||
|
var finder = new MsBuildProjectFinder(files.Root);
|
||||||
|
|
||||||
|
Assert.Throws<FileNotFoundException>(() => finder.FindMsBuildProject(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoesNotMatchXproj()
|
||||||
|
{
|
||||||
|
using (var files = new TemporaryFileProvider())
|
||||||
|
{
|
||||||
|
var finder = new MsBuildProjectFinder(files.Root);
|
||||||
|
files.Add("test.xproj", "");
|
||||||
|
|
||||||
|
Assert.Throws<FileNotFoundException>(() => finder.FindMsBuildProject(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ThrowsWhenMultipleFile()
|
||||||
|
{
|
||||||
|
using (var files = new TemporaryFileProvider())
|
||||||
|
{
|
||||||
|
files.Add("Test1.csproj", "");
|
||||||
|
files.Add("Test2.csproj", "");
|
||||||
|
var finder = new MsBuildProjectFinder(files.Root);
|
||||||
|
|
||||||
|
Assert.Throws<FileNotFoundException>(() => finder.FindMsBuildProject(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ThrowsWhenFileDoesNotExist()
|
||||||
|
{
|
||||||
|
using (var files = new TemporaryFileProvider())
|
||||||
|
{
|
||||||
|
var finder = new MsBuildProjectFinder(files.Root);
|
||||||
|
|
||||||
|
Assert.Throws<FileNotFoundException>(() => finder.FindMsBuildProject("test.csproj"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ThrowsWhenRootDoesNotExist()
|
||||||
|
{
|
||||||
|
var files = new TemporaryFileProvider();
|
||||||
|
var finder = new MsBuildProjectFinder(files.Root);
|
||||||
|
files.Dispose();
|
||||||
|
Assert.Throws<FileNotFoundException>(() => finder.FindMsBuildProject(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,327 @@
|
||||||
|
// 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.Text;
|
||||||
|
using Microsoft.Extensions.Configuration.UserSecrets;
|
||||||
|
using Microsoft.Extensions.Configuration.UserSecrets.Tests;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Tests
|
||||||
|
{
|
||||||
|
public class SecretManagerTests : IClassFixture<UserSecretsTestFixture>
|
||||||
|
{
|
||||||
|
private readonly TestConsole _console;
|
||||||
|
private readonly UserSecretsTestFixture _fixture;
|
||||||
|
private readonly StringBuilder _output = new StringBuilder();
|
||||||
|
|
||||||
|
public SecretManagerTests(UserSecretsTestFixture fixture, ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
_fixture = fixture;
|
||||||
|
|
||||||
|
_console = new TestConsole(output)
|
||||||
|
{
|
||||||
|
Error = new StringWriter(_output),
|
||||||
|
Out = new StringWriter(_output),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Program CreateProgram()
|
||||||
|
{
|
||||||
|
return new Program(_console, Directory.GetCurrentDirectory());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null)]
|
||||||
|
[InlineData("")]
|
||||||
|
public void Error_MissingId(string id)
|
||||||
|
{
|
||||||
|
var project = Path.Combine(_fixture.CreateProject(id), "TestProject.csproj");
|
||||||
|
var secretManager = CreateProgram();
|
||||||
|
|
||||||
|
secretManager.RunInternal("list", "-p", project);
|
||||||
|
Assert.Contains(Resources.FormatError_ProjectMissingId(project), _output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Error_InvalidProjectFormat()
|
||||||
|
{
|
||||||
|
var project = Path.Combine(_fixture.CreateProject("<"), "TestProject.csproj");
|
||||||
|
var secretManager = CreateProgram();
|
||||||
|
|
||||||
|
secretManager.RunInternal("list", "-p", project);
|
||||||
|
Assert.Contains(Resources.FormatError_ProjectFailedToLoad(project), _output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Error_Project_DoesNotExist()
|
||||||
|
{
|
||||||
|
var projectPath = Path.Combine(_fixture.GetTempSecretProject(), "does_not_exist", "TestProject.csproj");
|
||||||
|
var secretManager = CreateProgram();
|
||||||
|
|
||||||
|
secretManager.RunInternal("list", "--project", projectPath);
|
||||||
|
Assert.Contains(Resources.FormatError_ProjectPath_NotFound(projectPath), _output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SupportsRelativePaths()
|
||||||
|
{
|
||||||
|
var projectPath = _fixture.GetTempSecretProject();
|
||||||
|
var cwd = Path.Combine(projectPath, "nested1");
|
||||||
|
Directory.CreateDirectory(cwd);
|
||||||
|
var secretManager = new Program(_console, cwd);
|
||||||
|
|
||||||
|
secretManager.RunInternal("list", "-p", ".." + Path.DirectorySeparatorChar, "--verbose");
|
||||||
|
|
||||||
|
Assert.Contains(Resources.FormatMessage_Project_File_Path(Path.Combine(cwd, "..", "TestProject.csproj")), _output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void SetSecrets(bool fromCurrentDirectory)
|
||||||
|
{
|
||||||
|
var secrets = new KeyValuePair<string, string>[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>("key1", Guid.NewGuid().ToString()),
|
||||||
|
new KeyValuePair<string, string>("Facebook:AppId", Guid.NewGuid().ToString()),
|
||||||
|
new KeyValuePair<string, string>(@"key-@\/.~123!#$%^&*())-+==", @"key-@\/.~123!#$%^&*())-+=="),
|
||||||
|
new KeyValuePair<string, string>("key2", string.Empty)
|
||||||
|
};
|
||||||
|
|
||||||
|
var projectPath = _fixture.GetTempSecretProject();
|
||||||
|
var dir = fromCurrentDirectory
|
||||||
|
? projectPath
|
||||||
|
: Path.GetTempPath();
|
||||||
|
var secretManager = new Program(_console, dir);
|
||||||
|
|
||||||
|
foreach (var secret in secrets)
|
||||||
|
{
|
||||||
|
var parameters = fromCurrentDirectory ?
|
||||||
|
new string[] { "set", secret.Key, secret.Value } :
|
||||||
|
new string[] { "set", secret.Key, secret.Value, "-p", projectPath };
|
||||||
|
secretManager.RunInternal(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var keyValue in secrets)
|
||||||
|
{
|
||||||
|
Assert.Contains(
|
||||||
|
string.Format("Successfully saved {0} = {1} to the secret store.", keyValue.Key, keyValue.Value),
|
||||||
|
_output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
_output.Clear();
|
||||||
|
var args = fromCurrentDirectory
|
||||||
|
? new string[] { "list" }
|
||||||
|
: new string[] { "list", "-p", projectPath };
|
||||||
|
secretManager.RunInternal(args);
|
||||||
|
foreach (var keyValue in secrets)
|
||||||
|
{
|
||||||
|
Assert.Contains(
|
||||||
|
string.Format("{0} = {1}", keyValue.Key, keyValue.Value),
|
||||||
|
_output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove secrets.
|
||||||
|
_output.Clear();
|
||||||
|
foreach (var secret in secrets)
|
||||||
|
{
|
||||||
|
var parameters = fromCurrentDirectory ?
|
||||||
|
new string[] { "remove", secret.Key } :
|
||||||
|
new string[] { "remove", secret.Key, "-p", projectPath };
|
||||||
|
secretManager.RunInternal(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify secrets are removed.
|
||||||
|
_output.Clear();
|
||||||
|
args = fromCurrentDirectory
|
||||||
|
? new string[] { "list" }
|
||||||
|
: new string[] { "list", "-p", projectPath };
|
||||||
|
secretManager.RunInternal(args);
|
||||||
|
Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetSecret_Update_Existing_Secret()
|
||||||
|
{
|
||||||
|
var projectPath = _fixture.GetTempSecretProject();
|
||||||
|
var secretManager = CreateProgram();
|
||||||
|
|
||||||
|
secretManager.RunInternal("set", "secret1", "value1", "-p", projectPath);
|
||||||
|
Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _output.ToString());
|
||||||
|
secretManager.RunInternal("set", "secret1", "value2", "-p", projectPath);
|
||||||
|
Assert.Contains("Successfully saved secret1 = value2 to the secret store.", _output.ToString());
|
||||||
|
|
||||||
|
_output.Clear();
|
||||||
|
|
||||||
|
secretManager.RunInternal("list", "-p", projectPath);
|
||||||
|
Assert.Contains("secret1 = value2", _output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetSecret_With_Verbose_Flag()
|
||||||
|
{
|
||||||
|
string secretId;
|
||||||
|
var projectPath = _fixture.GetTempSecretProject(out secretId);
|
||||||
|
var secretManager = CreateProgram();
|
||||||
|
|
||||||
|
secretManager.RunInternal("-v", "set", "secret1", "value1", "-p", projectPath);
|
||||||
|
Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _output.ToString());
|
||||||
|
Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _output.ToString());
|
||||||
|
Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _output.ToString());
|
||||||
|
_output.Clear();
|
||||||
|
|
||||||
|
secretManager.RunInternal("-v", "list", "-p", projectPath);
|
||||||
|
|
||||||
|
Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _output.ToString());
|
||||||
|
Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _output.ToString());
|
||||||
|
Assert.Contains("secret1 = value1", _output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Remove_Non_Existing_Secret()
|
||||||
|
{
|
||||||
|
var projectPath = _fixture.GetTempSecretProject();
|
||||||
|
var secretManager = CreateProgram();
|
||||||
|
secretManager.RunInternal("remove", "secret1", "-p", projectPath);
|
||||||
|
Assert.Contains("Cannot find 'secret1' in the secret store.", _output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Remove_Is_Case_Insensitive()
|
||||||
|
{
|
||||||
|
var projectPath = _fixture.GetTempSecretProject();
|
||||||
|
var secretManager = CreateProgram();
|
||||||
|
secretManager.RunInternal("set", "SeCreT1", "value", "-p", projectPath);
|
||||||
|
secretManager.RunInternal("list", "-p", projectPath);
|
||||||
|
Assert.Contains("SeCreT1 = value", _output.ToString());
|
||||||
|
secretManager.RunInternal("remove", "secret1", "-p", projectPath);
|
||||||
|
|
||||||
|
_output.Clear();
|
||||||
|
secretManager.RunInternal("list", "-p", projectPath);
|
||||||
|
|
||||||
|
Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void List_Flattens_Nested_Objects()
|
||||||
|
{
|
||||||
|
string secretId;
|
||||||
|
var projectPath = _fixture.GetTempSecretProject(out secretId);
|
||||||
|
var secretsFile = PathHelper.GetSecretsPathFromSecretsId(secretId);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
|
||||||
|
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
|
||||||
|
var secretManager = CreateProgram();
|
||||||
|
secretManager.RunInternal("list", "-p", projectPath);
|
||||||
|
Assert.Contains("AzureAd:ClientSecret = abcd郩˙î", _output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void List_Json()
|
||||||
|
{
|
||||||
|
string id;
|
||||||
|
var projectPath = _fixture.GetTempSecretProject(out id);
|
||||||
|
var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
|
||||||
|
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
|
||||||
|
var secretManager = new Program(_console, Path.GetDirectoryName(projectPath));
|
||||||
|
secretManager.RunInternal("list", "--id", id, "--json");
|
||||||
|
var stdout = _output.ToString();
|
||||||
|
Assert.Contains("//BEGIN", stdout);
|
||||||
|
Assert.Contains(@"""AzureAd:ClientSecret"": ""abcd郩˙î""", stdout);
|
||||||
|
Assert.Contains("//END", stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Set_Flattens_Nested_Objects()
|
||||||
|
{
|
||||||
|
string secretId;
|
||||||
|
var projectPath = _fixture.GetTempSecretProject(out secretId);
|
||||||
|
var secretsFile = PathHelper.GetSecretsPathFromSecretsId(secretId);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
|
||||||
|
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
|
||||||
|
var secretManager = CreateProgram();
|
||||||
|
secretManager.RunInternal("set", "AzureAd:ClientSecret", "¡™£¢∞", "-p", projectPath);
|
||||||
|
secretManager.RunInternal("list", "-p", projectPath);
|
||||||
|
|
||||||
|
Assert.Contains("AzureAd:ClientSecret = ¡™£¢∞", _output.ToString());
|
||||||
|
var fileContents = File.ReadAllText(secretsFile, Encoding.UTF8);
|
||||||
|
Assert.Equal(@"{
|
||||||
|
""AzureAd:ClientSecret"": ""¡™£¢∞""
|
||||||
|
}",
|
||||||
|
fileContents, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void List_Empty_Secrets_File()
|
||||||
|
{
|
||||||
|
var projectPath = _fixture.GetTempSecretProject();
|
||||||
|
var secretManager = CreateProgram();
|
||||||
|
secretManager.RunInternal("list", "-p", projectPath);
|
||||||
|
Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void Clear_Secrets(bool fromCurrentDirectory)
|
||||||
|
{
|
||||||
|
var projectPath = _fixture.GetTempSecretProject();
|
||||||
|
|
||||||
|
var dir = fromCurrentDirectory
|
||||||
|
? projectPath
|
||||||
|
: Path.GetTempPath();
|
||||||
|
|
||||||
|
var secretManager = new Program(_console, dir);
|
||||||
|
|
||||||
|
var secrets = new KeyValuePair<string, string>[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>("key1", Guid.NewGuid().ToString()),
|
||||||
|
new KeyValuePair<string, string>("Facebook:AppId", Guid.NewGuid().ToString()),
|
||||||
|
new KeyValuePair<string, string>(@"key-@\/.~123!#$%^&*())-+==", @"key-@\/.~123!#$%^&*())-+=="),
|
||||||
|
new KeyValuePair<string, string>("key2", string.Empty)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var secret in secrets)
|
||||||
|
{
|
||||||
|
var parameters = fromCurrentDirectory ?
|
||||||
|
new string[] { "set", secret.Key, secret.Value } :
|
||||||
|
new string[] { "set", secret.Key, secret.Value, "-p", projectPath };
|
||||||
|
secretManager.RunInternal(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var keyValue in secrets)
|
||||||
|
{
|
||||||
|
Assert.Contains(
|
||||||
|
string.Format("Successfully saved {0} = {1} to the secret store.", keyValue.Key, keyValue.Value),
|
||||||
|
_output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify secrets are persisted.
|
||||||
|
_output.Clear();
|
||||||
|
var args = fromCurrentDirectory ?
|
||||||
|
new string[] { "list" } :
|
||||||
|
new string[] { "list", "-p", projectPath };
|
||||||
|
secretManager.RunInternal(args);
|
||||||
|
foreach (var keyValue in secrets)
|
||||||
|
{
|
||||||
|
Assert.Contains(
|
||||||
|
string.Format("{0} = {1}", keyValue.Key, keyValue.Value),
|
||||||
|
_output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear secrets.
|
||||||
|
_output.Clear();
|
||||||
|
args = fromCurrentDirectory ? new string[] { "clear" } : new string[] { "clear", "-p", projectPath };
|
||||||
|
secretManager.RunInternal(args);
|
||||||
|
|
||||||
|
args = fromCurrentDirectory ? new string[] { "list" } : new string[] { "list", "-p", projectPath };
|
||||||
|
secretManager.RunInternal(args);
|
||||||
|
Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.IO;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.Extensions.SecretManager.Tools.Internal;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Tests
|
||||||
|
{
|
||||||
|
|
||||||
|
public class SetCommandTest
|
||||||
|
{
|
||||||
|
private readonly ITestOutputHelper _output;
|
||||||
|
|
||||||
|
public SetCommandTest(ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
_output = output;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetsFromPipedInput()
|
||||||
|
{
|
||||||
|
var input = @"
|
||||||
|
{
|
||||||
|
""Key1"": ""str value"",
|
||||||
|
""Key2"": 1234,
|
||||||
|
""Key3"": false
|
||||||
|
}";
|
||||||
|
var testConsole = new TestConsole(_output)
|
||||||
|
{
|
||||||
|
IsInputRedirected = true,
|
||||||
|
In = new StringReader(input)
|
||||||
|
};
|
||||||
|
var secretStore = new TestSecretsStore(_output);
|
||||||
|
var command = new SetCommand.FromStdInStrategy();
|
||||||
|
|
||||||
|
command.Execute(new CommandContext(secretStore, new TestReporter(_output), testConsole));
|
||||||
|
|
||||||
|
Assert.Equal(3, secretStore.Count);
|
||||||
|
Assert.Equal("str value", secretStore["Key1"]);
|
||||||
|
Assert.Equal("1234", secretStore["Key2"]);
|
||||||
|
Assert.Equal("False", secretStore["Key3"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParsesNestedObjects()
|
||||||
|
{
|
||||||
|
var input = @"
|
||||||
|
{
|
||||||
|
""Key1"": {
|
||||||
|
""nested"" : ""value""
|
||||||
|
},
|
||||||
|
""array"": [ 1, 2 ]
|
||||||
|
}";
|
||||||
|
|
||||||
|
var testConsole = new TestConsole(_output)
|
||||||
|
{
|
||||||
|
IsInputRedirected = true,
|
||||||
|
In = new StringReader(input)
|
||||||
|
};
|
||||||
|
var secretStore = new TestSecretsStore(_output);
|
||||||
|
var command = new SetCommand.FromStdInStrategy();
|
||||||
|
|
||||||
|
command.Execute(new CommandContext(secretStore, new TestReporter(_output), testConsole));
|
||||||
|
|
||||||
|
Assert.Equal(3, secretStore.Count);
|
||||||
|
Assert.True(secretStore.ContainsKey("Key1:nested"));
|
||||||
|
Assert.Equal("value", secretStore["Key1:nested"]);
|
||||||
|
Assert.Equal("1", secretStore["array:0"]);
|
||||||
|
Assert.Equal("2", secretStore["array:1"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnlyPipesInIfNoArgs()
|
||||||
|
{
|
||||||
|
var testConsole = new TestConsole(_output)
|
||||||
|
{
|
||||||
|
IsInputRedirected = true,
|
||||||
|
In = new StringReader("")
|
||||||
|
};
|
||||||
|
var options = CommandLineOptions.Parse(new [] { "set", "key", "value" }, testConsole);
|
||||||
|
Assert.IsType<SetCommand.ForOneValueStrategy>(options.Command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestSecretsStore : SecretsStore
|
||||||
|
{
|
||||||
|
public TestSecretsStore(ITestOutputHelper output)
|
||||||
|
: base("xyz", new TestReporter(output))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IDictionary<string, string> Load(string userSecretsId)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Save()
|
||||||
|
{
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
// 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.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Tests
|
||||||
|
{
|
||||||
|
internal class TemporaryFileProvider : IDisposable
|
||||||
|
{
|
||||||
|
public TemporaryFileProvider()
|
||||||
|
{
|
||||||
|
Root = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "tmpfiles", Guid.NewGuid().ToString())).FullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Root { get; }
|
||||||
|
|
||||||
|
public void Add(string filename, string contents)
|
||||||
|
{
|
||||||
|
File.WriteAllText(Path.Combine(Root, filename), contents, Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Directory.Delete(Root, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Configuration.UserSecrets.Tests
|
||||||
|
{
|
||||||
|
public class UserSecretsTestFixture : IDisposable
|
||||||
|
{
|
||||||
|
private Stack<Action> _disposables = new Stack<Action>();
|
||||||
|
|
||||||
|
public const string TestSecretsId = "b918174fa80346bbb7f4a386729c0eff";
|
||||||
|
|
||||||
|
public UserSecretsTestFixture()
|
||||||
|
{
|
||||||
|
_disposables.Push(() => TryDelete(Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(TestSecretsId))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
while (_disposables.Count > 0)
|
||||||
|
{
|
||||||
|
_disposables.Pop()?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetTempSecretProject()
|
||||||
|
{
|
||||||
|
string userSecretsId;
|
||||||
|
return GetTempSecretProject(out userSecretsId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private const string ProjectTemplate = @"<Project ToolsVersion=""15.0"" Sdk=""Microsoft.NET.Sdk"">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFrameworks>netcoreapp2.1</TargetFrameworks>
|
||||||
|
{0}
|
||||||
|
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include=""**\*.cs"" Exclude=""Excluded.cs;$(DefaultItemExcludes)"" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>";
|
||||||
|
|
||||||
|
public string GetTempSecretProject(out string userSecretsId)
|
||||||
|
{
|
||||||
|
userSecretsId = Guid.NewGuid().ToString();
|
||||||
|
return CreateProject(userSecretsId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CreateProject(string userSecretsId)
|
||||||
|
{
|
||||||
|
var projectPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "usersecretstest", Guid.NewGuid().ToString()));
|
||||||
|
var prop = string.IsNullOrEmpty(userSecretsId)
|
||||||
|
? string.Empty
|
||||||
|
: $"<UserSecretsId>{userSecretsId}</UserSecretsId>";
|
||||||
|
|
||||||
|
File.WriteAllText(
|
||||||
|
Path.Combine(projectPath.FullName, "TestProject.csproj"),
|
||||||
|
string.Format(ProjectTemplate, prop));
|
||||||
|
|
||||||
|
var id = userSecretsId;
|
||||||
|
_disposables.Push(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// may throw if id is bad
|
||||||
|
var secretsDir = Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(id));
|
||||||
|
TryDelete(secretsDir);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
});
|
||||||
|
_disposables.Push(() => TryDelete(projectPath.FullName));
|
||||||
|
|
||||||
|
return projectPath.FullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDelete(string directory)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.Delete(directory, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Ignore failures.
|
||||||
|
Console.WriteLine("Failed to delete " + directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<AssemblyName>Microsoft.Extensions.SecretManager.Tools.Tests</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\..\shared\test\**\*.cs" />
|
||||||
|
<Content Include="..\src\assets\SecretManager.targets" Link="assets\SecretManager.targets" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\src\dotnet-user-secrets.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.Extensions.Configuration.UserSecrets" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
dotnet-watch
|
||||||
|
============
|
||||||
|
`dotnet-watch` is a file watcher for `dotnet` that restarts the specified application when changes in the source code are detected.
|
||||||
|
|
||||||
|
### How To Use
|
||||||
|
|
||||||
|
The command must be executed in the directory that contains the project to be watched.
|
||||||
|
|
||||||
|
Usage: dotnet watch [options] [[--] <args>...]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-?|-h|--help Show help information
|
||||||
|
-q|--quiet Suppresses all output except warnings and errors
|
||||||
|
-v|--verbose Show verbose output
|
||||||
|
|
||||||
|
Add `watch` after `dotnet` and before the command arguments that you want to run:
|
||||||
|
|
||||||
|
| What you want to run | Dotnet watch command |
|
||||||
|
| ---------------------------------------------- | -------------------------------------------------------- |
|
||||||
|
| dotnet run | dotnet **watch** run |
|
||||||
|
| dotnet run --arg1 value1 | dotnet **watch** run --arg1 value |
|
||||||
|
| dotnet run --framework net451 -- --arg1 value1 | dotnet **watch** run --framework net451 -- --arg1 value1 |
|
||||||
|
| dotnet test | dotnet **watch** test |
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
Some configuration options can be passed to `dotnet watch` through environment variables. The available variables are:
|
||||||
|
|
||||||
|
| Variable | Effect |
|
||||||
|
| ---------------------------------------------- | -------------------------------------------------------- |
|
||||||
|
| DOTNET_USE_POLLING_FILE_WATCHER | If set to "1" or "true", `dotnet watch` will use a polling file watcher instead of CoreFx's `FileSystemWatcher`. Used when watching files on network shares or Docker mounted volumes. |
|
||||||
|
|
||||||
|
### MSBuild
|
||||||
|
|
||||||
|
dotnet-watch can be configured from the MSBuild project file being watched.
|
||||||
|
|
||||||
|
**Watch items**
|
||||||
|
|
||||||
|
dotnet-watch will watch all items in the **Watch** item group.
|
||||||
|
By default, this group inclues all items in **Compile** and **EmbeddedResource**.
|
||||||
|
|
||||||
|
More items can be added to watch in a project file by adding items to 'Watch'.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- extends watching group to include *.js files -->
|
||||||
|
<Watch Include="**\*.js" Exclude="node_modules\**\*.js;$(DefaultExcludes)" />
|
||||||
|
</ItemGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
dotnet-watch will ignore Compile and EmbeddedResource items with the `Watch="false"` attribute.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- exclude Generated.cs from dotnet-watch -->
|
||||||
|
<Compile Update="Generated.cs" Watch="false" />
|
||||||
|
<!-- exclude Strings.resx from dotnet-watch -->
|
||||||
|
<EmbeddedResource Update="Strings.resx" Watch="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Project References**
|
||||||
|
|
||||||
|
By default, dotnet-watch will scan the entire graph of project references and watch all files within those projects.
|
||||||
|
|
||||||
|
dotnet-watch will ignore project references with the `Watch="false"` attribute.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ClassLibrary1\ClassLibrary1.csproj" Watch="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
**Advanced configuration**
|
||||||
|
|
||||||
|
dotnet-watch performs a design-time build to find items to watch.
|
||||||
|
When this build is run, dotnet-watch will set the property `DotNetWatchBuild=true`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ItemGroup Condition="'$(DotNetWatchBuild)'=='true'">
|
||||||
|
<!-- only included in the project when dotnet-watch is running -->
|
||||||
|
</ItemGroup>
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
// 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.Reflection;
|
||||||
|
using Microsoft.DotNet.Watcher.Tools;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher
|
||||||
|
{
|
||||||
|
internal class CommandLineOptions
|
||||||
|
{
|
||||||
|
public string Project { get; private set; }
|
||||||
|
public bool IsHelp { get; private set; }
|
||||||
|
public bool IsQuiet { get; private set; }
|
||||||
|
public bool IsVerbose { get; private set; }
|
||||||
|
public IList<string> RemainingArguments { get; private set; }
|
||||||
|
public bool ListFiles { get; private set; }
|
||||||
|
|
||||||
|
public static bool IsPollingEnabled
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var envVar = Environment.GetEnvironmentVariable("DOTNET_USE_POLLING_FILE_WATCHER");
|
||||||
|
return envVar != null &&
|
||||||
|
(envVar.Equals("1", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
envVar.Equals("true", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CommandLineOptions Parse(string[] args, IConsole console)
|
||||||
|
{
|
||||||
|
Ensure.NotNull(args, nameof(args));
|
||||||
|
Ensure.NotNull(console, nameof(console));
|
||||||
|
|
||||||
|
var app = new CommandLineApplication(throwOnUnexpectedArg: false)
|
||||||
|
{
|
||||||
|
Name = "dotnet watch",
|
||||||
|
FullName = "Microsoft DotNet File Watcher",
|
||||||
|
Out = console.Out,
|
||||||
|
Error = console.Error,
|
||||||
|
AllowArgumentSeparator = true,
|
||||||
|
ExtendedHelpText = @"
|
||||||
|
Environment variables:
|
||||||
|
|
||||||
|
DOTNET_USE_POLLING_FILE_WATCHER
|
||||||
|
When set to '1' or 'true', dotnet-watch will poll the file system for
|
||||||
|
changes. This is required for some file systems, such as network shares,
|
||||||
|
Docker mounted volumes, and other virtual file systems.
|
||||||
|
|
||||||
|
DOTNET_WATCH
|
||||||
|
dotnet-watch sets this variable to '1' on all child processes launched.
|
||||||
|
|
||||||
|
Remarks:
|
||||||
|
The special option '--' is used to delimit the end of the options and
|
||||||
|
the beginning of arguments that will be passed to the child dotnet process.
|
||||||
|
Its use is optional. When the special option '--' is not used,
|
||||||
|
dotnet-watch will use the first unrecognized argument as the beginning
|
||||||
|
of all arguments passed into the child dotnet process.
|
||||||
|
|
||||||
|
For example: dotnet watch -- --verbose run
|
||||||
|
|
||||||
|
Even though '--verbose' is an option dotnet-watch supports, the use of '--'
|
||||||
|
indicates that '--verbose' should be treated instead as an argument for
|
||||||
|
dotnet-run.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dotnet watch run
|
||||||
|
dotnet watch test
|
||||||
|
"
|
||||||
|
};
|
||||||
|
|
||||||
|
app.HelpOption("-?|-h|--help");
|
||||||
|
// TODO multiple shouldn't be too hard to support
|
||||||
|
var optProjects = app.Option("-p|--project <PROJECT>", "The project to watch",
|
||||||
|
CommandOptionType.SingleValue);
|
||||||
|
|
||||||
|
var optQuiet = app.Option("-q|--quiet", "Suppresses all output except warnings and errors",
|
||||||
|
CommandOptionType.NoValue);
|
||||||
|
var optVerbose = app.VerboseOption();
|
||||||
|
|
||||||
|
var optList = app.Option("--list", "Lists all discovered files without starting the watcher",
|
||||||
|
CommandOptionType.NoValue);
|
||||||
|
|
||||||
|
app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly);
|
||||||
|
|
||||||
|
if (app.Execute(args) != 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optQuiet.HasValue() && optVerbose.HasValue())
|
||||||
|
{
|
||||||
|
throw new CommandParsingException(app, Resources.Error_QuietAndVerboseSpecified);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.RemainingArguments.Count == 0
|
||||||
|
&& !app.IsShowingInformation
|
||||||
|
&& !optList.HasValue())
|
||||||
|
{
|
||||||
|
app.ShowHelp();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CommandLineOptions
|
||||||
|
{
|
||||||
|
Project = optProjects.Value(),
|
||||||
|
IsQuiet = optQuiet.HasValue(),
|
||||||
|
IsVerbose = optVerbose.HasValue(),
|
||||||
|
RemainingArguments = app.RemainingArguments,
|
||||||
|
IsHelp = app.IsShowingInformation,
|
||||||
|
ListFiles = optList.HasValue(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
// 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.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.DotNet.Watcher.Internal;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher
|
||||||
|
{
|
||||||
|
public class DotNetWatcher
|
||||||
|
{
|
||||||
|
private readonly IReporter _reporter;
|
||||||
|
private readonly ProcessRunner _processRunner;
|
||||||
|
|
||||||
|
public DotNetWatcher(IReporter reporter)
|
||||||
|
{
|
||||||
|
Ensure.NotNull(reporter, nameof(reporter));
|
||||||
|
|
||||||
|
_reporter = reporter;
|
||||||
|
_processRunner = new ProcessRunner(reporter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task WatchAsync(ProcessSpec processSpec, IFileSetFactory fileSetFactory,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Ensure.NotNull(processSpec, nameof(processSpec));
|
||||||
|
|
||||||
|
var cancelledTaskSource = new TaskCompletionSource<object>();
|
||||||
|
cancellationToken.Register(state => ((TaskCompletionSource<object>) state).TrySetResult(null),
|
||||||
|
cancelledTaskSource);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var fileSet = await fileSetFactory.CreateAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (fileSet == null)
|
||||||
|
{
|
||||||
|
_reporter.Error("Failed to find a list of files to watch");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var currentRunCancellationSource = new CancellationTokenSource())
|
||||||
|
using (var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(
|
||||||
|
cancellationToken,
|
||||||
|
currentRunCancellationSource.Token))
|
||||||
|
using (var fileSetWatcher = new FileSetWatcher(fileSet, _reporter))
|
||||||
|
{
|
||||||
|
var fileSetTask = fileSetWatcher.GetChangedFileAsync(combinedCancellationSource.Token);
|
||||||
|
var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token);
|
||||||
|
|
||||||
|
var args = ArgumentEscaper.EscapeAndConcatenate(processSpec.Arguments);
|
||||||
|
_reporter.Verbose($"Running {processSpec.ShortDisplayName()} with the following arguments: {args}");
|
||||||
|
|
||||||
|
_reporter.Output("Started");
|
||||||
|
|
||||||
|
var finishedTask = await Task.WhenAny(processTask, fileSetTask, cancelledTaskSource.Task);
|
||||||
|
|
||||||
|
// Regardless of the which task finished first, make sure everything is cancelled
|
||||||
|
// and wait for dotnet to exit. We don't want orphan processes
|
||||||
|
currentRunCancellationSource.Cancel();
|
||||||
|
|
||||||
|
await Task.WhenAll(processTask, fileSetTask);
|
||||||
|
|
||||||
|
if (processTask.Result == 0)
|
||||||
|
{
|
||||||
|
_reporter.Output("Exited");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_reporter.Error($"Exited with error code {processTask.Result}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finishedTask == cancelledTaskSource.Task || cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finishedTask == processTask)
|
||||||
|
{
|
||||||
|
_reporter.Warn("Waiting for a file to change before restarting dotnet...");
|
||||||
|
|
||||||
|
// Now wait for a file to change before restarting process
|
||||||
|
await fileSetWatcher.GetChangedFileAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(fileSetTask.Result))
|
||||||
|
{
|
||||||
|
_reporter.Output($"File changed: {fileSetTask.Result}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher
|
||||||
|
{
|
||||||
|
public interface IFileSet : IEnumerable<string>
|
||||||
|
{
|
||||||
|
bool Contains(string filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
// 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.DotNet.Watcher
|
||||||
|
{
|
||||||
|
public interface IFileSetFactory
|
||||||
|
{
|
||||||
|
Task<IFileSet> CreateAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
public class FileSet : IFileSet
|
||||||
|
{
|
||||||
|
private readonly HashSet<string> _files;
|
||||||
|
|
||||||
|
public FileSet(IEnumerable<string> files)
|
||||||
|
{
|
||||||
|
_files = new HashSet<string>(files, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(string filePath) => _files.Contains(filePath);
|
||||||
|
|
||||||
|
public int Count => _files.Count;
|
||||||
|
|
||||||
|
public IEnumerator<string> GetEnumerator() => _files.GetEnumerator();
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => _files.GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
public class FileSetWatcher : IDisposable
|
||||||
|
{
|
||||||
|
private readonly FileWatcher _fileWatcher;
|
||||||
|
private readonly IFileSet _fileSet;
|
||||||
|
|
||||||
|
public FileSetWatcher(IFileSet fileSet, IReporter reporter)
|
||||||
|
{
|
||||||
|
Ensure.NotNull(fileSet, nameof(fileSet));
|
||||||
|
|
||||||
|
_fileSet = fileSet;
|
||||||
|
_fileWatcher = new FileWatcher(reporter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetChangedFileAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
foreach (var file in _fileSet)
|
||||||
|
{
|
||||||
|
_fileWatcher.WatchDirectory(Path.GetDirectoryName(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<string>();
|
||||||
|
cancellationToken.Register(() => tcs.TrySetResult(null));
|
||||||
|
|
||||||
|
Action<string> callback = path =>
|
||||||
|
{
|
||||||
|
if (_fileSet.Contains(path))
|
||||||
|
{
|
||||||
|
tcs.TrySetResult(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_fileWatcher.OnFileChange += callback;
|
||||||
|
var changedFile = await tcs.Task;
|
||||||
|
_fileWatcher.OnFileChange -= callback;
|
||||||
|
|
||||||
|
return changedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_fileWatcher.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
// 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.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
public class FileWatcher
|
||||||
|
{
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
private readonly IDictionary<string, IFileSystemWatcher> _watchers;
|
||||||
|
private readonly IReporter _reporter;
|
||||||
|
|
||||||
|
public FileWatcher()
|
||||||
|
: this(NullReporter.Singleton)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public FileWatcher(IReporter reporter)
|
||||||
|
{
|
||||||
|
_reporter = reporter ?? throw new ArgumentNullException(nameof(reporter));
|
||||||
|
_watchers = new Dictionary<string, IFileSystemWatcher>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public event Action<string> OnFileChange;
|
||||||
|
|
||||||
|
public void WatchDirectory(string directory)
|
||||||
|
{
|
||||||
|
EnsureNotDisposed();
|
||||||
|
AddDirectoryWatcher(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
foreach (var watcher in _watchers)
|
||||||
|
{
|
||||||
|
watcher.Value.OnFileChange -= WatcherChangedHandler;
|
||||||
|
watcher.Value.OnError -= WatcherErrorHandler;
|
||||||
|
watcher.Value.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_watchers.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddDirectoryWatcher(string directory)
|
||||||
|
{
|
||||||
|
directory = EnsureTrailingSlash(directory);
|
||||||
|
|
||||||
|
var alreadyWatched = _watchers
|
||||||
|
.Where(d => directory.StartsWith(d.Key))
|
||||||
|
.Any();
|
||||||
|
|
||||||
|
if (alreadyWatched)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var redundantWatchers = _watchers
|
||||||
|
.Where(d => d.Key.StartsWith(directory))
|
||||||
|
.Select(d => d.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (redundantWatchers.Any())
|
||||||
|
{
|
||||||
|
foreach (var watcher in redundantWatchers)
|
||||||
|
{
|
||||||
|
DisposeWatcher(watcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newWatcher = FileWatcherFactory.CreateWatcher(directory);
|
||||||
|
newWatcher.OnFileChange += WatcherChangedHandler;
|
||||||
|
newWatcher.OnError += WatcherErrorHandler;
|
||||||
|
newWatcher.EnableRaisingEvents = true;
|
||||||
|
|
||||||
|
_watchers.Add(directory, newWatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WatcherErrorHandler(object sender, Exception error)
|
||||||
|
{
|
||||||
|
if (sender is IFileSystemWatcher watcher)
|
||||||
|
{
|
||||||
|
_reporter.Warn($"The file watcher observing '{watcher.BasePath}' encountered an error: {error.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WatcherChangedHandler(object sender, string changedPath)
|
||||||
|
{
|
||||||
|
NotifyChange(changedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NotifyChange(string path)
|
||||||
|
{
|
||||||
|
if (OnFileChange != null)
|
||||||
|
{
|
||||||
|
OnFileChange(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisposeWatcher(string directory)
|
||||||
|
{
|
||||||
|
var watcher = _watchers[directory];
|
||||||
|
_watchers.Remove(directory);
|
||||||
|
|
||||||
|
watcher.EnableRaisingEvents = false;
|
||||||
|
|
||||||
|
watcher.OnFileChange -= WatcherChangedHandler;
|
||||||
|
watcher.OnError -= WatcherErrorHandler;
|
||||||
|
|
||||||
|
watcher.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureNotDisposed()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
throw new ObjectDisposedException(nameof(FileWatcher));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string EnsureTrailingSlash(string path)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(path) &&
|
||||||
|
path[path.Length - 1] != Path.DirectorySeparatorChar)
|
||||||
|
{
|
||||||
|
return path + Path.DirectorySeparatorChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
// 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.ComponentModel;
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
internal class DotnetFileWatcher : IFileSystemWatcher
|
||||||
|
{
|
||||||
|
private readonly Func<string, FileSystemWatcher> _watcherFactory;
|
||||||
|
|
||||||
|
private FileSystemWatcher _fileSystemWatcher;
|
||||||
|
|
||||||
|
private readonly object _createLock = new object();
|
||||||
|
|
||||||
|
public DotnetFileWatcher(string watchedDirectory)
|
||||||
|
: this(watchedDirectory, DefaultWatcherFactory)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal DotnetFileWatcher(string watchedDirectory, Func<string, FileSystemWatcher> fileSystemWatcherFactory)
|
||||||
|
{
|
||||||
|
Ensure.NotNull(fileSystemWatcherFactory, nameof(fileSystemWatcherFactory));
|
||||||
|
Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory));
|
||||||
|
|
||||||
|
BasePath = watchedDirectory;
|
||||||
|
_watcherFactory = fileSystemWatcherFactory;
|
||||||
|
CreateFileSystemWatcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<string> OnFileChange;
|
||||||
|
|
||||||
|
public event EventHandler<Exception> OnError;
|
||||||
|
|
||||||
|
public string BasePath { get; }
|
||||||
|
|
||||||
|
private static FileSystemWatcher DefaultWatcherFactory(string watchedDirectory)
|
||||||
|
{
|
||||||
|
Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory));
|
||||||
|
|
||||||
|
return new FileSystemWatcher(watchedDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WatcherErrorHandler(object sender, ErrorEventArgs e)
|
||||||
|
{
|
||||||
|
var exception = e.GetException();
|
||||||
|
|
||||||
|
// Win32Exception may be triggered when setting EnableRaisingEvents on a file system type
|
||||||
|
// that is not supported, such as a network share. Don't attempt to recreate the watcher
|
||||||
|
// in this case as it will cause a StackOverflowException
|
||||||
|
if (!(exception is Win32Exception))
|
||||||
|
{
|
||||||
|
// Recreate the watcher if it is a recoverable error.
|
||||||
|
CreateFileSystemWatcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
OnError?.Invoke(this, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WatcherRenameHandler(object sender, RenamedEventArgs e)
|
||||||
|
{
|
||||||
|
NotifyChange(e.OldFullPath);
|
||||||
|
NotifyChange(e.FullPath);
|
||||||
|
|
||||||
|
if (Directory.Exists(e.FullPath))
|
||||||
|
{
|
||||||
|
foreach (var newLocation in Directory.EnumerateFileSystemEntries(e.FullPath, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
// Calculated previous path of this moved item.
|
||||||
|
var oldLocation = Path.Combine(e.OldFullPath, newLocation.Substring(e.FullPath.Length + 1));
|
||||||
|
NotifyChange(oldLocation);
|
||||||
|
NotifyChange(newLocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WatcherChangeHandler(object sender, FileSystemEventArgs e)
|
||||||
|
{
|
||||||
|
NotifyChange(e.FullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NotifyChange(string fullPath)
|
||||||
|
{
|
||||||
|
// Only report file changes
|
||||||
|
OnFileChange?.Invoke(this, fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateFileSystemWatcher()
|
||||||
|
{
|
||||||
|
lock (_createLock)
|
||||||
|
{
|
||||||
|
bool enableEvents = false;
|
||||||
|
|
||||||
|
if (_fileSystemWatcher != null)
|
||||||
|
{
|
||||||
|
enableEvents = _fileSystemWatcher.EnableRaisingEvents;
|
||||||
|
|
||||||
|
_fileSystemWatcher.EnableRaisingEvents = false;
|
||||||
|
|
||||||
|
_fileSystemWatcher.Created -= WatcherChangeHandler;
|
||||||
|
_fileSystemWatcher.Deleted -= WatcherChangeHandler;
|
||||||
|
_fileSystemWatcher.Changed -= WatcherChangeHandler;
|
||||||
|
_fileSystemWatcher.Renamed -= WatcherRenameHandler;
|
||||||
|
_fileSystemWatcher.Error -= WatcherErrorHandler;
|
||||||
|
|
||||||
|
_fileSystemWatcher.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_fileSystemWatcher = _watcherFactory(BasePath);
|
||||||
|
_fileSystemWatcher.IncludeSubdirectories = true;
|
||||||
|
|
||||||
|
_fileSystemWatcher.Created += WatcherChangeHandler;
|
||||||
|
_fileSystemWatcher.Deleted += WatcherChangeHandler;
|
||||||
|
_fileSystemWatcher.Changed += WatcherChangeHandler;
|
||||||
|
_fileSystemWatcher.Renamed += WatcherRenameHandler;
|
||||||
|
_fileSystemWatcher.Error += WatcherErrorHandler;
|
||||||
|
|
||||||
|
_fileSystemWatcher.EnableRaisingEvents = enableEvents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool EnableRaisingEvents
|
||||||
|
{
|
||||||
|
get => _fileSystemWatcher.EnableRaisingEvents;
|
||||||
|
set => _fileSystemWatcher.EnableRaisingEvents = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_fileSystemWatcher.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
public static class FileWatcherFactory
|
||||||
|
{
|
||||||
|
public static IFileSystemWatcher CreateWatcher(string watchedDirectory)
|
||||||
|
=> CreateWatcher(watchedDirectory, CommandLineOptions.IsPollingEnabled);
|
||||||
|
|
||||||
|
public static IFileSystemWatcher CreateWatcher(string watchedDirectory, bool usePollingWatcher)
|
||||||
|
{
|
||||||
|
return usePollingWatcher ?
|
||||||
|
new PollingFileWatcher(watchedDirectory) :
|
||||||
|
new DotnetFileWatcher(watchedDirectory) as IFileSystemWatcher;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
public interface IFileSystemWatcher : IDisposable
|
||||||
|
{
|
||||||
|
event EventHandler<string> OnFileChange;
|
||||||
|
|
||||||
|
event EventHandler<Exception> OnError;
|
||||||
|
|
||||||
|
string BasePath { get; }
|
||||||
|
|
||||||
|
bool EnableRaisingEvents { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
internal class PollingFileWatcher : IFileSystemWatcher
|
||||||
|
{
|
||||||
|
// The minimum interval to rerun the scan
|
||||||
|
private static readonly TimeSpan _minRunInternal = TimeSpan.FromSeconds(.5);
|
||||||
|
|
||||||
|
private readonly DirectoryInfo _watchedDirectory;
|
||||||
|
|
||||||
|
private Dictionary<string, FileMeta> _knownEntities = new Dictionary<string, FileMeta>();
|
||||||
|
private Dictionary<string, FileMeta> _tempDictionary = new Dictionary<string, FileMeta>();
|
||||||
|
private HashSet<string> _changes = new HashSet<string>();
|
||||||
|
|
||||||
|
private Thread _pollingThread;
|
||||||
|
private bool _raiseEvents;
|
||||||
|
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public PollingFileWatcher(string watchedDirectory)
|
||||||
|
{
|
||||||
|
Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory));
|
||||||
|
|
||||||
|
_watchedDirectory = new DirectoryInfo(watchedDirectory);
|
||||||
|
BasePath = _watchedDirectory.FullName;
|
||||||
|
|
||||||
|
_pollingThread = new Thread(new ThreadStart(PollingLoop));
|
||||||
|
_pollingThread.IsBackground = true;
|
||||||
|
_pollingThread.Name = nameof(PollingFileWatcher);
|
||||||
|
|
||||||
|
CreateKnownFilesSnapshot();
|
||||||
|
|
||||||
|
_pollingThread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<string> OnFileChange;
|
||||||
|
|
||||||
|
#pragma warning disable CS0067 // not used
|
||||||
|
public event EventHandler<Exception> OnError;
|
||||||
|
#pragma warning restore
|
||||||
|
|
||||||
|
public string BasePath { get; }
|
||||||
|
|
||||||
|
public bool EnableRaisingEvents
|
||||||
|
{
|
||||||
|
get => _raiseEvents;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
EnsureNotDisposed();
|
||||||
|
_raiseEvents = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PollingLoop()
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
stopwatch.Start();
|
||||||
|
|
||||||
|
while (!_disposed)
|
||||||
|
{
|
||||||
|
if (stopwatch.Elapsed < _minRunInternal)
|
||||||
|
{
|
||||||
|
// Don't run too often
|
||||||
|
// The min wait time here can be double
|
||||||
|
// the value of the variable (FYI)
|
||||||
|
Thread.Sleep(_minRunInternal);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopwatch.Reset();
|
||||||
|
|
||||||
|
if (!_raiseEvents)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckForChangedFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateKnownFilesSnapshot()
|
||||||
|
{
|
||||||
|
_knownEntities.Clear();
|
||||||
|
|
||||||
|
ForeachEntityInDirectory(_watchedDirectory, f =>
|
||||||
|
{
|
||||||
|
_knownEntities.Add(f.FullName, new FileMeta(f));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckForChangedFiles()
|
||||||
|
{
|
||||||
|
_changes.Clear();
|
||||||
|
|
||||||
|
ForeachEntityInDirectory(_watchedDirectory, f =>
|
||||||
|
{
|
||||||
|
var fullFilePath = f.FullName;
|
||||||
|
|
||||||
|
if (!_knownEntities.ContainsKey(fullFilePath))
|
||||||
|
{
|
||||||
|
// New file
|
||||||
|
RecordChange(f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var fileMeta = _knownEntities[fullFilePath];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (fileMeta.FileInfo.LastWriteTime != f.LastWriteTime)
|
||||||
|
{
|
||||||
|
// File changed
|
||||||
|
RecordChange(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
_knownEntities[fullFilePath] = new FileMeta(fileMeta.FileInfo, true);
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
_knownEntities[fullFilePath] = new FileMeta(fileMeta.FileInfo, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_tempDictionary.Add(f.FullName, new FileMeta(f));
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var file in _knownEntities)
|
||||||
|
{
|
||||||
|
if (!file.Value.FoundAgain)
|
||||||
|
{
|
||||||
|
// File deleted
|
||||||
|
RecordChange(file.Value.FileInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifyChanges();
|
||||||
|
|
||||||
|
// Swap the two dictionaries
|
||||||
|
var swap = _knownEntities;
|
||||||
|
_knownEntities = _tempDictionary;
|
||||||
|
_tempDictionary = swap;
|
||||||
|
|
||||||
|
_tempDictionary.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecordChange(FileSystemInfo fileInfo)
|
||||||
|
{
|
||||||
|
if (fileInfo == null ||
|
||||||
|
_changes.Contains(fileInfo.FullName) ||
|
||||||
|
fileInfo.FullName.Equals(_watchedDirectory.FullName, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_changes.Add(fileInfo.FullName);
|
||||||
|
if (fileInfo.FullName != _watchedDirectory.FullName)
|
||||||
|
{
|
||||||
|
var file = fileInfo as FileInfo;
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
RecordChange(file.Directory);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var dir = fileInfo as DirectoryInfo;
|
||||||
|
if (dir != null)
|
||||||
|
{
|
||||||
|
RecordChange(dir.Parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ForeachEntityInDirectory(DirectoryInfo dirInfo, Action<FileSystemInfo> fileAction)
|
||||||
|
{
|
||||||
|
if (!dirInfo.Exists)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entities = dirInfo.EnumerateFileSystemInfos("*.*");
|
||||||
|
foreach (var entity in entities)
|
||||||
|
{
|
||||||
|
fileAction(entity);
|
||||||
|
|
||||||
|
var subdirInfo = entity as DirectoryInfo;
|
||||||
|
if (subdirInfo != null)
|
||||||
|
{
|
||||||
|
ForeachEntityInDirectory(subdirInfo, fileAction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NotifyChanges()
|
||||||
|
{
|
||||||
|
foreach (var path in _changes)
|
||||||
|
{
|
||||||
|
if (_disposed || !_raiseEvents)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OnFileChange != null)
|
||||||
|
{
|
||||||
|
OnFileChange(this, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureNotDisposed()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
throw new ObjectDisposedException(nameof(PollingFileWatcher));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
EnableRaisingEvents = false;
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct FileMeta
|
||||||
|
{
|
||||||
|
public FileMeta(FileSystemInfo fileInfo, bool foundAgain = false)
|
||||||
|
{
|
||||||
|
FileInfo = fileInfo;
|
||||||
|
FoundAgain = foundAgain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileSystemInfo FileInfo;
|
||||||
|
|
||||||
|
public bool FoundAgain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
public class MsBuildFileSetFactory : IFileSetFactory
|
||||||
|
{
|
||||||
|
private const string TargetName = "GenerateWatchList";
|
||||||
|
private const string WatchTargetsFileName = "DotNetWatch.targets";
|
||||||
|
private readonly IReporter _reporter;
|
||||||
|
private readonly string _projectFile;
|
||||||
|
private readonly OutputSink _outputSink;
|
||||||
|
private readonly ProcessRunner _processRunner;
|
||||||
|
private readonly bool _waitOnError;
|
||||||
|
private readonly IReadOnlyList<string> _buildFlags;
|
||||||
|
|
||||||
|
public MsBuildFileSetFactory(IReporter reporter,
|
||||||
|
string projectFile,
|
||||||
|
bool waitOnError,
|
||||||
|
bool trace)
|
||||||
|
: this(reporter, projectFile, new OutputSink(), trace)
|
||||||
|
{
|
||||||
|
_waitOnError = waitOnError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// output sink is for testing
|
||||||
|
internal MsBuildFileSetFactory(IReporter reporter,
|
||||||
|
string projectFile,
|
||||||
|
OutputSink outputSink,
|
||||||
|
bool trace)
|
||||||
|
{
|
||||||
|
Ensure.NotNull(reporter, nameof(reporter));
|
||||||
|
Ensure.NotNullOrEmpty(projectFile, nameof(projectFile));
|
||||||
|
Ensure.NotNull(outputSink, nameof(outputSink));
|
||||||
|
|
||||||
|
_reporter = reporter;
|
||||||
|
_projectFile = projectFile;
|
||||||
|
_outputSink = outputSink;
|
||||||
|
_processRunner = new ProcessRunner(reporter);
|
||||||
|
_buildFlags = InitializeArgs(FindTargetsFile(), trace);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IFileSet> CreateAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var watchList = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var projectDir = Path.GetDirectoryName(_projectFile);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var capture = _outputSink.StartCapture();
|
||||||
|
// TODO adding files doesn't currently work. Need to provide a way to detect new files
|
||||||
|
// find files
|
||||||
|
var processSpec = new ProcessSpec
|
||||||
|
{
|
||||||
|
Executable = DotNetMuxer.MuxerPathOrDefault(),
|
||||||
|
WorkingDirectory = projectDir,
|
||||||
|
Arguments = new[]
|
||||||
|
{
|
||||||
|
"msbuild",
|
||||||
|
_projectFile,
|
||||||
|
$"/p:_DotNetWatchListFile={watchList}"
|
||||||
|
}.Concat(_buildFlags),
|
||||||
|
OutputCapture = capture
|
||||||
|
};
|
||||||
|
|
||||||
|
_reporter.Verbose($"Running MSBuild target '{TargetName}' on '{_projectFile}'");
|
||||||
|
|
||||||
|
var exitCode = await _processRunner.RunAsync(processSpec, cancellationToken);
|
||||||
|
|
||||||
|
if (exitCode == 0 && File.Exists(watchList))
|
||||||
|
{
|
||||||
|
var fileset = new FileSet(
|
||||||
|
File.ReadAllLines(watchList)
|
||||||
|
.Select(l => l?.Trim())
|
||||||
|
.Where(l => !string.IsNullOrEmpty(l)));
|
||||||
|
|
||||||
|
_reporter.Verbose($"Watching {fileset.Count} file(s) for changes");
|
||||||
|
#if DEBUG
|
||||||
|
|
||||||
|
foreach (var file in fileset)
|
||||||
|
{
|
||||||
|
_reporter.Verbose($" -> {file}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Assert(fileset.All(Path.IsPathRooted), "All files should be rooted paths");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return fileset;
|
||||||
|
}
|
||||||
|
|
||||||
|
_reporter.Error($"Error(s) finding watch items project file '{Path.GetFileName(_projectFile)}'");
|
||||||
|
|
||||||
|
_reporter.Output($"MSBuild output from target '{TargetName}':");
|
||||||
|
_reporter.Output(string.Empty);
|
||||||
|
|
||||||
|
foreach (var line in capture.Lines)
|
||||||
|
{
|
||||||
|
_reporter.Output($" {line}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_reporter.Output(string.Empty);
|
||||||
|
|
||||||
|
if (!_waitOnError)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_reporter.Warn("Fix the error to continue or press Ctrl+C to exit.");
|
||||||
|
|
||||||
|
var fileSet = new FileSet(new[] { _projectFile });
|
||||||
|
|
||||||
|
using (var watcher = new FileSetWatcher(fileSet, _reporter))
|
||||||
|
{
|
||||||
|
await watcher.GetChangedFileAsync(cancellationToken);
|
||||||
|
|
||||||
|
_reporter.Output($"File changed: {_projectFile}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (File.Exists(watchList))
|
||||||
|
{
|
||||||
|
File.Delete(watchList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<string> InitializeArgs(string watchTargetsFile, bool trace)
|
||||||
|
{
|
||||||
|
var args = new List<string>
|
||||||
|
{
|
||||||
|
"/nologo",
|
||||||
|
"/v:n",
|
||||||
|
"/t:" + TargetName,
|
||||||
|
"/p:DotNetWatchBuild=true", // extensibility point for users
|
||||||
|
"/p:DesignTimeBuild=true", // don't do expensive things
|
||||||
|
"/p:CustomAfterMicrosoftCommonTargets=" + watchTargetsFile,
|
||||||
|
"/p:CustomAfterMicrosoftCommonCrossTargetingTargets=" + watchTargetsFile,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (trace)
|
||||||
|
{
|
||||||
|
// enables capturing markers to know which projects have been visited
|
||||||
|
args.Add("/p:_DotNetWatchTraceOutput=true");
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FindTargetsFile()
|
||||||
|
{
|
||||||
|
var assemblyDir = Path.GetDirectoryName(typeof(MsBuildFileSetFactory).Assembly.Location);
|
||||||
|
var searchPaths = new[]
|
||||||
|
{
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "assets"),
|
||||||
|
Path.Combine(assemblyDir, "assets"),
|
||||||
|
AppContext.BaseDirectory,
|
||||||
|
assemblyDir,
|
||||||
|
};
|
||||||
|
|
||||||
|
var targetPath = searchPaths.Select(p => Path.Combine(p, WatchTargetsFileName)).FirstOrDefault(File.Exists);
|
||||||
|
if (targetPath == null)
|
||||||
|
{
|
||||||
|
_reporter.Error("Fatal error: could not find DotNetWatch.targets");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return targetPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
// 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 Microsoft.DotNet.Watcher.Tools;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
internal class MsBuildProjectFinder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Finds a compatible MSBuild project.
|
||||||
|
/// <param name="searchBase">The base directory to search</param>
|
||||||
|
/// <param name="project">The filename of the project. Can be null.</param>
|
||||||
|
/// </summary>
|
||||||
|
public static string FindMsBuildProject(string searchBase, string project)
|
||||||
|
{
|
||||||
|
Ensure.NotNullOrEmpty(searchBase, nameof(searchBase));
|
||||||
|
|
||||||
|
var projectPath = project ?? searchBase;
|
||||||
|
|
||||||
|
if (!Path.IsPathRooted(projectPath))
|
||||||
|
{
|
||||||
|
projectPath = Path.Combine(searchBase, projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists(projectPath))
|
||||||
|
{
|
||||||
|
var projects = Directory.EnumerateFileSystemEntries(projectPath, "*.*proj", SearchOption.TopDirectoryOnly)
|
||||||
|
.Where(f => !".xproj".Equals(Path.GetExtension(f), StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (projects.Count > 1)
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException(Resources.FormatError_MultipleProjectsFound(projectPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projects.Count == 0)
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException(Resources.FormatError_NoProjectsFound(projectPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(projectPath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException(Resources.FormatError_ProjectPath_NotFound(projectPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
// 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.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
public class OutputCapture
|
||||||
|
{
|
||||||
|
private readonly List<string> _lines = new List<string>();
|
||||||
|
public IEnumerable<string> Lines => _lines;
|
||||||
|
public void AddLine(string line) => _lines.Add(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
public class OutputSink
|
||||||
|
{
|
||||||
|
public OutputCapture Current { get; private set; }
|
||||||
|
public OutputCapture StartCapture()
|
||||||
|
{
|
||||||
|
return (Current = new OutputCapture());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.Internal;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
{
|
||||||
|
public class ProcessRunner
|
||||||
|
{
|
||||||
|
private readonly IReporter _reporter;
|
||||||
|
|
||||||
|
public ProcessRunner(IReporter reporter)
|
||||||
|
{
|
||||||
|
Ensure.NotNull(reporter, nameof(reporter));
|
||||||
|
|
||||||
|
_reporter = reporter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// May not be necessary in the future. See https://github.com/dotnet/corefx/issues/12039
|
||||||
|
public async Task<int> RunAsync(ProcessSpec processSpec, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Ensure.NotNull(processSpec, nameof(processSpec));
|
||||||
|
|
||||||
|
int exitCode;
|
||||||
|
|
||||||
|
var stopwatch = new Stopwatch();
|
||||||
|
|
||||||
|
using (var process = CreateProcess(processSpec))
|
||||||
|
using (var processState = new ProcessState(process))
|
||||||
|
{
|
||||||
|
cancellationToken.Register(() => processState.TryKill());
|
||||||
|
|
||||||
|
stopwatch.Start();
|
||||||
|
process.Start();
|
||||||
|
_reporter.Verbose($"Started '{processSpec.Executable}' with process id {process.Id}");
|
||||||
|
|
||||||
|
if (processSpec.IsOutputCaptured)
|
||||||
|
{
|
||||||
|
await Task.WhenAll(
|
||||||
|
processState.Task,
|
||||||
|
ConsumeStreamAsync(process.StandardOutput, processSpec.OutputCapture.AddLine),
|
||||||
|
ConsumeStreamAsync(process.StandardError, processSpec.OutputCapture.AddLine)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await processState.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
exitCode = process.ExitCode;
|
||||||
|
stopwatch.Stop();
|
||||||
|
_reporter.Verbose($"Process id {process.Id} ran for {stopwatch.ElapsedMilliseconds}ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Process CreateProcess(ProcessSpec processSpec)
|
||||||
|
{
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
EnableRaisingEvents = true,
|
||||||
|
StartInfo =
|
||||||
|
{
|
||||||
|
FileName = processSpec.Executable,
|
||||||
|
Arguments = ArgumentEscaper.EscapeAndConcatenate(processSpec.Arguments),
|
||||||
|
UseShellExecute = false,
|
||||||
|
WorkingDirectory = processSpec.WorkingDirectory,
|
||||||
|
RedirectStandardOutput = processSpec.IsOutputCaptured,
|
||||||
|
RedirectStandardError = processSpec.IsOutputCaptured,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var env in processSpec.EnvironmentVariables)
|
||||||
|
{
|
||||||
|
process.StartInfo.Environment.Add(env.Key, env.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return process;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ConsumeStreamAsync(StreamReader reader, Action<string> consume)
|
||||||
|
{
|
||||||
|
string line;
|
||||||
|
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
|
||||||
|
{
|
||||||
|
consume?.Invoke(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ProcessState : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Process _process;
|
||||||
|
private readonly TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>();
|
||||||
|
private volatile bool _disposed;
|
||||||
|
|
||||||
|
public ProcessState(Process process)
|
||||||
|
{
|
||||||
|
_process = process;
|
||||||
|
_process.Exited += OnExited;
|
||||||
|
Task = _tcs.Task.ContinueWith(_ =>
|
||||||
|
{
|
||||||
|
// We need to use two WaitForExit calls to ensure that all of the output/events are processed. Previously
|
||||||
|
// this code used Process.Exited, which could result in us missing some output due to the ordering of
|
||||||
|
// events.
|
||||||
|
//
|
||||||
|
// See the remarks here: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit#System_Diagnostics_Process_WaitForExit_System_Int32_
|
||||||
|
if (!process.WaitForExit(Int32.MaxValue))
|
||||||
|
{
|
||||||
|
throw new TimeoutException();
|
||||||
|
}
|
||||||
|
|
||||||
|
process.WaitForExit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Task { get; }
|
||||||
|
|
||||||
|
public void TryKill()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_process.HasExited)
|
||||||
|
{
|
||||||
|
_process.KillTree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExited(object sender, EventArgs args)
|
||||||
|
=> _tcs.TrySetResult(null);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
_disposed = true;
|
||||||
|
TryKill();
|
||||||
|
_process.Exited -= OnExited;
|
||||||
|
_process.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// 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 Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher
|
||||||
|
{
|
||||||
|
public class PrefixConsoleReporter : ConsoleReporter
|
||||||
|
{
|
||||||
|
private object _lock = new object();
|
||||||
|
|
||||||
|
public PrefixConsoleReporter(IConsole console, bool verbose, bool quiet)
|
||||||
|
: base(console, verbose, quiet)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
protected override void WriteLine(TextWriter writer, string message, ConsoleColor? color)
|
||||||
|
{
|
||||||
|
const string prefix = "watch : ";
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||||
|
writer.Write(prefix);
|
||||||
|
Console.ResetColor();
|
||||||
|
|
||||||
|
base.WriteLine(writer, message, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.DotNet.Watcher.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher
|
||||||
|
{
|
||||||
|
public class ProcessSpec
|
||||||
|
{
|
||||||
|
public string Executable { get; set; }
|
||||||
|
public string WorkingDirectory { get; set; }
|
||||||
|
public IDictionary<string, string> EnvironmentVariables { get; } = new Dictionary<string, string>();
|
||||||
|
public IEnumerable<string> Arguments { get; set; }
|
||||||
|
public OutputCapture OutputCapture { get; set; }
|
||||||
|
|
||||||
|
public string ShortDisplayName()
|
||||||
|
=> Path.GetFileNameWithoutExtension(Executable);
|
||||||
|
|
||||||
|
public bool IsOutputCaptured => OutputCapture != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.DotNet.Watcher.Internal;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher
|
||||||
|
{
|
||||||
|
public class Program : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IConsole _console;
|
||||||
|
private readonly string _workingDir;
|
||||||
|
private readonly CancellationTokenSource _cts;
|
||||||
|
private IReporter _reporter;
|
||||||
|
|
||||||
|
public Program(IConsole console, string workingDir)
|
||||||
|
{
|
||||||
|
Ensure.NotNull(console, nameof(console));
|
||||||
|
Ensure.NotNullOrEmpty(workingDir, nameof(workingDir));
|
||||||
|
|
||||||
|
_console = console;
|
||||||
|
_workingDir = workingDir;
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
_console.CancelKeyPress += OnCancelKeyPress;
|
||||||
|
_reporter = CreateReporter(verbose: true, quiet: false, console: _console);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<int> Main(string[] args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DebugHelper.HandleDebugSwitch(ref args);
|
||||||
|
using (var program = new Program(PhysicalConsole.Singleton, Directory.GetCurrentDirectory()))
|
||||||
|
{
|
||||||
|
return await program.RunAsync(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Unexpected error:");
|
||||||
|
Console.Error.WriteLine(ex.ToString());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> RunAsync(string[] args)
|
||||||
|
{
|
||||||
|
CommandLineOptions options;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
options = CommandLineOptions.Parse(args, _console);
|
||||||
|
}
|
||||||
|
catch (CommandParsingException ex)
|
||||||
|
{
|
||||||
|
_reporter.Error(ex.Message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
// invalid args syntax
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.IsHelp)
|
||||||
|
{
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update reporter as configured by options
|
||||||
|
_reporter = CreateReporter(options.IsVerbose, options.IsQuiet, _console);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.ListFiles)
|
||||||
|
{
|
||||||
|
return await ListFilesAsync(_reporter,
|
||||||
|
options.Project,
|
||||||
|
_cts.Token);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return await MainInternalAsync(_reporter,
|
||||||
|
options.Project,
|
||||||
|
options.RemainingArguments,
|
||||||
|
_cts.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (ex is TaskCanceledException || ex is OperationCanceledException)
|
||||||
|
{
|
||||||
|
// swallow when only exception is the CTRL+C forced an exit
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_reporter.Error(ex.ToString());
|
||||||
|
_reporter.Error("An unexpected error occurred");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs args)
|
||||||
|
{
|
||||||
|
// suppress CTRL+C on the first press
|
||||||
|
args.Cancel = !_cts.IsCancellationRequested;
|
||||||
|
|
||||||
|
if (args.Cancel)
|
||||||
|
{
|
||||||
|
_reporter.Output("Shutdown requested. Press Ctrl+C again to force exit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_cts.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<int> MainInternalAsync(
|
||||||
|
IReporter reporter,
|
||||||
|
string project,
|
||||||
|
ICollection<string> args,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// TODO multiple projects should be easy enough to add here
|
||||||
|
string projectFile;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, project);
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException ex)
|
||||||
|
{
|
||||||
|
reporter.Error(ex.Message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileSetFactory = new MsBuildFileSetFactory(reporter,
|
||||||
|
projectFile,
|
||||||
|
waitOnError: true,
|
||||||
|
trace: false);
|
||||||
|
var processInfo = new ProcessSpec
|
||||||
|
{
|
||||||
|
Executable = DotNetMuxer.MuxerPathOrDefault(),
|
||||||
|
WorkingDirectory = Path.GetDirectoryName(projectFile),
|
||||||
|
Arguments = args,
|
||||||
|
EnvironmentVariables =
|
||||||
|
{
|
||||||
|
["DOTNET_WATCH"] = "1"
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (CommandLineOptions.IsPollingEnabled)
|
||||||
|
{
|
||||||
|
_reporter.Output("Polling file watcher is enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
await new DotNetWatcher(reporter)
|
||||||
|
.WatchAsync(processInfo, fileSetFactory, cancellationToken);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<int> ListFilesAsync(
|
||||||
|
IReporter reporter,
|
||||||
|
string project,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// TODO multiple projects should be easy enough to add here
|
||||||
|
string projectFile;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, project);
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException ex)
|
||||||
|
{
|
||||||
|
reporter.Error(ex.Message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileSetFactory = new MsBuildFileSetFactory(reporter,
|
||||||
|
projectFile,
|
||||||
|
waitOnError: false,
|
||||||
|
trace: false);
|
||||||
|
var files = await fileSetFactory.CreateAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (files == null)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
_console.Out.WriteLine(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReporter CreateReporter(bool verbose, bool quiet, IConsole console)
|
||||||
|
=> new PrefixConsoleReporter(console, verbose || CliContext.IsGlobalVerbose(), quiet);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_console.CancelKeyPress -= OnCancelKeyPress;
|
||||||
|
_cts.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.DotNet.Watcher.Tools.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
// <auto-generated />
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools
|
||||||
|
{
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Resources;
|
||||||
|
|
||||||
|
internal static class Resources
|
||||||
|
{
|
||||||
|
private static readonly ResourceManager _resourceManager
|
||||||
|
= new ResourceManager("Microsoft.DotNet.Watcher.Tools.Resources", typeof(Resources).GetTypeInfo().Assembly);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The project file '{path}' does not exist.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Error_ProjectPath_NotFound
|
||||||
|
{
|
||||||
|
get { return GetString("Error_ProjectPath_NotFound"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The project file '{path}' does not exist.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatError_ProjectPath_NotFound(object path)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectPath_NotFound", "path"), path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Error_MultipleProjectsFound
|
||||||
|
{
|
||||||
|
get { return GetString("Error_MultipleProjectsFound"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatError_MultipleProjectsFound(object projectPath)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Error_MultipleProjectsFound", "projectPath"), projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Error_NoProjectsFound
|
||||||
|
{
|
||||||
|
get { return GetString("Error_NoProjectsFound"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatError_NoProjectsFound(object projectPath)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Error_NoProjectsFound", "projectPath"), projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cannot specify both '--quiet' and '--verbose' options.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Error_QuietAndVerboseSpecified
|
||||||
|
{
|
||||||
|
get { return GetString("Error_QuietAndVerboseSpecified"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cannot specify both '--quiet' and '--verbose' options.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatError_QuietAndVerboseSpecified()
|
||||||
|
{
|
||||||
|
return GetString("Error_QuietAndVerboseSpecified");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
|
{
|
||||||
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
||||||
|
System.Diagnostics.Debug.Assert(value != null);
|
||||||
|
|
||||||
|
if (formatterNames != null)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < formatterNames.Length; i++)
|
||||||
|
{
|
||||||
|
value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="Error_ProjectPath_NotFound" xml:space="preserve">
|
||||||
|
<value>The project file '{path}' does not exist.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Error_MultipleProjectsFound" xml:space="preserve">
|
||||||
|
<value>Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Error_NoProjectsFound" xml:space="preserve">
|
||||||
|
<value>Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Error_QuietAndVerboseSpecified" xml:space="preserve">
|
||||||
|
<value>Cannot specify both '--quiet' and '--verbose' options.</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<!--
|
||||||
|
=========================================================================
|
||||||
|
GenerateWatchList
|
||||||
|
|
||||||
|
Main target called by dotnet-watch. It gathers MSBuild items and writes
|
||||||
|
them to a file.
|
||||||
|
=========================================================================
|
||||||
|
-->
|
||||||
|
<Target Name="GenerateWatchList"
|
||||||
|
DependsOnTargets="_CollectWatchItems">
|
||||||
|
<WriteLinesToFile Overwrite="true"
|
||||||
|
File="$(_DotNetWatchListFile)"
|
||||||
|
Lines="@(Watch -> '%(FullPath)')" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
=========================================================================
|
||||||
|
_CollectWatchItems
|
||||||
|
|
||||||
|
Gathers all files to be watched.
|
||||||
|
Returns: @(Watch)
|
||||||
|
=========================================================================
|
||||||
|
-->
|
||||||
|
<PropertyGroup>
|
||||||
|
<_CollectWatchItemsDependsOn Condition=" '$(TargetFrameworks)' != '' AND '$(TargetFramework)' == '' ">
|
||||||
|
_CollectWatchItemsPerFramework;
|
||||||
|
</_CollectWatchItemsDependsOn>
|
||||||
|
<_CollectWatchItemsDependsOn Condition=" '$(TargetFramework)' != '' ">
|
||||||
|
_CoreCollectWatchItems;
|
||||||
|
</_CollectWatchItemsDependsOn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="_CollectWatchItems" DependsOnTargets="$(_CollectWatchItemsDependsOn)" Returns="@(Watch)" />
|
||||||
|
|
||||||
|
<Target Name="_CollectWatchItemsPerFramework">
|
||||||
|
<ItemGroup>
|
||||||
|
<_TargetFramework Include="$(TargetFrameworks)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<MSBuild Projects="$(MSBuildProjectFullPath)"
|
||||||
|
Targets="_CoreCollectWatchItems"
|
||||||
|
Properties="TargetFramework=%(_TargetFramework.Identity)">
|
||||||
|
<Output TaskParameter="TargetOutputs" ItemName="Watch" />
|
||||||
|
</MSBuild>
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="_CoreCollectWatchItems" Returns="@(Watch)">
|
||||||
|
<!-- message used to debug -->
|
||||||
|
<Message Importance="High" Text="Collecting watch items from '$(MSBuildProjectName)'" Condition="'$(_DotNetWatchTraceOutput)'=='true'" />
|
||||||
|
|
||||||
|
<Error Text="TargetFramework should be set" Condition="'$(TargetFramework)' == '' "/>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Watch Include="%(Compile.FullPath)" Condition="'%(Compile.Watch)' != 'false'" />
|
||||||
|
<Watch Include="%(EmbeddedResource.FullPath)" Condition="'%(EmbeddedResource.Watch)' != 'false'"/>
|
||||||
|
<Watch Include="$(MSBuildProjectFullPath)" />
|
||||||
|
<_WatchProjects Include="%(ProjectReference.Identity)" Condition="'%(ProjectReference.Watch)' != 'false'" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<MSBuild Projects="@(_WatchProjects)"
|
||||||
|
Targets="_CollectWatchItems"
|
||||||
|
BuildInParallel="true">
|
||||||
|
<Output TaskParameter="TargetOutputs" ItemName="Watch" />
|
||||||
|
</MSBuild>
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<OutputType>exe</OutputType>
|
||||||
|
<Description>Command line tool to watch for source file changes during development and restart the dotnet command.</Description>
|
||||||
|
<RootNamespace>Microsoft.DotNet.Watcher.Tools</RootNamespace>
|
||||||
|
<PackageTags>dotnet;watch</PackageTags>
|
||||||
|
<PackAsTool>true</PackAsTool>
|
||||||
|
<!-- This is a requirement for Microsoft tool packages only. -->
|
||||||
|
<PackAsToolShimRuntimeIdentifiers>win-x64;win-x86</PackAsToolShimRuntimeIdentifiers>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\..\shared\src\**\*.cs" />
|
||||||
|
<None Include="assets\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.CommandLineUtils.Sources" PrivateAssets="All" Version="$(MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Process.Sources" PrivateAssets="All" Version="$(MicrosoftExtensionsProcessSourcesPackageVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
|
{
|
||||||
|
public class AppWithDepsTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly AppWithDeps _app;
|
||||||
|
|
||||||
|
public AppWithDepsTests(ITestOutputHelper logger)
|
||||||
|
{
|
||||||
|
_app = new AppWithDeps(logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChangeFileInDependency()
|
||||||
|
{
|
||||||
|
await _app.StartWatcherAsync();
|
||||||
|
|
||||||
|
var fileToChange = Path.Combine(_app.DependencyFolder, "Foo.cs");
|
||||||
|
var programCs = File.ReadAllText(fileToChange);
|
||||||
|
File.WriteAllText(fileToChange, programCs);
|
||||||
|
|
||||||
|
await _app.HasRestarted();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_app.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AppWithDeps : WatchableApp
|
||||||
|
{
|
||||||
|
private const string Dependency = "Dependency";
|
||||||
|
|
||||||
|
public AppWithDeps(ITestOutputHelper logger)
|
||||||
|
: base("AppWithDeps", logger)
|
||||||
|
{
|
||||||
|
Scenario.AddTestProjectFolder(Dependency);
|
||||||
|
|
||||||
|
DependencyFolder = Path.Combine(Scenario.WorkFolder, Dependency);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DependencyFolder { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
// 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 Xunit;
|
||||||
|
using Xunit.Sdk;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools.Tests
|
||||||
|
{
|
||||||
|
public static class AssertEx
|
||||||
|
{
|
||||||
|
public static void EqualFileList(string root, IEnumerable<string> expectedFiles, IEnumerable<string> actualFiles)
|
||||||
|
{
|
||||||
|
var expected = expectedFiles.Select(p => Path.Combine(root, p));
|
||||||
|
EqualFileList(expected, actualFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void EqualFileList(IEnumerable<string> expectedFiles, IEnumerable<string> actualFiles)
|
||||||
|
{
|
||||||
|
string normalize(string p) => p.Replace('\\', '/');
|
||||||
|
var expected = new HashSet<string>(expectedFiles.Select(normalize));
|
||||||
|
var actual = new HashSet<string>(actualFiles.Where(p => !string.IsNullOrEmpty(p)).Select(normalize));
|
||||||
|
if (!expected.SetEquals(actual))
|
||||||
|
{
|
||||||
|
throw new AssertActualExpectedException(
|
||||||
|
expected: "\n" + string.Join("\n", expected),
|
||||||
|
actual: "\n" + string.Join("\n", actual),
|
||||||
|
userMessage: "File sets should be equal");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading.Tasks.Dataflow;
|
||||||
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
using Microsoft.Extensions.Internal;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
|
{
|
||||||
|
public class AwaitableProcess : IDisposable
|
||||||
|
{
|
||||||
|
private Process _process;
|
||||||
|
private readonly ProcessSpec _spec;
|
||||||
|
private BufferBlock<string> _source;
|
||||||
|
private ITestOutputHelper _logger;
|
||||||
|
|
||||||
|
public AwaitableProcess(ProcessSpec spec, ITestOutputHelper logger)
|
||||||
|
{
|
||||||
|
_spec = spec;
|
||||||
|
_logger = logger;
|
||||||
|
_source = new BufferBlock<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (_process != null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Already started");
|
||||||
|
}
|
||||||
|
|
||||||
|
_process = new Process
|
||||||
|
{
|
||||||
|
EnableRaisingEvents = true,
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
UseShellExecute = false,
|
||||||
|
FileName = _spec.Executable,
|
||||||
|
WorkingDirectory = _spec.WorkingDirectory,
|
||||||
|
Arguments = ArgumentEscaper.EscapeAndConcatenate(_spec.Arguments),
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
Environment =
|
||||||
|
{
|
||||||
|
["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_process.OutputDataReceived += OnData;
|
||||||
|
_process.ErrorDataReceived += OnData;
|
||||||
|
_process.Exited += OnExit;
|
||||||
|
|
||||||
|
_process.Start();
|
||||||
|
_process.BeginErrorReadLine();
|
||||||
|
_process.BeginOutputReadLine();
|
||||||
|
_logger.WriteLine($"{DateTime.Now}: process start: '{_process.StartInfo.FileName} {_process.StartInfo.Arguments}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetOutputLineAsync(string message, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
_logger.WriteLine($"Waiting for output line [msg == '{message}']. Will wait for {timeout.TotalSeconds} sec.");
|
||||||
|
return await GetOutputLineAsync(m => message == m).TimeoutAfter(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetOutputLineStartsWithAsync(string message, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
_logger.WriteLine($"Waiting for output line [msg.StartsWith('{message}')]. Will wait for {timeout.TotalSeconds} sec.");
|
||||||
|
return await GetOutputLineAsync(m => m.StartsWith(message)).TimeoutAfter(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetOutputLineAsync(Predicate<string> predicate)
|
||||||
|
{
|
||||||
|
while (!_source.Completion.IsCompleted)
|
||||||
|
{
|
||||||
|
while (await _source.OutputAvailableAsync())
|
||||||
|
{
|
||||||
|
var next = await _source.ReceiveAsync();
|
||||||
|
_logger.WriteLine($"{DateTime.Now}: recv: '{next}'");
|
||||||
|
if (predicate(next))
|
||||||
|
{
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IList<string>> GetAllOutputLines()
|
||||||
|
{
|
||||||
|
var lines = new List<string>();
|
||||||
|
while (!_source.Completion.IsCompleted)
|
||||||
|
{
|
||||||
|
while (await _source.OutputAvailableAsync())
|
||||||
|
{
|
||||||
|
var next = await _source.ReceiveAsync();
|
||||||
|
_logger.WriteLine($"{DateTime.Now}: recv: '{next}'");
|
||||||
|
lines.Add(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnData(object sender, DataReceivedEventArgs args)
|
||||||
|
{
|
||||||
|
var line = args.Data ?? string.Empty;
|
||||||
|
_logger.WriteLine($"{DateTime.Now}: post: '{line}'");
|
||||||
|
_source.Post(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExit(object sender, EventArgs args)
|
||||||
|
{
|
||||||
|
// Wait to ensure the process has exited and all output consumed
|
||||||
|
_process.WaitForExit();
|
||||||
|
_source.Complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_source.Complete();
|
||||||
|
|
||||||
|
if (_process != null)
|
||||||
|
{
|
||||||
|
if (!_process.HasExited)
|
||||||
|
{
|
||||||
|
_process.KillTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
_process.ErrorDataReceived -= OnData;
|
||||||
|
_process.OutputDataReceived -= OnData;
|
||||||
|
_process.Exited -= OnExit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
// 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;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools.Tests
|
||||||
|
{
|
||||||
|
public class CommandLineOptionsTests
|
||||||
|
{
|
||||||
|
private readonly IConsole _console;
|
||||||
|
private readonly StringBuilder _stdout = new StringBuilder();
|
||||||
|
|
||||||
|
public CommandLineOptionsTests(ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
_console = new TestConsole(output)
|
||||||
|
{
|
||||||
|
Out = new StringWriter(_stdout),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(new object[] { new[] { "-h" } })]
|
||||||
|
[InlineData(new object[] { new[] { "-?" } })]
|
||||||
|
[InlineData(new object[] { new[] { "--help" } })]
|
||||||
|
[InlineData(new object[] { new[] { "--help", "--bogus" } })]
|
||||||
|
[InlineData(new object[] { new[] { "--" } })]
|
||||||
|
[InlineData(new object[] { new string[0] })]
|
||||||
|
public void HelpArgs(string[] args)
|
||||||
|
{
|
||||||
|
var options = CommandLineOptions.Parse(args, _console);
|
||||||
|
|
||||||
|
Assert.True(options.IsHelp);
|
||||||
|
Assert.Contains("Usage: dotnet watch ", _stdout.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(new[] { "run" }, new[] { "run" })]
|
||||||
|
[InlineData(new[] { "run", "--", "subarg" }, new[] { "run", "--", "subarg" })]
|
||||||
|
[InlineData(new[] { "--", "run", "--", "subarg" }, new[] { "run", "--", "subarg" })]
|
||||||
|
[InlineData(new[] { "--unrecognized-arg" }, new[] { "--unrecognized-arg" })]
|
||||||
|
public void ParsesRemainingArgs(string[] args, string[] expected)
|
||||||
|
{
|
||||||
|
var options = CommandLineOptions.Parse(args, _console);
|
||||||
|
|
||||||
|
Assert.Equal(expected, options.RemainingArguments.ToArray());
|
||||||
|
Assert.False(options.IsHelp);
|
||||||
|
Assert.Empty(_stdout.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CannotHaveQuietAndVerbose()
|
||||||
|
{
|
||||||
|
var ex = Assert.Throws<CommandParsingException>(() => CommandLineOptions.Parse(new[] { "--quiet", "--verbose" }, _console));
|
||||||
|
Assert.Equal(Resources.Error_QuietAndVerboseSpecified, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
// 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.Text;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Tools.Tests
|
||||||
|
{
|
||||||
|
public class ReporterTests
|
||||||
|
{
|
||||||
|
private static readonly string EOL = Environment.NewLine;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WritesToStandardStreams()
|
||||||
|
{
|
||||||
|
var testConsole = new TestConsole();
|
||||||
|
var reporter = new ConsoleReporter(testConsole, verbose: true, quiet: false);
|
||||||
|
|
||||||
|
// stdout
|
||||||
|
reporter.Verbose("verbose");
|
||||||
|
Assert.Equal("verbose" + EOL, testConsole.GetOutput());
|
||||||
|
testConsole.Clear();
|
||||||
|
|
||||||
|
reporter.Output("out");
|
||||||
|
Assert.Equal("out" + EOL, testConsole.GetOutput());
|
||||||
|
testConsole.Clear();
|
||||||
|
|
||||||
|
reporter.Warn("warn");
|
||||||
|
Assert.Equal("warn" + EOL, testConsole.GetOutput());
|
||||||
|
testConsole.Clear();
|
||||||
|
|
||||||
|
// stderr
|
||||||
|
reporter.Error("error");
|
||||||
|
Assert.Equal("error" + EOL, testConsole.GetError());
|
||||||
|
testConsole.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestConsole : IConsole
|
||||||
|
{
|
||||||
|
private readonly StringBuilder _out;
|
||||||
|
private readonly StringBuilder _error;
|
||||||
|
|
||||||
|
public TestConsole()
|
||||||
|
{
|
||||||
|
_out = new StringBuilder();
|
||||||
|
_error = new StringBuilder();
|
||||||
|
Out = new StringWriter(_out);
|
||||||
|
Error = new StringWriter(_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
event ConsoleCancelEventHandler IConsole.CancelKeyPress
|
||||||
|
{
|
||||||
|
add { }
|
||||||
|
remove { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetOutput() => _out.ToString();
|
||||||
|
public string GetError() => _error.ToString();
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_out.Clear();
|
||||||
|
_error.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetColor()
|
||||||
|
{
|
||||||
|
ForegroundColor = default(ConsoleColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextWriter Out { get; }
|
||||||
|
public TextWriter Error { get; }
|
||||||
|
public TextReader In { get; }
|
||||||
|
public bool IsInputRedirected { get; }
|
||||||
|
public bool IsOutputRedirected { get; }
|
||||||
|
public bool IsErrorRedirected { get; }
|
||||||
|
public ConsoleColor ForegroundColor { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
// 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.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
|
{
|
||||||
|
public class DotNetWatcherTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly KitchenSinkApp _app;
|
||||||
|
|
||||||
|
public DotNetWatcherTests(ITestOutputHelper logger)
|
||||||
|
{
|
||||||
|
_app = new KitchenSinkApp(logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunsWithDotnetWatchEnvVariable()
|
||||||
|
{
|
||||||
|
Assert.True(string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DOTNET_WATCH")), "DOTNET_WATCH cannot be set already when this test is running");
|
||||||
|
|
||||||
|
await _app.StartWatcherAsync();
|
||||||
|
const string messagePrefix = "DOTNET_WATCH = ";
|
||||||
|
var message = await _app.Process.GetOutputLineStartsWithAsync(messagePrefix, TimeSpan.FromMinutes(2));
|
||||||
|
var envValue = message.Substring(messagePrefix.Length);
|
||||||
|
Assert.Equal("1", envValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_app.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class KitchenSinkApp : WatchableApp
|
||||||
|
{
|
||||||
|
public KitchenSinkApp(ITestOutputHelper logger)
|
||||||
|
: base("KitchenSink", logger)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,417 @@
|
||||||
|
// 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.Threading;
|
||||||
|
using Microsoft.DotNet.Watcher.Internal;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
|
{
|
||||||
|
public class FileWatcherTests
|
||||||
|
{
|
||||||
|
public FileWatcherTests(ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
_output = output;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int DefaultTimeout = 10 * 1000; // 10 sec
|
||||||
|
private readonly ITestOutputHelper _output;
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void NewFile(bool usePolling)
|
||||||
|
{
|
||||||
|
UsingTempDirectory(dir =>
|
||||||
|
{
|
||||||
|
using (var changedEv = new ManualResetEvent(false))
|
||||||
|
using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling))
|
||||||
|
{
|
||||||
|
var filesChanged = new HashSet<string>();
|
||||||
|
|
||||||
|
watcher.OnFileChange += (_, f) =>
|
||||||
|
{
|
||||||
|
filesChanged.Add(f);
|
||||||
|
changedEv.Set();
|
||||||
|
};
|
||||||
|
watcher.EnableRaisingEvents = true;
|
||||||
|
|
||||||
|
var testFileFullPath = Path.Combine(dir, "foo");
|
||||||
|
File.WriteAllText(testFileFullPath, string.Empty);
|
||||||
|
|
||||||
|
Assert.True(changedEv.WaitOne(DefaultTimeout));
|
||||||
|
Assert.Equal(testFileFullPath, filesChanged.Single());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void ChangeFile(bool usePolling)
|
||||||
|
{
|
||||||
|
UsingTempDirectory(dir =>
|
||||||
|
{
|
||||||
|
var testFileFullPath = Path.Combine(dir, "foo");
|
||||||
|
File.WriteAllText(testFileFullPath, string.Empty);
|
||||||
|
|
||||||
|
using (var changedEv = new ManualResetEvent(false))
|
||||||
|
using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling))
|
||||||
|
{
|
||||||
|
var filesChanged = new HashSet<string>();
|
||||||
|
|
||||||
|
EventHandler<string> handler = null;
|
||||||
|
handler = (_, f) =>
|
||||||
|
{
|
||||||
|
watcher.EnableRaisingEvents = false;
|
||||||
|
watcher.OnFileChange -= handler;
|
||||||
|
|
||||||
|
filesChanged.Add(f);
|
||||||
|
changedEv.Set();
|
||||||
|
};
|
||||||
|
|
||||||
|
watcher.OnFileChange += handler;
|
||||||
|
watcher.EnableRaisingEvents = true;
|
||||||
|
|
||||||
|
// On Unix the file write time is in 1s increments;
|
||||||
|
// if we don't wait, there's a chance that the polling
|
||||||
|
// watcher will not detect the change
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
File.WriteAllText(testFileFullPath, string.Empty);
|
||||||
|
|
||||||
|
Assert.True(changedEv.WaitOne(DefaultTimeout));
|
||||||
|
Assert.Equal(testFileFullPath, filesChanged.Single());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void MoveFile(bool usePolling)
|
||||||
|
{
|
||||||
|
UsingTempDirectory(dir =>
|
||||||
|
{
|
||||||
|
var srcFile = Path.Combine(dir, "foo");
|
||||||
|
var dstFile = Path.Combine(dir, "foo2");
|
||||||
|
|
||||||
|
File.WriteAllText(srcFile, string.Empty);
|
||||||
|
|
||||||
|
using (var changedEv = new ManualResetEvent(false))
|
||||||
|
using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling))
|
||||||
|
{
|
||||||
|
var filesChanged = new HashSet<string>();
|
||||||
|
|
||||||
|
EventHandler<string> handler = null;
|
||||||
|
handler = (_, f) =>
|
||||||
|
{
|
||||||
|
filesChanged.Add(f);
|
||||||
|
|
||||||
|
if (filesChanged.Count >= 2)
|
||||||
|
{
|
||||||
|
watcher.EnableRaisingEvents = false;
|
||||||
|
watcher.OnFileChange -= handler;
|
||||||
|
|
||||||
|
changedEv.Set();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watcher.OnFileChange += handler;
|
||||||
|
watcher.EnableRaisingEvents = true;
|
||||||
|
|
||||||
|
File.Move(srcFile, dstFile);
|
||||||
|
|
||||||
|
Assert.True(changedEv.WaitOne(DefaultTimeout));
|
||||||
|
Assert.Contains(srcFile, filesChanged);
|
||||||
|
Assert.Contains(dstFile, filesChanged);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FileInSubdirectory()
|
||||||
|
{
|
||||||
|
UsingTempDirectory(dir =>
|
||||||
|
{
|
||||||
|
var subdir = Path.Combine(dir, "subdir");
|
||||||
|
Directory.CreateDirectory(subdir);
|
||||||
|
|
||||||
|
var testFileFullPath = Path.Combine(subdir, "foo");
|
||||||
|
File.WriteAllText(testFileFullPath, string.Empty);
|
||||||
|
|
||||||
|
using (var changedEv = new ManualResetEvent(false))
|
||||||
|
using (var watcher = FileWatcherFactory.CreateWatcher(dir, true))
|
||||||
|
{
|
||||||
|
var filesChanged = new HashSet<string>();
|
||||||
|
|
||||||
|
EventHandler<string> handler = null;
|
||||||
|
handler = (_, f) =>
|
||||||
|
{
|
||||||
|
filesChanged.Add(f);
|
||||||
|
|
||||||
|
if (filesChanged.Count >= 2)
|
||||||
|
{
|
||||||
|
watcher.EnableRaisingEvents = false;
|
||||||
|
watcher.OnFileChange -= handler;
|
||||||
|
changedEv.Set();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watcher.OnFileChange += handler;
|
||||||
|
watcher.EnableRaisingEvents = true;
|
||||||
|
|
||||||
|
// On Unix the file write time is in 1s increments;
|
||||||
|
// if we don't wait, there's a chance that the polling
|
||||||
|
// watcher will not detect the change
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
File.WriteAllText(testFileFullPath, string.Empty);
|
||||||
|
|
||||||
|
Assert.True(changedEv.WaitOne(DefaultTimeout));
|
||||||
|
Assert.Contains(subdir, filesChanged);
|
||||||
|
Assert.Contains(testFileFullPath, filesChanged);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void NoNotificationIfDisabled(bool usePolling)
|
||||||
|
{
|
||||||
|
UsingTempDirectory(dir =>
|
||||||
|
{
|
||||||
|
using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling))
|
||||||
|
using (var changedEv = new ManualResetEvent(false))
|
||||||
|
{
|
||||||
|
watcher.OnFileChange += (_, f) => changedEv.Set();
|
||||||
|
|
||||||
|
// Disable
|
||||||
|
watcher.EnableRaisingEvents = false;
|
||||||
|
|
||||||
|
var testFileFullPath = Path.Combine(dir, "foo");
|
||||||
|
|
||||||
|
// On Unix the file write time is in 1s increments;
|
||||||
|
// if we don't wait, there's a chance that the polling
|
||||||
|
// watcher will not detect the change
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
File.WriteAllText(testFileFullPath, string.Empty);
|
||||||
|
|
||||||
|
Assert.False(changedEv.WaitOne(DefaultTimeout / 2));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void DisposedNoEvents(bool usePolling)
|
||||||
|
{
|
||||||
|
UsingTempDirectory(dir =>
|
||||||
|
{
|
||||||
|
using (var changedEv = new ManualResetEvent(false))
|
||||||
|
{
|
||||||
|
using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling))
|
||||||
|
{
|
||||||
|
watcher.OnFileChange += (_, f) => changedEv.Set();
|
||||||
|
watcher.EnableRaisingEvents = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var testFileFullPath = Path.Combine(dir, "foo");
|
||||||
|
|
||||||
|
// On Unix the file write time is in 1s increments;
|
||||||
|
// if we don't wait, there's a chance that the polling
|
||||||
|
// watcher will not detect the change
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
File.WriteAllText(testFileFullPath, string.Empty);
|
||||||
|
|
||||||
|
Assert.False(changedEv.WaitOne(DefaultTimeout / 2));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void MultipleFiles(bool usePolling)
|
||||||
|
{
|
||||||
|
UsingTempDirectory(dir =>
|
||||||
|
{
|
||||||
|
File.WriteAllText(Path.Combine(dir, "foo1"), string.Empty);
|
||||||
|
File.WriteAllText(Path.Combine(dir, "foo2"), string.Empty);
|
||||||
|
File.WriteAllText(Path.Combine(dir, "foo3"), string.Empty);
|
||||||
|
File.WriteAllText(Path.Combine(dir, "foo4"), string.Empty);
|
||||||
|
File.WriteAllText(Path.Combine(dir, "foo4"), string.Empty);
|
||||||
|
|
||||||
|
var testFileFullPath = Path.Combine(dir, "foo3");
|
||||||
|
|
||||||
|
using (var changedEv = new ManualResetEvent(false))
|
||||||
|
using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling))
|
||||||
|
{
|
||||||
|
var filesChanged = new HashSet<string>();
|
||||||
|
|
||||||
|
EventHandler<string> handler = null;
|
||||||
|
handler = (_, f) =>
|
||||||
|
{
|
||||||
|
watcher.EnableRaisingEvents = false;
|
||||||
|
watcher.OnFileChange -= handler;
|
||||||
|
filesChanged.Add(f);
|
||||||
|
changedEv.Set();
|
||||||
|
};
|
||||||
|
|
||||||
|
watcher.OnFileChange += handler;
|
||||||
|
watcher.EnableRaisingEvents = true;
|
||||||
|
|
||||||
|
// On Unix the file write time is in 1s increments;
|
||||||
|
// if we don't wait, there's a chance that the polling
|
||||||
|
// watcher will not detect the change
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
|
||||||
|
File.WriteAllText(testFileFullPath, string.Empty);
|
||||||
|
|
||||||
|
Assert.True(changedEv.WaitOne(DefaultTimeout));
|
||||||
|
Assert.Equal(testFileFullPath, filesChanged.Single());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void MultipleTriggers(bool usePolling)
|
||||||
|
{
|
||||||
|
var filesChanged = new HashSet<string>();
|
||||||
|
|
||||||
|
UsingTempDirectory(dir =>
|
||||||
|
{
|
||||||
|
using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling))
|
||||||
|
{
|
||||||
|
watcher.EnableRaisingEvents = true;
|
||||||
|
|
||||||
|
for (var i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
AssertFileChangeRaisesEvent(dir, watcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher.EnableRaisingEvents = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AssertFileChangeRaisesEvent(string directory, IFileSystemWatcher watcher)
|
||||||
|
{
|
||||||
|
var semaphoreSlim = new SemaphoreSlim(0);
|
||||||
|
var expectedPath = Path.Combine(directory, Path.GetRandomFileName());
|
||||||
|
EventHandler<string> handler = (object _, string f) =>
|
||||||
|
{
|
||||||
|
_output.WriteLine("File changed: " + f);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.Equals(f, expectedPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
semaphoreSlim.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// There's a known race condition here:
|
||||||
|
// even though we tell the watcher to stop raising events and we unsubscribe the handler
|
||||||
|
// there might be in-flight events that will still process. Since we dispose the reset
|
||||||
|
// event, this code will fail if the handler executes after Dispose happens.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
File.AppendAllText(expectedPath, " ");
|
||||||
|
|
||||||
|
watcher.OnFileChange += handler;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// On Unix the file write time is in 1s increments;
|
||||||
|
// if we don't wait, there's a chance that the polling
|
||||||
|
// watcher will not detect the change
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
File.AppendAllText(expectedPath, " ");
|
||||||
|
Assert.True(semaphoreSlim.Wait(DefaultTimeout), "Expected a file change event for " + expectedPath);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
watcher.OnFileChange -= handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void DeleteSubfolder(bool usePolling)
|
||||||
|
{
|
||||||
|
UsingTempDirectory(dir =>
|
||||||
|
{
|
||||||
|
var subdir = Path.Combine(dir, "subdir");
|
||||||
|
Directory.CreateDirectory(subdir);
|
||||||
|
|
||||||
|
var f1 = Path.Combine(subdir, "foo1");
|
||||||
|
var f2 = Path.Combine(subdir, "foo2");
|
||||||
|
var f3 = Path.Combine(subdir, "foo3");
|
||||||
|
|
||||||
|
File.WriteAllText(f1, string.Empty);
|
||||||
|
File.WriteAllText(f2, string.Empty);
|
||||||
|
File.WriteAllText(f3, string.Empty);
|
||||||
|
|
||||||
|
using (var changedEv = new AutoResetEvent(false))
|
||||||
|
using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling))
|
||||||
|
{
|
||||||
|
var filesChanged = new HashSet<string>();
|
||||||
|
|
||||||
|
EventHandler<string> handler = null;
|
||||||
|
handler = (_, f) =>
|
||||||
|
{
|
||||||
|
filesChanged.Add(f);
|
||||||
|
|
||||||
|
if (filesChanged.Count >= 4)
|
||||||
|
{
|
||||||
|
watcher.EnableRaisingEvents = false;
|
||||||
|
watcher.OnFileChange -= handler;
|
||||||
|
changedEv.Set();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watcher.OnFileChange += handler;
|
||||||
|
watcher.EnableRaisingEvents = true;
|
||||||
|
|
||||||
|
Directory.Delete(subdir, recursive: true);
|
||||||
|
|
||||||
|
Assert.True(changedEv.WaitOne(DefaultTimeout));
|
||||||
|
|
||||||
|
Assert.Contains(f1, filesChanged);
|
||||||
|
Assert.Contains(f2, filesChanged);
|
||||||
|
Assert.Contains(f3, filesChanged);
|
||||||
|
Assert.Contains(subdir, filesChanged);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UsingTempDirectory(Action<string> action)
|
||||||
|
{
|
||||||
|
var tempFolder = Path.Combine(Path.GetTempPath(), $"{nameof(FileWatcherTests)}-{Guid.NewGuid().ToString("N")}");
|
||||||
|
if (Directory.Exists(tempFolder))
|
||||||
|
{
|
||||||
|
Directory.Delete(tempFolder, recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(tempFolder);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action(tempFolder);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(tempFolder, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
// 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.Tasks;
|
||||||
|
using Microsoft.DotNet.Watcher.Tools.Tests;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
|
{
|
||||||
|
public class GlobbingAppTests : IDisposable
|
||||||
|
{
|
||||||
|
private GlobbingApp _app;
|
||||||
|
public GlobbingAppTests(ITestOutputHelper logger)
|
||||||
|
{
|
||||||
|
_app = new GlobbingApp(logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public async Task ChangeCompiledFile(bool usePollingWatcher)
|
||||||
|
{
|
||||||
|
_app.UsePollingWatcher = usePollingWatcher;
|
||||||
|
await _app.StartWatcherAsync();
|
||||||
|
|
||||||
|
var types = await _app.GetCompiledAppDefinedTypes();
|
||||||
|
Assert.Equal(2, types);
|
||||||
|
|
||||||
|
var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Foo.cs");
|
||||||
|
var programCs = File.ReadAllText(fileToChange);
|
||||||
|
File.WriteAllText(fileToChange, programCs);
|
||||||
|
|
||||||
|
await _app.HasRestarted();
|
||||||
|
types = await _app.GetCompiledAppDefinedTypes();
|
||||||
|
Assert.Equal(2, types);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteCompiledFile()
|
||||||
|
{
|
||||||
|
await _app.StartWatcherAsync();
|
||||||
|
|
||||||
|
var types = await _app.GetCompiledAppDefinedTypes();
|
||||||
|
Assert.Equal(2, types);
|
||||||
|
|
||||||
|
var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Foo.cs");
|
||||||
|
File.Delete(fileToChange);
|
||||||
|
|
||||||
|
await _app.HasRestarted();
|
||||||
|
types = await _app.GetCompiledAppDefinedTypes();
|
||||||
|
Assert.Equal(1, types);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteSourceFolder()
|
||||||
|
{
|
||||||
|
await _app.StartWatcherAsync();
|
||||||
|
|
||||||
|
var types = await _app.GetCompiledAppDefinedTypes();
|
||||||
|
Assert.Equal(2, types);
|
||||||
|
|
||||||
|
var folderToDelete = Path.Combine(_app.SourceDirectory, "include");
|
||||||
|
Directory.Delete(folderToDelete, recursive: true);
|
||||||
|
|
||||||
|
await _app.HasRestarted();
|
||||||
|
types = await _app.GetCompiledAppDefinedTypes();
|
||||||
|
Assert.Equal(1, types);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RenameCompiledFile()
|
||||||
|
{
|
||||||
|
await _app.StartWatcherAsync();
|
||||||
|
|
||||||
|
var oldFile = Path.Combine(_app.SourceDirectory, "include", "Foo.cs");
|
||||||
|
var newFile = Path.Combine(_app.SourceDirectory, "include", "Foo_new.cs");
|
||||||
|
File.Move(oldFile, newFile);
|
||||||
|
|
||||||
|
await _app.HasRestarted();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChangeExcludedFile()
|
||||||
|
{
|
||||||
|
await _app.StartWatcherAsync();
|
||||||
|
|
||||||
|
var changedFile = Path.Combine(_app.SourceDirectory, "exclude", "Baz.cs");
|
||||||
|
File.WriteAllText(changedFile, "");
|
||||||
|
|
||||||
|
var restart = _app.HasRestarted();
|
||||||
|
var finished = await Task.WhenAny(Task.Delay(TimeSpan.FromSeconds(10)), restart);
|
||||||
|
Assert.NotSame(restart, finished);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ListsFiles()
|
||||||
|
{
|
||||||
|
await _app.PrepareAsync();
|
||||||
|
_app.Start(new [] { "--list" });
|
||||||
|
var lines = await _app.Process.GetAllOutputLines();
|
||||||
|
|
||||||
|
AssertEx.EqualFileList(
|
||||||
|
_app.Scenario.WorkFolder,
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"GlobbingApp/Program.cs",
|
||||||
|
"GlobbingApp/include/Foo.cs",
|
||||||
|
"GlobbingApp/GlobbingApp.csproj",
|
||||||
|
},
|
||||||
|
lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_app.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GlobbingApp : WatchableApp
|
||||||
|
{
|
||||||
|
public GlobbingApp(ITestOutputHelper logger)
|
||||||
|
: base("GlobbingApp", logger)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetCompiledAppDefinedTypes()
|
||||||
|
{
|
||||||
|
var definedTypesMessage = await Process.GetOutputLineStartsWithAsync("Defined types = ", TimeSpan.FromSeconds(30));
|
||||||
|
return int.Parse(definedTypesMessage.Split('=').Last());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,289 @@
|
||||||
|
// 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.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
using Microsoft.DotNet.Watcher.Internal;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools.Tests
|
||||||
|
{
|
||||||
|
using ItemSpec = TemporaryCSharpProject.ItemSpec;
|
||||||
|
|
||||||
|
public class MsBuildFileSetFactoryTest : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IReporter _reporter;
|
||||||
|
private readonly TemporaryDirectory _tempDir;
|
||||||
|
public MsBuildFileSetFactoryTest(ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
_reporter = new TestReporter(output);
|
||||||
|
_tempDir = new TemporaryDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FindsCustomWatchItems()
|
||||||
|
{
|
||||||
|
_tempDir
|
||||||
|
.WithCSharpProject("Project1", out var target)
|
||||||
|
.WithTargetFrameworks("netcoreapp1.0")
|
||||||
|
.WithItem(new ItemSpec { Name = "Watch", Include = "*.js", Exclude = "gulpfile.js" })
|
||||||
|
.Dir()
|
||||||
|
.WithFile("Program.cs")
|
||||||
|
.WithFile("app.js")
|
||||||
|
.WithFile("gulpfile.js");
|
||||||
|
|
||||||
|
var fileset = await GetFileSet(target);
|
||||||
|
|
||||||
|
AssertEx.EqualFileList(
|
||||||
|
_tempDir.Root,
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"Project1.csproj",
|
||||||
|
"Program.cs",
|
||||||
|
"app.js"
|
||||||
|
},
|
||||||
|
fileset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExcludesDefaultItemsWithWatchFalseMetadata()
|
||||||
|
{
|
||||||
|
_tempDir
|
||||||
|
.WithCSharpProject("Project1", out var target)
|
||||||
|
.WithTargetFrameworks("net40")
|
||||||
|
.WithItem(new ItemSpec { Name = "EmbeddedResource", Update = "*.resx", Watch = false })
|
||||||
|
.Dir()
|
||||||
|
.WithFile("Program.cs")
|
||||||
|
.WithFile("Strings.resx");
|
||||||
|
|
||||||
|
var fileset = await GetFileSet(target);
|
||||||
|
|
||||||
|
AssertEx.EqualFileList(
|
||||||
|
_tempDir.Root,
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"Project1.csproj",
|
||||||
|
"Program.cs",
|
||||||
|
},
|
||||||
|
fileset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SingleTfm()
|
||||||
|
{
|
||||||
|
_tempDir
|
||||||
|
.SubDir("src")
|
||||||
|
.SubDir("Project1")
|
||||||
|
.WithCSharpProject("Project1", out var target)
|
||||||
|
.WithProperty("BaseIntermediateOutputPath", "obj")
|
||||||
|
.WithTargetFrameworks("netcoreapp1.0")
|
||||||
|
.Dir()
|
||||||
|
.WithFile("Program.cs")
|
||||||
|
.WithFile("Class1.cs")
|
||||||
|
.SubDir("obj").WithFile("ignored.cs").Up()
|
||||||
|
.SubDir("Properties").WithFile("Strings.resx").Up()
|
||||||
|
.Up()
|
||||||
|
.Up()
|
||||||
|
.Create();
|
||||||
|
|
||||||
|
var fileset = await GetFileSet(target);
|
||||||
|
|
||||||
|
AssertEx.EqualFileList(
|
||||||
|
_tempDir.Root,
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"src/Project1/Project1.csproj",
|
||||||
|
"src/Project1/Program.cs",
|
||||||
|
"src/Project1/Class1.cs",
|
||||||
|
"src/Project1/Properties/Strings.resx",
|
||||||
|
},
|
||||||
|
fileset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MultiTfm()
|
||||||
|
{
|
||||||
|
_tempDir
|
||||||
|
.SubDir("src")
|
||||||
|
.SubDir("Project1")
|
||||||
|
.WithCSharpProject("Project1", out var target)
|
||||||
|
.WithTargetFrameworks("netcoreapp1.0", "net451")
|
||||||
|
.WithProperty("EnableDefaultCompileItems", "false")
|
||||||
|
.WithItem("Compile", "Class1.netcore.cs", "'$(TargetFramework)'=='netcoreapp1.0'")
|
||||||
|
.WithItem("Compile", "Class1.desktop.cs", "'$(TargetFramework)'=='net451'")
|
||||||
|
.Dir()
|
||||||
|
.WithFile("Class1.netcore.cs")
|
||||||
|
.WithFile("Class1.desktop.cs")
|
||||||
|
.WithFile("Class1.notincluded.cs");
|
||||||
|
|
||||||
|
var fileset = await GetFileSet(target);
|
||||||
|
|
||||||
|
AssertEx.EqualFileList(
|
||||||
|
_tempDir.Root,
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"src/Project1/Project1.csproj",
|
||||||
|
"src/Project1/Class1.netcore.cs",
|
||||||
|
"src/Project1/Class1.desktop.cs",
|
||||||
|
},
|
||||||
|
fileset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProjectReferences_OneLevel()
|
||||||
|
{
|
||||||
|
_tempDir
|
||||||
|
.SubDir("src")
|
||||||
|
.SubDir("Project2")
|
||||||
|
.WithCSharpProject("Project2", out var proj2)
|
||||||
|
.WithTargetFrameworks("netstandard1.1")
|
||||||
|
.Dir()
|
||||||
|
.WithFile("Class2.cs")
|
||||||
|
.Up()
|
||||||
|
.SubDir("Project1")
|
||||||
|
.WithCSharpProject("Project1", out var target)
|
||||||
|
.WithTargetFrameworks("netcoreapp1.0", "net451")
|
||||||
|
.WithProjectReference(proj2)
|
||||||
|
.Dir()
|
||||||
|
.WithFile("Class1.cs");
|
||||||
|
|
||||||
|
var fileset = await GetFileSet(target);
|
||||||
|
|
||||||
|
AssertEx.EqualFileList(
|
||||||
|
_tempDir.Root,
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"src/Project2/Project2.csproj",
|
||||||
|
"src/Project2/Class2.cs",
|
||||||
|
"src/Project1/Project1.csproj",
|
||||||
|
"src/Project1/Class1.cs",
|
||||||
|
},
|
||||||
|
fileset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TransitiveProjectReferences_TwoLevels()
|
||||||
|
{
|
||||||
|
_tempDir
|
||||||
|
.SubDir("src")
|
||||||
|
.SubDir("Project3")
|
||||||
|
.WithCSharpProject("Project3", out var proj3)
|
||||||
|
.WithTargetFrameworks("netstandard1.0")
|
||||||
|
.Dir()
|
||||||
|
.WithFile("Class3.cs")
|
||||||
|
.Up()
|
||||||
|
.SubDir("Project2")
|
||||||
|
.WithCSharpProject("Project2", out TemporaryCSharpProject proj2)
|
||||||
|
.WithTargetFrameworks("netstandard1.1")
|
||||||
|
.WithProjectReference(proj3)
|
||||||
|
.Dir()
|
||||||
|
.WithFile("Class2.cs")
|
||||||
|
.Up()
|
||||||
|
.SubDir("Project1")
|
||||||
|
.WithCSharpProject("Project1", out TemporaryCSharpProject target)
|
||||||
|
.WithTargetFrameworks("netcoreapp1.0", "net451")
|
||||||
|
.WithProjectReference(proj2)
|
||||||
|
.Dir()
|
||||||
|
.WithFile("Class1.cs");
|
||||||
|
|
||||||
|
var fileset = await GetFileSet(target);
|
||||||
|
|
||||||
|
AssertEx.EqualFileList(
|
||||||
|
_tempDir.Root,
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"src/Project3/Project3.csproj",
|
||||||
|
"src/Project3/Class3.cs",
|
||||||
|
"src/Project2/Project2.csproj",
|
||||||
|
"src/Project2/Class2.cs",
|
||||||
|
"src/Project1/Project1.csproj",
|
||||||
|
"src/Project1/Class1.cs",
|
||||||
|
},
|
||||||
|
fileset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProjectReferences_Graph()
|
||||||
|
{
|
||||||
|
var graph = new TestProjectGraph(_tempDir);
|
||||||
|
graph.OnCreate(p => p.WithTargetFrameworks("net45"));
|
||||||
|
var matches = Regex.Matches(@"
|
||||||
|
A->B B->C C->D D->E
|
||||||
|
B->E
|
||||||
|
A->F F->G G->E
|
||||||
|
F->E
|
||||||
|
W->U
|
||||||
|
Y->Z
|
||||||
|
Y->B
|
||||||
|
Y->F",
|
||||||
|
@"(\w)->(\w)");
|
||||||
|
|
||||||
|
Assert.Equal(13, matches.Count);
|
||||||
|
foreach (Match m in matches)
|
||||||
|
{
|
||||||
|
var target = graph.GetOrCreate(m.Groups[2].Value);
|
||||||
|
graph.GetOrCreate(m.Groups[1].Value).WithProjectReference(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
graph.Find("A").WithProjectReference(graph.Find("W"), watch: false);
|
||||||
|
|
||||||
|
var output = new OutputSink();
|
||||||
|
var filesetFactory = new MsBuildFileSetFactory(_reporter, graph.GetOrCreate("A").Path, output, trace: true);
|
||||||
|
|
||||||
|
var fileset = await GetFileSet(filesetFactory);
|
||||||
|
|
||||||
|
_reporter.Output(string.Join(
|
||||||
|
Environment.NewLine,
|
||||||
|
output.Current.Lines.Select(l => "Sink output: " + l)));
|
||||||
|
|
||||||
|
var includedProjects = new[] { "A", "B", "C", "D", "E", "F", "G" };
|
||||||
|
AssertEx.EqualFileList(
|
||||||
|
_tempDir.Root,
|
||||||
|
includedProjects
|
||||||
|
.Select(p => $"{p}/{p}.csproj"),
|
||||||
|
fileset
|
||||||
|
);
|
||||||
|
|
||||||
|
// ensure unreachable projects exist but where not included
|
||||||
|
Assert.NotNull(graph.Find("W"));
|
||||||
|
Assert.NotNull(graph.Find("U"));
|
||||||
|
Assert.NotNull(graph.Find("Y"));
|
||||||
|
Assert.NotNull(graph.Find("Z"));
|
||||||
|
|
||||||
|
// ensure each project is only visited once for collecting watch items
|
||||||
|
Assert.All(includedProjects,
|
||||||
|
projectName =>
|
||||||
|
Assert.Single(output.Current.Lines,
|
||||||
|
line => line.Contains($"Collecting watch items from '{projectName}'"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<IFileSet> GetFileSet(TemporaryCSharpProject target)
|
||||||
|
=> GetFileSet(new MsBuildFileSetFactory(_reporter, target.Path, waitOnError: false, trace: false));
|
||||||
|
|
||||||
|
private async Task<IFileSet> GetFileSet(MsBuildFileSetFactory filesetFactory)
|
||||||
|
{
|
||||||
|
_tempDir.Create();
|
||||||
|
return await filesetFactory
|
||||||
|
.CreateAsync(CancellationToken.None)
|
||||||
|
.TimeoutAfter(TimeSpan.FromSeconds(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_tempDir.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
|
{
|
||||||
|
public class NoDepsAppTests : IDisposable
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
private readonly WatchableApp _app;
|
||||||
|
|
||||||
|
public NoDepsAppTests(ITestOutputHelper logger)
|
||||||
|
{
|
||||||
|
_app = new WatchableApp("NoDepsApp", logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RestartProcessOnFileChange()
|
||||||
|
{
|
||||||
|
await _app.StartWatcherAsync(new[] { "--no-exit" });
|
||||||
|
var pid = await _app.GetProcessId();
|
||||||
|
|
||||||
|
// Then wait for it to restart when we change a file
|
||||||
|
var fileToChange = Path.Combine(_app.SourceDirectory, "Program.cs");
|
||||||
|
var programCs = File.ReadAllText(fileToChange);
|
||||||
|
File.WriteAllText(fileToChange, programCs);
|
||||||
|
|
||||||
|
await _app.HasRestarted();
|
||||||
|
var pid2 = await _app.GetProcessId();
|
||||||
|
Assert.NotEqual(pid, pid2);
|
||||||
|
|
||||||
|
// first app should have shut down
|
||||||
|
Assert.Throws<ArgumentException>(() => Process.GetProcessById(pid));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RestartProcessThatTerminatesAfterFileChange()
|
||||||
|
{
|
||||||
|
await _app.StartWatcherAsync();
|
||||||
|
var pid = await _app.GetProcessId();
|
||||||
|
await _app.HasExited(); // process should exit after run
|
||||||
|
await _app.IsWaitingForFileChange();
|
||||||
|
|
||||||
|
var fileToChange = Path.Combine(_app.SourceDirectory, "Program.cs");
|
||||||
|
var programCs = File.ReadAllText(fileToChange);
|
||||||
|
File.WriteAllText(fileToChange, programCs);
|
||||||
|
|
||||||
|
await _app.HasRestarted();
|
||||||
|
var pid2 = await _app.GetProcessId();
|
||||||
|
Assert.NotEqual(pid, pid2);
|
||||||
|
await _app.HasExited(); // process should exit after run
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_app.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
// 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.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools.Tests
|
||||||
|
{
|
||||||
|
public class ProgramTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly TemporaryDirectory _tempDir;
|
||||||
|
private readonly TestConsole _console;
|
||||||
|
|
||||||
|
public ProgramTests(ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
_tempDir = new TemporaryDirectory();
|
||||||
|
_console = new TestConsole(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConsoleCancelKey()
|
||||||
|
{
|
||||||
|
_tempDir
|
||||||
|
.WithCSharpProject("testproj")
|
||||||
|
.WithTargetFrameworks("netcoreapp1.0")
|
||||||
|
.Dir()
|
||||||
|
.WithFile("Program.cs")
|
||||||
|
.Create();
|
||||||
|
|
||||||
|
var stdout = new StringBuilder();
|
||||||
|
_console.Out = new StringWriter(stdout);
|
||||||
|
var program = new Program(_console, _tempDir.Root)
|
||||||
|
.RunAsync(new[] { "run" });
|
||||||
|
|
||||||
|
await _console.CancelKeyPressSubscribed.TimeoutAfter(TimeSpan.FromSeconds(30));
|
||||||
|
_console.ConsoleCancelKey();
|
||||||
|
|
||||||
|
var exitCode = await program.TimeoutAfter(TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
|
Assert.Contains("Shutdown requested. Press Ctrl+C again to force exit.", stdout.ToString());
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_tempDir.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.Internal;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
|
{
|
||||||
|
public class ProjectToolScenario : IDisposable
|
||||||
|
{
|
||||||
|
private static readonly string TestProjectSourceRoot = Path.Combine(AppContext.BaseDirectory, "TestProjects");
|
||||||
|
private readonly ITestOutputHelper _logger;
|
||||||
|
|
||||||
|
public ProjectToolScenario()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectToolScenario(ITestOutputHelper logger)
|
||||||
|
{
|
||||||
|
WorkFolder = Path.Combine(AppContext.BaseDirectory, "tmp", Path.GetRandomFileName());
|
||||||
|
DotNetWatchPath = Path.Combine(AppContext.BaseDirectory, "tool", "dotnet-watch.dll");
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
|
_logger?.WriteLine($"The temporary test folder is {WorkFolder}");
|
||||||
|
|
||||||
|
CreateTestDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string WorkFolder { get; }
|
||||||
|
|
||||||
|
public string DotNetWatchPath { get; }
|
||||||
|
|
||||||
|
public void AddTestProjectFolder(string projectName)
|
||||||
|
{
|
||||||
|
var srcFolder = Path.Combine(TestProjectSourceRoot, projectName);
|
||||||
|
var destinationFolder = Path.Combine(WorkFolder, Path.GetFileName(projectName));
|
||||||
|
_logger?.WriteLine($"Copying project {srcFolder} to {destinationFolder}");
|
||||||
|
|
||||||
|
Directory.CreateDirectory(destinationFolder);
|
||||||
|
|
||||||
|
foreach (var directory in Directory.GetDirectories(srcFolder, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory.Replace(srcFolder, destinationFolder));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var file in Directory.GetFiles(srcFolder, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
File.Copy(file, file.Replace(srcFolder, destinationFolder), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RestoreAsync(string project)
|
||||||
|
{
|
||||||
|
_logger?.WriteLine($"Restoring msbuild project in {project}");
|
||||||
|
return ExecuteCommandAsync(project, TimeSpan.FromSeconds(120), "restore");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task BuildAsync(string project)
|
||||||
|
{
|
||||||
|
_logger?.WriteLine($"Building {project}");
|
||||||
|
return ExecuteCommandAsync(project, TimeSpan.FromSeconds(60), "build");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteCommandAsync(string project, TimeSpan timeout, params string[] arguments)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<object>();
|
||||||
|
project = Path.Combine(WorkFolder, project);
|
||||||
|
_logger?.WriteLine($"Project directory: '{project}'");
|
||||||
|
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
EnableRaisingEvents = true,
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = DotNetMuxer.MuxerPathOrDefault(),
|
||||||
|
Arguments = ArgumentEscaper.EscapeAndConcatenate(arguments),
|
||||||
|
WorkingDirectory = project,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
Environment =
|
||||||
|
{
|
||||||
|
["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
void OnData(object sender, DataReceivedEventArgs args)
|
||||||
|
=> _logger?.WriteLine(args.Data ?? string.Empty);
|
||||||
|
|
||||||
|
void OnExit(object sender, EventArgs args)
|
||||||
|
{
|
||||||
|
_logger?.WriteLine($"Process exited {process.Id}");
|
||||||
|
tcs.TrySetResult(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.ErrorDataReceived += OnData;
|
||||||
|
process.OutputDataReceived += OnData;
|
||||||
|
process.Exited += OnExit;
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
process.BeginErrorReadLine();
|
||||||
|
process.BeginOutputReadLine();
|
||||||
|
|
||||||
|
_logger?.WriteLine($"Started process {process.Id}: {process.StartInfo.FileName} {process.StartInfo.Arguments}");
|
||||||
|
|
||||||
|
var done = await Task.WhenAny(tcs.Task, Task.Delay(timeout));
|
||||||
|
process.CancelErrorRead();
|
||||||
|
process.CancelOutputRead();
|
||||||
|
|
||||||
|
process.ErrorDataReceived -= OnData;
|
||||||
|
process.OutputDataReceived -= OnData;
|
||||||
|
process.Exited -= OnExit;
|
||||||
|
|
||||||
|
if (!ReferenceEquals(done, tcs.Task))
|
||||||
|
{
|
||||||
|
if (!process.HasExited)
|
||||||
|
{
|
||||||
|
_logger?.WriteLine($"Killing process {process.Id}");
|
||||||
|
process.KillTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TimeoutException($"Process timed out after {timeout.TotalSeconds} seconds");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger?.WriteLine($"Process exited {process.Id} with code {process.ExitCode}");
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Exit code {process.ExitCode}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateTestDirectory()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(WorkFolder);
|
||||||
|
|
||||||
|
File.WriteAllText(Path.Combine(WorkFolder, "Directory.Build.props"), "<Project />");
|
||||||
|
|
||||||
|
var restoreSources = GetMetadata("TestSettings:RestoreSources");
|
||||||
|
var frameworkVersion = GetMetadata("TestSettings:RuntimeFrameworkVersion");
|
||||||
|
|
||||||
|
var dbTargets = new XDocument(
|
||||||
|
new XElement("Project",
|
||||||
|
new XElement("PropertyGroup",
|
||||||
|
new XElement("RuntimeFrameworkVersion", frameworkVersion),
|
||||||
|
new XElement("RestoreSources", restoreSources))));
|
||||||
|
dbTargets.Save(Path.Combine(WorkFolder, "Directory.Build.targets"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetMetadata(string key)
|
||||||
|
{
|
||||||
|
return typeof(ProjectToolScenario)
|
||||||
|
.Assembly
|
||||||
|
.GetCustomAttributes<AssemblyMetadataAttribute>()
|
||||||
|
.First(a => string.Equals(a.Key, key, StringComparison.Ordinal))
|
||||||
|
.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(WorkFolder, recursive: true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to delete {WorkFolder}. Retrying...");
|
||||||
|
Thread.Sleep(TimeSpan.FromSeconds(5));
|
||||||
|
Directory.Delete(WorkFolder, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
// 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.CompilerServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
|
{
|
||||||
|
public class WatchableApp : IDisposable
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan DefaultMessageTimeOut = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
private const string StartedMessage = "Started";
|
||||||
|
private const string ExitingMessage = "Exiting";
|
||||||
|
private const string WatchExitedMessage = "watch : Exited";
|
||||||
|
private const string WaitingForFileChangeMessage = "watch : Waiting for a file to change";
|
||||||
|
|
||||||
|
private readonly ITestOutputHelper _logger;
|
||||||
|
private string _appName;
|
||||||
|
private bool _prepared;
|
||||||
|
|
||||||
|
|
||||||
|
public WatchableApp(string appName, ITestOutputHelper logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_appName = appName;
|
||||||
|
Scenario = new ProjectToolScenario(logger);
|
||||||
|
Scenario.AddTestProjectFolder(appName);
|
||||||
|
SourceDirectory = Path.Combine(Scenario.WorkFolder, appName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectToolScenario Scenario { get; }
|
||||||
|
|
||||||
|
public AwaitableProcess Process { get; protected set; }
|
||||||
|
|
||||||
|
public string SourceDirectory { get; }
|
||||||
|
|
||||||
|
public Task HasRestarted()
|
||||||
|
=> Process.GetOutputLineAsync(StartedMessage, DefaultMessageTimeOut);
|
||||||
|
|
||||||
|
public async Task HasExited()
|
||||||
|
{
|
||||||
|
await Process.GetOutputLineAsync(ExitingMessage, DefaultMessageTimeOut);
|
||||||
|
await Process.GetOutputLineAsync(WatchExitedMessage, DefaultMessageTimeOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task IsWaitingForFileChange()
|
||||||
|
{
|
||||||
|
await Process.GetOutputLineStartsWithAsync(WaitingForFileChangeMessage, DefaultMessageTimeOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UsePollingWatcher { get; set; }
|
||||||
|
|
||||||
|
public async Task<int> GetProcessId()
|
||||||
|
{
|
||||||
|
var line = await Process.GetOutputLineStartsWithAsync("PID =", DefaultMessageTimeOut);
|
||||||
|
var pid = line.Split('=').Last();
|
||||||
|
return int.Parse(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PrepareAsync()
|
||||||
|
{
|
||||||
|
await Scenario.RestoreAsync(_appName);
|
||||||
|
await Scenario.BuildAsync(_appName);
|
||||||
|
_prepared = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start(IEnumerable<string> arguments, [CallerMemberName] string name = null)
|
||||||
|
{
|
||||||
|
if (!_prepared)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Call {nameof(PrepareAsync)} first");
|
||||||
|
}
|
||||||
|
|
||||||
|
var args = new List<string>
|
||||||
|
{
|
||||||
|
Scenario.DotNetWatchPath,
|
||||||
|
};
|
||||||
|
args.AddRange(arguments);
|
||||||
|
|
||||||
|
var spec = new ProcessSpec
|
||||||
|
{
|
||||||
|
Executable = DotNetMuxer.MuxerPathOrDefault(),
|
||||||
|
Arguments = args,
|
||||||
|
WorkingDirectory = SourceDirectory,
|
||||||
|
EnvironmentVariables =
|
||||||
|
{
|
||||||
|
["DOTNET_CLI_CONTEXT_VERBOSE"] = bool.TrueString,
|
||||||
|
["DOTNET_USE_POLLING_FILE_WATCHER"] = UsePollingWatcher.ToString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Process = new AwaitableProcess(spec, _logger);
|
||||||
|
Process.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartWatcherAsync([CallerMemberName] string name = null)
|
||||||
|
=> StartWatcherAsync(Array.Empty<string>(), name);
|
||||||
|
|
||||||
|
public async Task StartWatcherAsync(string[] arguments, [CallerMemberName] string name = null)
|
||||||
|
{
|
||||||
|
if (!_prepared)
|
||||||
|
{
|
||||||
|
await PrepareAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var args = new[] { "run", "--" }.Concat(arguments);
|
||||||
|
Start(args, name);
|
||||||
|
|
||||||
|
// Make this timeout long because it depends much on the MSBuild compilation speed.
|
||||||
|
// Slow machines may take a bit to compile and boot test apps
|
||||||
|
await Process.GetOutputLineAsync(StartedMessage, TimeSpan.FromMinutes(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Dispose()
|
||||||
|
{
|
||||||
|
_logger?.WriteLine("Disposing WatchableApp");
|
||||||
|
Process?.Dispose();
|
||||||
|
Scenario?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<OutputType>exe</OutputType>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Dependency\Dependency.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
// 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.Threading;
|
||||||
|
|
||||||
|
namespace ConsoleApplication
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
private static readonly int processId = Process.GetCurrentProcess().Id;
|
||||||
|
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Started");
|
||||||
|
Console.WriteLine($"PID = " + Process.GetCurrentProcess().Id);
|
||||||
|
Thread.Sleep(Timeout.Infinite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Dependency
|
||||||
|
{
|
||||||
|
public class Foo
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<OutputType>exe</OutputType>
|
||||||
|
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Program.cs;include\*.cs" Exclude="exclude\*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
// 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.Reflection;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace ConsoleApplication
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Started");
|
||||||
|
Console.WriteLine("PID = " + Process.GetCurrentProcess().Id);
|
||||||
|
Console.WriteLine("Defined types = " + typeof(Program).GetTypeInfo().Assembly.DefinedTypes.Count());
|
||||||
|
Thread.Sleep(Timeout.Infinite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace GlobbingApp.exclude
|
||||||
|
{
|
||||||
|
public class Baz
|
||||||
|
{
|
||||||
|
"THIS FILE SHOULD NOT BE INCLUDED IN COMPILATION"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace GlobbingApp.include
|
||||||
|
{
|
||||||
|
public class Foo
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<Project>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<BaseIntermediateOutputPath>.net/obj</BaseIntermediateOutputPath>
|
||||||
|
<BaseOutputPath>.net/bin</BaseOutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -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;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace KitchenSink
|
||||||
|
{
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Started");
|
||||||
|
Console.WriteLine("PID = " + Process.GetCurrentProcess().Id);
|
||||||
|
Console.WriteLine("DOTNET_WATCH = " + Environment.GetEnvironmentVariable("DOTNET_WATCH"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<OutputType>exe</OutputType>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace ConsoleApplication
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Started");
|
||||||
|
Console.WriteLine($"PID = " + Process.GetCurrentProcess().Id);
|
||||||
|
if (args.Length > 0 && args[0] == "--no-exit")
|
||||||
|
{
|
||||||
|
Thread.Sleep(Timeout.Infinite);
|
||||||
|
}
|
||||||
|
Console.WriteLine("Exiting");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools.Tests
|
||||||
|
{
|
||||||
|
public class TemporaryCSharpProject
|
||||||
|
{
|
||||||
|
private const string Template =
|
||||||
|
@"<Project Sdk=""Microsoft.NET.Sdk"">
|
||||||
|
<PropertyGroup>
|
||||||
|
{0}
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
{1}
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>";
|
||||||
|
|
||||||
|
private readonly string _filename;
|
||||||
|
private readonly TemporaryDirectory _directory;
|
||||||
|
private List<string> _items = new List<string>();
|
||||||
|
private List<string> _properties = new List<string>();
|
||||||
|
|
||||||
|
public TemporaryCSharpProject(string name, TemporaryDirectory directory)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
_filename = name + ".csproj";
|
||||||
|
_directory = directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
public string Path => System.IO.Path.Combine(_directory.Root, _filename);
|
||||||
|
|
||||||
|
public TemporaryCSharpProject WithTargetFrameworks(params string[] tfms)
|
||||||
|
{
|
||||||
|
Debug.Assert(tfms.Length > 0);
|
||||||
|
var propertySpec = new PropertySpec
|
||||||
|
{
|
||||||
|
Value = string.Join(";", tfms)
|
||||||
|
};
|
||||||
|
propertySpec.Name = tfms.Length == 1
|
||||||
|
? "TargetFramework"
|
||||||
|
: "TargetFrameworks";
|
||||||
|
|
||||||
|
return WithProperty(propertySpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemporaryCSharpProject WithProperty(string name, string value)
|
||||||
|
=> WithProperty(new PropertySpec { Name = name, Value = value });
|
||||||
|
|
||||||
|
public TemporaryCSharpProject WithProperty(PropertySpec property)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.Append('<').Append(property.Name).Append('>')
|
||||||
|
.Append(property.Value)
|
||||||
|
.Append("</").Append(property.Name).Append('>');
|
||||||
|
_properties.Add(sb.ToString());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemporaryCSharpProject WithItem(string itemName, string include, string condition = null)
|
||||||
|
=> WithItem(new ItemSpec { Name = itemName, Include = include, Condition = condition });
|
||||||
|
|
||||||
|
public TemporaryCSharpProject WithItem(ItemSpec item)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder("<");
|
||||||
|
sb.Append(item.Name).Append(" ");
|
||||||
|
if (item.Include != null) sb.Append(" Include=\"").Append(item.Include).Append('"');
|
||||||
|
if (item.Remove != null) sb.Append(" Remove=\"").Append(item.Remove).Append('"');
|
||||||
|
if (item.Update != null) sb.Append(" Update=\"").Append(item.Update).Append('"');
|
||||||
|
if (item.Exclude != null) sb.Append(" Exclude=\"").Append(item.Exclude).Append('"');
|
||||||
|
if (item.Condition != null) sb.Append(" Exclude=\"").Append(item.Condition).Append('"');
|
||||||
|
if (!item.Watch) sb.Append(" Watch=\"false\" ");
|
||||||
|
sb.Append(" />");
|
||||||
|
_items.Add(sb.ToString());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemporaryCSharpProject WithProjectReference(TemporaryCSharpProject reference, bool watch = true)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(this, reference))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Can add project reference to self");
|
||||||
|
}
|
||||||
|
|
||||||
|
return WithItem(new ItemSpec { Name = "ProjectReference", Include = reference.Path, Watch = watch });
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemporaryDirectory Dir() => _directory;
|
||||||
|
|
||||||
|
public void Create()
|
||||||
|
{
|
||||||
|
_directory.CreateFile(_filename, string.Format(Template, string.Join("\r\n", _properties), string.Join("\r\n", _items)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ItemSpec
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Include { get; set; }
|
||||||
|
public string Exclude { get; set; }
|
||||||
|
public string Update { get; set; }
|
||||||
|
public string Remove { get; set; }
|
||||||
|
public bool Watch { get; set; } = true;
|
||||||
|
public string Condition { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PropertySpec
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools.Tests
|
||||||
|
{
|
||||||
|
public class TemporaryDirectory : IDisposable
|
||||||
|
{
|
||||||
|
private List<TemporaryCSharpProject> _projects = new List<TemporaryCSharpProject>();
|
||||||
|
private List<TemporaryDirectory> _subdirs = new List<TemporaryDirectory>();
|
||||||
|
private Dictionary<string, string> _files = new Dictionary<string, string>();
|
||||||
|
private TemporaryDirectory _parent;
|
||||||
|
|
||||||
|
public TemporaryDirectory()
|
||||||
|
{
|
||||||
|
Root = Path.Combine(Path.GetTempPath(), "dotnet-watch-tests", Guid.NewGuid().ToString("N"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TemporaryDirectory(string path, TemporaryDirectory parent)
|
||||||
|
{
|
||||||
|
_parent = parent;
|
||||||
|
Root = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemporaryDirectory SubDir(string name)
|
||||||
|
{
|
||||||
|
var subdir = new TemporaryDirectory(Path.Combine(Root, name), this);
|
||||||
|
_subdirs.Add(subdir);
|
||||||
|
return subdir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Root { get; }
|
||||||
|
|
||||||
|
public TemporaryCSharpProject WithCSharpProject(string name)
|
||||||
|
{
|
||||||
|
var project = new TemporaryCSharpProject(name, this);
|
||||||
|
_projects.Add(project);
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemporaryCSharpProject WithCSharpProject(string name, out TemporaryCSharpProject project)
|
||||||
|
{
|
||||||
|
project = WithCSharpProject(name);
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemporaryDirectory WithFile(string name, string contents = "")
|
||||||
|
{
|
||||||
|
_files[name] = contents;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemporaryDirectory Up()
|
||||||
|
{
|
||||||
|
if (_parent == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("This is the root directory");
|
||||||
|
}
|
||||||
|
return _parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Create()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Root);
|
||||||
|
|
||||||
|
foreach (var dir in _subdirs)
|
||||||
|
{
|
||||||
|
dir.Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var project in _projects)
|
||||||
|
{
|
||||||
|
project.Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var file in _files)
|
||||||
|
{
|
||||||
|
CreateFile(file.Key, file.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CreateFile(string filename, string contents)
|
||||||
|
{
|
||||||
|
File.WriteAllText(Path.Combine(Root, filename), contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Root == null || !Directory.Exists(Root) || _parent != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(Root, recursive: true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Test cleanup failed to delete '{Root}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.DotNet.Watcher.Tools.Tests
|
||||||
|
{
|
||||||
|
public class TestProjectGraph
|
||||||
|
{
|
||||||
|
private readonly TemporaryDirectory _directory;
|
||||||
|
private Action<TemporaryCSharpProject> _onCreate;
|
||||||
|
private Dictionary<string, TemporaryCSharpProject> _projects = new Dictionary<string, TemporaryCSharpProject>();
|
||||||
|
public TestProjectGraph(TemporaryDirectory directory)
|
||||||
|
{
|
||||||
|
_directory = directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnCreate(Action<TemporaryCSharpProject> onCreate)
|
||||||
|
{
|
||||||
|
_onCreate = onCreate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemporaryCSharpProject Find(string projectName)
|
||||||
|
=> _projects.ContainsKey(projectName)
|
||||||
|
? _projects[projectName]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
public TemporaryCSharpProject GetOrCreate(string projectName)
|
||||||
|
{
|
||||||
|
TemporaryCSharpProject sourceProj;
|
||||||
|
if (!_projects.TryGetValue(projectName, out sourceProj))
|
||||||
|
{
|
||||||
|
sourceProj = _directory.SubDir(projectName).WithCSharpProject(projectName);
|
||||||
|
_onCreate?.Invoke(sourceProj);
|
||||||
|
_projects.Add(projectName, sourceProj);
|
||||||
|
}
|
||||||
|
return sourceProj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<AssemblyName>Microsoft.DotNet.Watcher.Tools.Tests</AssemblyName>
|
||||||
|
<DefaultItemExcludes>$(DefaultItemExcludes);TestProjects\**\*</DefaultItemExcludes>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\..\shared\test\**\*.cs" />
|
||||||
|
<Content Include="TestProjects\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\src\dotnet-watch.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||||
|
<_Parameter1>TestSettings:RestoreSources</_Parameter1>
|
||||||
|
<_Parameter2>$(RestoreSources)</_Parameter2>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||||
|
<_Parameter1>TestSettings:RuntimeFrameworkVersion</_Parameter1>
|
||||||
|
<_Parameter2>$(RuntimeFrameworkVersion)</_Parameter2>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target Name="CleanTestProjects" BeforeTargets="CoreCompile">
|
||||||
|
<RemoveDir Directories="$(TargetDir)TestProjects" Condition="Exists('$(TargetDir)TestProjects')" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="PublishDotNetWatch" BeforeTargets="Build">
|
||||||
|
<MSBuild Projects="..\src\dotnet-watch.csproj"
|
||||||
|
Targets="Publish"
|
||||||
|
Properties="PublishDir=$(OutputPath)\tool\;Configuration=$(Configuration)" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
// 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.Extensions.Tools.Internal
|
||||||
|
{
|
||||||
|
public static class CliContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// dotnet --verbose subcommand
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static bool IsGlobalVerbose()
|
||||||
|
{
|
||||||
|
bool globalVerbose;
|
||||||
|
bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE"), out globalVerbose);
|
||||||
|
return globalVerbose;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue