Merge source code from aspnet/DotNetTools into this repo
This commit is contained in:
commit
b3ad3aa989
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<Project>
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.targets))\Directory.Build.targets" />
|
||||
|
||||
<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\**\*.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,44 @@
|
|||
<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>
|
||||
<Reference Include="Microsoft.Extensions.Process.Sources" />
|
||||
<Reference Include="Microsoft.Extensions.CommandLineUtils.Sources" />
|
||||
</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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// 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.Reflection;
|
||||
|
||||
namespace Microsoft.Extensions.CommandLineUtils
|
||||
{
|
||||
internal static class CommandLineApplicationExtensions
|
||||
{
|
||||
public static CommandOption HelpOption(this CommandLineApplication app)
|
||||
=> app.HelpOption("-?|-h|--help");
|
||||
|
||||
public static CommandOption VerboseOption(this CommandLineApplication app)
|
||||
=> app.Option("-v|--verbose", "Show verbose output", CommandOptionType.NoValue, inherited: true);
|
||||
|
||||
public static void OnExecute(this CommandLineApplication app, Action action)
|
||||
=> app.OnExecute(() =>
|
||||
{
|
||||
action();
|
||||
return 0;
|
||||
});
|
||||
|
||||
public static void VersionOptionFromAssemblyAttributes(this CommandLineApplication app, Assembly assembly)
|
||||
=> app.VersionOption("--version", GetInformationalVersion(assembly));
|
||||
|
||||
private static string GetInformationalVersion(Assembly assembly)
|
||||
{
|
||||
var attribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
|
||||
|
||||
var versionAttribute = attribute == null
|
||||
? assembly.GetName().Version.ToString()
|
||||
: attribute.InformationalVersion;
|
||||
|
||||
return versionAttribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.Extensions.Tools.Internal
|
||||
{
|
||||
public class ConsoleReporter : IReporter
|
||||
{
|
||||
private object _writeLock = new object();
|
||||
|
||||
public ConsoleReporter(IConsole console)
|
||||
: this(console, verbose: false, quiet: false)
|
||||
{ }
|
||||
|
||||
public ConsoleReporter(IConsole console, bool verbose, bool quiet)
|
||||
{
|
||||
Ensure.NotNull(console, nameof(console));
|
||||
|
||||
Console = console;
|
||||
IsVerbose = verbose;
|
||||
IsQuiet = quiet;
|
||||
}
|
||||
|
||||
protected IConsole Console { get; }
|
||||
public bool IsVerbose { get; set; }
|
||||
public bool IsQuiet { get; set; }
|
||||
|
||||
protected virtual void WriteLine(TextWriter writer, string message, ConsoleColor? color)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
if (color.HasValue)
|
||||
{
|
||||
Console.ForegroundColor = color.Value;
|
||||
}
|
||||
|
||||
writer.WriteLine(message);
|
||||
|
||||
if (color.HasValue)
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Error(string message)
|
||||
=> WriteLine(Console.Error, message, ConsoleColor.Red);
|
||||
public virtual void Warn(string message)
|
||||
=> WriteLine(Console.Out, message, ConsoleColor.Yellow);
|
||||
|
||||
public virtual void Output(string message)
|
||||
{
|
||||
if (IsQuiet)
|
||||
{
|
||||
return;
|
||||
}
|
||||
WriteLine(Console.Out, message, color: null);
|
||||
}
|
||||
|
||||
public virtual void Verbose(string message)
|
||||
{
|
||||
if (!IsVerbose)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WriteLine(Console.Out, message, ConsoleColor.DarkGray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Extensions.Tools.Internal
|
||||
{
|
||||
public static class DebugHelper
|
||||
{
|
||||
[Conditional("DEBUG")]
|
||||
public static void HandleDebugSwitch(ref string[] args)
|
||||
{
|
||||
if (args.Length > 0 && string.Equals("--debug", args[0], StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args = args.Skip(1).ToArray();
|
||||
Console.WriteLine("Waiting for debugger to attach. Press ENTER to continue");
|
||||
Console.WriteLine($"Process ID: {Process.GetCurrentProcess().Id}");
|
||||
Console.ReadLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
namespace Microsoft.Extensions.Tools.Internal
|
||||
{
|
||||
internal static class Ensure
|
||||
{
|
||||
public static T NotNull<T>(T obj, string paramName)
|
||||
where T : class
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
throw new ArgumentNullException(paramName);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static string NotNullOrEmpty(string obj, string paramName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(obj))
|
||||
{
|
||||
throw new ArgumentException("Value cannot be null or an empty string.", paramName);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.IO;
|
||||
|
||||
namespace Microsoft.Extensions.Tools.Internal
|
||||
{
|
||||
public interface IConsole
|
||||
{
|
||||
event ConsoleCancelEventHandler CancelKeyPress;
|
||||
TextWriter Out { get; }
|
||||
TextWriter Error { get; }
|
||||
TextReader In { get; }
|
||||
bool IsInputRedirected { get; }
|
||||
bool IsOutputRedirected { get; }
|
||||
bool IsErrorRedirected { get; }
|
||||
ConsoleColor ForegroundColor { get; set; }
|
||||
void ResetColor();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
namespace Microsoft.Extensions.Tools.Internal
|
||||
{
|
||||
public interface IReporter
|
||||
{
|
||||
void Verbose(string message);
|
||||
void Output(string message);
|
||||
void Warn(string message);
|
||||
void Error(string message);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// 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.Tools.Internal
|
||||
{
|
||||
public class NullReporter : IReporter
|
||||
{
|
||||
private NullReporter()
|
||||
{ }
|
||||
|
||||
public static IReporter Singleton { get; } = new NullReporter();
|
||||
|
||||
public void Verbose(string message)
|
||||
{ }
|
||||
|
||||
public void Output(string message)
|
||||
{ }
|
||||
|
||||
public void Warn(string message)
|
||||
{ }
|
||||
|
||||
public void Error(string message)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.Extensions.Tools.Internal
|
||||
{
|
||||
public class PhysicalConsole : IConsole
|
||||
{
|
||||
private PhysicalConsole()
|
||||
{
|
||||
Console.CancelKeyPress += (o, e) =>
|
||||
{
|
||||
CancelKeyPress?.Invoke(o, e);
|
||||
};
|
||||
}
|
||||
|
||||
public static IConsole Singleton { get; } = new PhysicalConsole();
|
||||
|
||||
public event ConsoleCancelEventHandler CancelKeyPress;
|
||||
public TextWriter Error => Console.Error;
|
||||
public TextReader In => Console.In;
|
||||
public TextWriter Out => Console.Out;
|
||||
public bool IsInputRedirected => Console.IsInputRedirected;
|
||||
public bool IsOutputRedirected => Console.IsOutputRedirected;
|
||||
public bool IsErrorRedirected => Console.IsErrorRedirected;
|
||||
public ConsoleColor ForegroundColor
|
||||
{
|
||||
get => Console.ForegroundColor;
|
||||
set => Console.ForegroundColor = value;
|
||||
}
|
||||
|
||||
public void ResetColor() => Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
// 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.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Extensions.Tools.Internal
|
||||
{
|
||||
public class TestConsole : IConsole
|
||||
{
|
||||
private event ConsoleCancelEventHandler _cancelKeyPress;
|
||||
private readonly TaskCompletionSource<bool> _cancelKeySubscribed = new TaskCompletionSource<bool>();
|
||||
|
||||
public TestConsole(ITestOutputHelper output)
|
||||
{
|
||||
var writer = new TestOutputWriter(output);
|
||||
Error = writer;
|
||||
Out = writer;
|
||||
}
|
||||
|
||||
public event ConsoleCancelEventHandler CancelKeyPress
|
||||
{
|
||||
add
|
||||
{
|
||||
_cancelKeyPress += value;
|
||||
_cancelKeySubscribed.TrySetResult(true);
|
||||
}
|
||||
remove => _cancelKeyPress -= value;
|
||||
}
|
||||
|
||||
public Task CancelKeyPressSubscribed => _cancelKeySubscribed.Task;
|
||||
|
||||
public TextWriter Error { get; set; }
|
||||
public TextWriter Out { get; set; }
|
||||
public TextReader In { get; set; } = new StringReader(string.Empty);
|
||||
public bool IsInputRedirected { get; set; } = false;
|
||||
public bool IsOutputRedirected { get; } = false;
|
||||
public bool IsErrorRedirected { get; } = false;
|
||||
public ConsoleColor ForegroundColor { get; set; }
|
||||
|
||||
public ConsoleCancelEventArgs ConsoleCancelKey()
|
||||
{
|
||||
var ctor = typeof(ConsoleCancelEventArgs)
|
||||
.GetTypeInfo()
|
||||
.DeclaredConstructors
|
||||
.Single(c => c.GetParameters().First().ParameterType == typeof(ConsoleSpecialKey));
|
||||
var args = (ConsoleCancelEventArgs)ctor.Invoke(new object[] { ConsoleSpecialKey.ControlC });
|
||||
_cancelKeyPress.Invoke(this, args);
|
||||
return args;
|
||||
}
|
||||
|
||||
public void ResetColor()
|
||||
{
|
||||
}
|
||||
|
||||
private class TestOutputWriter : TextWriter
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
private readonly StringBuilder _sb = new StringBuilder();
|
||||
|
||||
public TestOutputWriter(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
public override Encoding Encoding => Encoding.Unicode;
|
||||
|
||||
public override void Write(char value)
|
||||
{
|
||||
if (value == '\r' || value == '\n')
|
||||
{
|
||||
if (_sb.Length > 0)
|
||||
{
|
||||
_output.WriteLine(_sb.ToString());
|
||||
_sb.Clear();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_sb.Append(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue