Merge source code from aspnet/DotNetTools into this repo

This commit is contained in:
Nate McMaster 2018-11-13 15:36:59 -08:00
commit b3ad3aa989
No known key found for this signature in database
GPG Key ID: A778D9601BD78810
101 changed files with 7214 additions and 0 deletions

View File

@ -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"
]
}
}

View File

@ -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>

View File

@ -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));
}
}
}

View File

@ -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>

32
src/Tools/README.md Normal file
View File

@ -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
```

View File

@ -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.

View File

@ -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;
}
}
}
}

View File

@ -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>

View File

@ -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.

View File

@ -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);
}
}
}
}

View File

@ -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("'", "''");
}
}
}

View File

@ -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>

View File

@ -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.

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}
}

View File

@ -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;
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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")]

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -0,0 +1,5 @@
<Project>
<Target Name="_ExtractUserSecretsMetadata">
<WriteLinesToFile File="$(_UserSecretsMetadataFile)" Lines="$(UserSecretsId)" />
</Target>
</Project>

View File

@ -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>

View File

@ -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));
}
}
}

View File

@ -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());
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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>

View File

@ -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>
```

View File

@ -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(),
};
}
}
}

View File

@ -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}");
}
}
}
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}
}

View File

@ -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();
}
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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")]

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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; }
}
}
}

View File

@ -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");
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}
}

View File

@ -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)
{
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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());
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>

View File

@ -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
{
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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"
}
}

View File

@ -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
{
}
}

View File

@ -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>

View File

@ -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"));
}
}
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<OutputType>exe</OutputType>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>

View File

@ -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");
}
}
}

View File

@ -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; }
}
}
}

View File

@ -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}'");
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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)
{ }
}
}

View File

@ -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();
}
}

View File

@ -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