Create console reporter API

The reporter API breaks down complex formatter in to composable compontents. Adds 'ReporterBuilder' and 'FormatterBuilder' as syntax sugar for creating reporters with complex formatting rules

Changes in dotnet-watch
 - Remove dependency on Microsoft.Extensions.Logging and instead use IReporter for console output
 - Only use color output when stdout and stderr are not being redirected
 - Make the default output less noisy

Changes in dotnet-user-secrets
 - Remove dependency on Microsoft.Extensions.Logging to use IReporter

Changes in dotnet-sql-cache
 - Remove dependency on Microsoft.Extensions.Logging to use IReporter
 - Add --verbose option
This commit is contained in:
Nate McMaster 2016-11-23 12:15:23 -08:00
parent e0f85971a3
commit 17da5242e0
No known key found for this signature in database
GPG Key ID: BD729980AA6A21BD
53 changed files with 978 additions and 694 deletions

View File

@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14 # Visual Studio 14
VisualStudioVersion = 14.0.25420.1 VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
@ -28,6 +28,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{59E02BDF
EndProject EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "NuGetPackager", "tools\NuGetPackager\NuGetPackager.xproj", "{8B781D87-1FC3-4A34-9089-2BDF6B562B85}" Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "NuGetPackager", "tools\NuGetPackager\NuGetPackager.xproj", "{8B781D87-1FC3-4A34-9089-2BDF6B562B85}"
EndProject EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.Tools.Tests", "test\Microsoft.Extensions.Tools.Tests\Microsoft.Extensions.Tools.Tests.xproj", "{A24BF1D1-4326-4455-A528-09F1E20EDC83}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -62,6 +64,10 @@ Global
{8B781D87-1FC3-4A34-9089-2BDF6B562B85}.Debug|Any CPU.Build.0 = Debug|Any CPU {8B781D87-1FC3-4A34-9089-2BDF6B562B85}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B781D87-1FC3-4A34-9089-2BDF6B562B85}.Release|Any CPU.ActiveCfg = Release|Any CPU {8B781D87-1FC3-4A34-9089-2BDF6B562B85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B781D87-1FC3-4A34-9089-2BDF6B562B85}.Release|Any CPU.Build.0 = Release|Any CPU {8B781D87-1FC3-4A34-9089-2BDF6B562B85}.Release|Any CPU.Build.0 = Release|Any CPU
{A24BF1D1-4326-4455-A528-09F1E20EDC83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A24BF1D1-4326-4455-A528-09F1E20EDC83}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A24BF1D1-4326-4455-A528-09F1E20EDC83}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A24BF1D1-4326-4455-A528-09F1E20EDC83}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -74,5 +80,6 @@ Global
{8A2E6961-6B12-4A8E-8215-3E7301D52EAC} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134} {8A2E6961-6B12-4A8E-8215-3E7301D52EAC} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9} = {66517987-2A5A-4330-B130-207039378FD4} {53F3B53D-303A-4DAA-9C38-4F55195FA5B9} = {66517987-2A5A-4330-B130-207039378FD4}
{8B781D87-1FC3-4A34-9089-2BDF6B562B85} = {59E02BDF-98DE-4D64-B576-2D0299D5E052} {8B781D87-1FC3-4A34-9089-2BDF6B562B85} = {59E02BDF-98DE-4D64-B576-2D0299D5E052}
{A24BF1D1-4326-4455-A528-09F1E20EDC83} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // 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.Collections.Generic;
using System.Reflection; using System.Reflection;
using Microsoft.DotNet.Watcher.Tools; using Microsoft.DotNet.Watcher.Tools;
@ -17,9 +16,11 @@ namespace Microsoft.DotNet.Watcher
public bool IsQuiet { get; private set; } public bool IsQuiet { get; private set; }
public bool IsVerbose { get; private set; } public bool IsVerbose { get; private set; }
public IList<string> RemainingArguments { get; private set; } public IList<string> RemainingArguments { get; private set; }
public static CommandLineOptions Parse(string[] args, IConsole console) public static CommandLineOptions Parse(string[] args, IConsole console)
{ {
Ensure.NotNull(args, nameof(args)); Ensure.NotNull(args, nameof(args));
Ensure.NotNull(console, nameof(console));
var app = new CommandLineApplication(throwOnUnexpectedArg: false) var app = new CommandLineApplication(throwOnUnexpectedArg: false)
{ {
@ -60,20 +61,10 @@ Examples:
CommandOptionType.SingleValue); // TODO multiple shouldn't be too hard to support CommandOptionType.SingleValue); // TODO multiple shouldn't be too hard to support
var optQuiet = app.Option("-q|--quiet", "Suppresses all output except warnings and errors", var optQuiet = app.Option("-q|--quiet", "Suppresses all output except warnings and errors",
CommandOptionType.NoValue); CommandOptionType.NoValue);
var optVerbose = app.Option("-v|--verbose", "Show verbose output", var optVerbose = app.VerboseOption();
CommandOptionType.NoValue);
app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly); app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly);
app.OnExecute(() =>
{
if (app.RemainingArguments.Count == 0)
{
app.ShowHelp();
}
return 0;
});
if (app.Execute(args) != 0) if (app.Execute(args) != 0)
{ {
return null; return null;
@ -81,8 +72,12 @@ Examples:
if (optQuiet.HasValue() && optVerbose.HasValue()) if (optQuiet.HasValue() && optVerbose.HasValue())
{ {
console.Error.WriteLine(Resources.Error_QuietAndVerboseSpecified.Bold().Red()); throw new CommandParsingException(app, Resources.Error_QuietAndVerboseSpecified);
return null; }
if (app.RemainingArguments.Count == 0)
{
app.ShowHelp();
} }
return new CommandLineOptions return new CommandLineOptions
@ -95,4 +90,4 @@ Examples:
}; };
} }
} }
} }

View File

@ -1,64 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
namespace Microsoft.DotNet.Watcher
{
/// <summary>
/// Logger to print formatted command output.
/// </summary>
public class CommandOutputLogger : ILogger
{
private readonly CommandOutputProvider _provider;
private readonly AnsiConsole _outConsole;
private readonly string _loggerName;
public CommandOutputLogger(CommandOutputProvider commandOutputProvider, string loggerName, bool useConsoleColor)
{
_provider = commandOutputProvider;
_outConsole = AnsiConsole.GetOutput(useConsoleColor);
_loggerName = loggerName;
}
public IDisposable BeginScope<TState>(TState state)
{
throw new NotImplementedException();
}
public bool IsEnabled(LogLevel logLevel)
{
if (logLevel < _provider.LogLevel)
{
return false;
}
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (IsEnabled(logLevel))
{
_outConsole.WriteLine($"[{_loggerName}] {Caption(logLevel)}: {formatter(state, exception)}");
}
}
private string Caption(LogLevel logLevel)
{
switch (logLevel)
{
case LogLevel.Trace: return "\x1b[35mtrce\x1b[39m";
case LogLevel.Debug: return "\x1b[35mdbug\x1b[39m";
case LogLevel.Information: return "\x1b[32minfo\x1b[39m";
case LogLevel.Warning: return "\x1b[33mwarn\x1b[39m";
case LogLevel.Error: return "\x1b[31mfail\x1b[39m";
case LogLevel.Critical: return "\x1b[31mcrit\x1b[39m";
}
throw new Exception("Unknown LogLevel");
}
}
}

View File

@ -1,29 +0,0 @@
// Copyright (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.InteropServices;
using Microsoft.Extensions.Logging;
namespace Microsoft.DotNet.Watcher
{
public class CommandOutputProvider : ILoggerProvider
{
private readonly bool _isWindows;
public CommandOutputProvider()
{
_isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
}
public ILogger CreateLogger(string name)
{
return new CommandOutputLogger(this, name, useConsoleColor: _isWindows);
}
public void Dispose()
{
}
public LogLevel LogLevel { get; set; } = LogLevel.Information;
}
}

View File

@ -1,33 +1,35 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // 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;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.DotNet.Watcher.Internal; using Microsoft.DotNet.Watcher.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher namespace Microsoft.DotNet.Watcher
{ {
public class DotNetWatcher public class DotNetWatcher
{ {
private readonly ILogger _logger; private readonly IReporter _reporter;
private readonly ProcessRunner _processRunner; private readonly ProcessRunner _processRunner;
public DotNetWatcher(ILogger logger) public DotNetWatcher(IReporter reporter)
{ {
Ensure.NotNull(logger, nameof(logger)); Ensure.NotNull(reporter, nameof(reporter));
_logger = logger; _reporter = reporter;
_processRunner = new ProcessRunner(logger); _processRunner = new ProcessRunner(reporter);
} }
public async Task WatchAsync(ProcessSpec processSpec, IFileSetFactory fileSetFactory, CancellationToken cancellationToken) public async Task WatchAsync(ProcessSpec processSpec, IFileSetFactory fileSetFactory,
CancellationToken cancellationToken)
{ {
Ensure.NotNull(processSpec, nameof(processSpec)); Ensure.NotNull(processSpec, nameof(processSpec));
var cancelledTaskSource = new TaskCompletionSource<object>(); var cancelledTaskSource = new TaskCompletionSource<object>();
cancellationToken.Register(state => ((TaskCompletionSource<object>)state).TrySetResult(null), cancelledTaskSource); cancellationToken.Register(state => ((TaskCompletionSource<object>) state).TrySetResult(null),
cancelledTaskSource);
while (true) while (true)
{ {
@ -46,9 +48,10 @@ namespace Microsoft.DotNet.Watcher
var fileSetTask = fileSetWatcher.GetChangedFileAsync(combinedCancellationSource.Token); var fileSetTask = fileSetWatcher.GetChangedFileAsync(combinedCancellationSource.Token);
var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token); var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token);
_logger.LogInformation("Running {execName} with the following arguments: {args}", var args = ArgumentEscaper.EscapeAndConcatenate(processSpec.Arguments);
processSpec.ShortDisplayName(), _reporter.Verbose($"Running {processSpec.ShortDisplayName()} with the following arguments: {args}");
ArgumentEscaper.EscapeAndConcatenate(processSpec.Arguments));
_reporter.Output("Started");
var finishedTask = await Task.WhenAny(processTask, fileSetTask, cancelledTaskSource.Task); var finishedTask = await Task.WhenAny(processTask, fileSetTask, cancelledTaskSource.Task);
@ -58,6 +61,15 @@ namespace Microsoft.DotNet.Watcher
await Task.WhenAll(processTask, fileSetTask); 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) if (finishedTask == cancelledTaskSource.Task || cancellationToken.IsCancellationRequested)
{ {
return; return;
@ -65,7 +77,7 @@ namespace Microsoft.DotNet.Watcher
if (finishedTask == processTask) if (finishedTask == processTask)
{ {
_logger.LogInformation("Waiting for a file to change before restarting dotnet..."); _reporter.Warn("Waiting for a file to change before restarting dotnet...");
// Now wait for a file to change before restarting process // Now wait for a file to change before restarting process
await fileSetWatcher.GetChangedFileAsync(cancellationToken); await fileSetWatcher.GetChangedFileAsync(cancellationToken);
@ -73,10 +85,10 @@ namespace Microsoft.DotNet.Watcher
if (!string.IsNullOrEmpty(fileSetTask.Result)) if (!string.IsNullOrEmpty(fileSetTask.Result))
{ {
_logger.LogInformation($"File changed: {fileSetTask.Result}"); _reporter.Output($"File changed: {fileSetTask.Result}");
} }
} }
} }
} }
} }
} }

View File

@ -18,6 +18,8 @@ namespace Microsoft.DotNet.Watcher.Internal
public bool Contains(string filePath) => _files.Contains(filePath); public bool Contains(string filePath) => _files.Contains(filePath);
public int Count => _files.Count;
public IEnumerator<string> GetEnumerator() => _files.GetEnumerator(); public IEnumerator<string> GetEnumerator() => _files.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _files.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _files.GetEnumerator();
} }

View File

@ -7,48 +7,47 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher.Internal namespace Microsoft.DotNet.Watcher.Internal
{ {
public class MsBuildFileSetFactory : IFileSetFactory public class MsBuildFileSetFactory : IFileSetFactory
{ {
private const string TargetName = "GenerateWatchList";
private const string ProjectExtensionFileExtension = ".dotnetwatch.targets"; private const string ProjectExtensionFileExtension = ".dotnetwatch.targets";
private const string WatchTargetsFileName = "DotNetWatchCommon.targets"; private const string WatchTargetsFileName = "DotNetWatchCommon.targets";
private readonly ILogger _logger; private readonly IReporter _reporter;
private readonly string _projectFile; private readonly string _projectFile;
private readonly string _watchTargetsDir; private readonly string _watchTargetsDir;
private readonly OutputSink _outputSink; private readonly OutputSink _outputSink;
private readonly ProcessRunner _processRunner; private readonly ProcessRunner _processRunner;
public MsBuildFileSetFactory(ILogger logger, string projectFile) public MsBuildFileSetFactory(IReporter reporter, string projectFile)
: this(logger, projectFile, new OutputSink()) : this(reporter, projectFile, new OutputSink())
{ {
} }
// output sink is for testing // output sink is for testing
internal MsBuildFileSetFactory(ILogger logger, string projectFile, OutputSink outputSink) internal MsBuildFileSetFactory(IReporter reporter, string projectFile, OutputSink outputSink)
{ {
Ensure.NotNull(logger, nameof(logger)); Ensure.NotNull(reporter, nameof(reporter));
Ensure.NotNullOrEmpty(projectFile, nameof(projectFile)); Ensure.NotNullOrEmpty(projectFile, nameof(projectFile));
Ensure.NotNull(outputSink, nameof(outputSink)); Ensure.NotNull(outputSink, nameof(outputSink));
_logger = logger; _reporter = reporter;
_projectFile = projectFile; _projectFile = projectFile;
_watchTargetsDir = FindWatchTargetsDir(); _watchTargetsDir = FindWatchTargetsDir();
_outputSink = outputSink; _outputSink = outputSink;
_processRunner = new ProcessRunner(logger); _processRunner = new ProcessRunner(reporter);
} }
internal List<string> BuildFlags { get; } = new List<string> internal List<string> BuildFlags { get; } = new List<string>
{ {
"/nologo", "/nologo",
"/v:n", "/v:n",
"/t:GenerateWatchList", "/t:" + TargetName,
"/p:DotNetWatchBuild=true", // extensibility point for users "/p:DotNetWatchBuild=true", // extensibility point for users
"/p:DesignTimeBuild=true", // don't do expensive things "/p:DesignTimeBuild=true", // don't do expensive things
}; };
@ -66,10 +65,6 @@ namespace Microsoft.DotNet.Watcher.Internal
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
#if DEBUG
var stopwatch = new Stopwatch();
stopwatch.Start();
#endif
var capture = _outputSink.StartCapture(); var capture = _outputSink.StartCapture();
// TODO adding files doesn't currently work. Need to provide a way to detect new files // TODO adding files doesn't currently work. Need to provide a way to detect new files
// find files // find files
@ -80,53 +75,56 @@ namespace Microsoft.DotNet.Watcher.Internal
Arguments = new[] Arguments = new[]
{ {
"msbuild", "msbuild",
_projectFile, _projectFile,
$"/p:_DotNetWatchTargetsLocation={_watchTargetsDir}", // add our dotnet-watch targets $"/p:_DotNetWatchTargetsLocation={_watchTargetsDir}", // add our dotnet-watch targets
$"/p:_DotNetWatchListFile={watchList}" $"/p:_DotNetWatchListFile={watchList}"
}.Concat(BuildFlags), }.Concat(BuildFlags),
OutputCapture = capture OutputCapture = capture
}; };
_reporter.Verbose($"Running MSBuild target '{TargetName}' on '{_projectFile}'");
var exitCode = await _processRunner.RunAsync(processSpec, cancellationToken); var exitCode = await _processRunner.RunAsync(processSpec, cancellationToken);
if (exitCode == 0) if (exitCode == 0)
{ {
var files = File.ReadAllLines(watchList) var fileset = new FileSet(
File.ReadAllLines(watchList)
.Select(l => l?.Trim()) .Select(l => l?.Trim())
.Where(l => !string.IsNullOrEmpty(l)); .Where(l => !string.IsNullOrEmpty(l)));
var fileset = new FileSet(files);
_reporter.Verbose($"Watching {fileset.Count} file(s) for changes");
#if DEBUG #if DEBUG
_logger.LogDebug(string.Join(Environment.NewLine, fileset));
Debug.Assert(files.All(Path.IsPathRooted), "All files should be rooted paths"); foreach (var file in fileset)
stopwatch.Stop(); {
_logger.LogDebug("Gathered project information in {time}ms", stopwatch.ElapsedMilliseconds); _reporter.Verbose($" -> {file}");
}
Debug.Assert(fileset.All(Path.IsPathRooted), "All files should be rooted paths");
#endif #endif
return fileset; return fileset;
} }
var sb = new StringBuilder() _reporter.Error($"Error(s) finding watch items project file '{Path.GetFileName(_projectFile)}'");
.Append("Error(s) finding watch items project file '")
.Append(Path.GetFileName(_projectFile)) _reporter.Output($"MSBuild output from target '{TargetName}'");
.AppendLine("' :");
foreach (var line in capture.Lines) foreach (var line in capture.Lines)
{ {
sb.Append(" [MSBUILD] :").AppendLine(line); _reporter.Output($" [MSBUILD] : {line}");
} }
_logger.LogError(sb.ToString()); _reporter.Warn("Fix the error to continue or press Ctrl+C to exit.");
_logger.LogInformation("Fix the error to continue or press Ctrl+C to exit.");
var fileSet = new FileSet(new[] { _projectFile }); var fileSet = new FileSet(new[] {_projectFile});
using (var watcher = new FileSetWatcher(fileSet)) using (var watcher = new FileSetWatcher(fileSet))
{ {
await watcher.GetChangedFileAsync(cancellationToken); await watcher.GetChangedFileAsync(cancellationToken);
_logger.LogInformation($"File changed: {_projectFile}"); _reporter.Output($"File changed: {_projectFile}");
} }
} }
} }
@ -143,7 +141,8 @@ namespace Microsoft.DotNet.Watcher.Internal
var projectExtensionsPath = Path.Combine(Path.GetDirectoryName(_projectFile), "obj"); var projectExtensionsPath = Path.Combine(Path.GetDirectoryName(_projectFile), "obj");
// see https://github.com/Microsoft/msbuild/blob/bf9b21cc7869b96ea2289ff31f6aaa5e1d525a26/src/XMakeTasks/Microsoft.Common.targets#L127 // see https://github.com/Microsoft/msbuild/blob/bf9b21cc7869b96ea2289ff31f6aaa5e1d525a26/src/XMakeTasks/Microsoft.Common.targets#L127
var projectExtensionFile = Path.Combine(projectExtensionsPath, Path.GetFileName(_projectFile) + ProjectExtensionFileExtension); var projectExtensionFile = Path.Combine(projectExtensionsPath,
Path.GetFileName(_projectFile) + ProjectExtensionFileExtension);
if (!File.Exists(projectExtensionFile)) if (!File.Exists(projectExtensionFile))
{ {
@ -173,4 +172,4 @@ namespace Microsoft.DotNet.Watcher.Internal
return Path.GetDirectoryName(targetPath); return Path.GetDirectoryName(targetPath);
} }
} }
} }

View File

@ -7,20 +7,19 @@ using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher.Internal namespace Microsoft.DotNet.Watcher.Internal
{ {
public class ProcessRunner public class ProcessRunner
{ {
private readonly ILogger _logger; private readonly IReporter _reporter;
public ProcessRunner(ILogger logger) public ProcessRunner(IReporter reporter)
{ {
Ensure.NotNull(logger, nameof(logger)); Ensure.NotNull(reporter, nameof(reporter));
_logger = logger; _reporter = reporter;
} }
// May not be necessary in the future. See https://github.com/dotnet/corefx/issues/12039 // May not be necessary in the future. See https://github.com/dotnet/corefx/issues/12039
@ -30,12 +29,16 @@ namespace Microsoft.DotNet.Watcher.Internal
int exitCode; int exitCode;
var stopwatch = new Stopwatch();
using (var process = CreateProcess(processSpec)) using (var process = CreateProcess(processSpec))
using (var processState = new ProcessState(process)) using (var processState = new ProcessState(process))
{ {
cancellationToken.Register(() => processState.TryKill()); cancellationToken.Register(() => processState.TryKill());
stopwatch.Start();
process.Start(); process.Start();
_reporter.Verbose($"Started '{processSpec.Executable}' with process id {process.Id}");
if (processSpec.IsOutputCaptured) if (processSpec.IsOutputCaptured)
{ {
@ -47,16 +50,12 @@ namespace Microsoft.DotNet.Watcher.Internal
} }
else else
{ {
_logger.LogInformation("{execName} process id: {pid}", processSpec.ShortDisplayName(), process.Id);
await processState.Task; await processState.Task;
} }
exitCode = process.ExitCode; exitCode = process.ExitCode;
} stopwatch.Stop();
_reporter.Verbose($"Process id {process.Id} ran for {stopwatch.ElapsedMilliseconds}ms");
if (!processSpec.IsOutputCaptured)
{
LogResult(processSpec, exitCode);
} }
return exitCode; return exitCode;
@ -81,19 +80,6 @@ namespace Microsoft.DotNet.Watcher.Internal
return process; return process;
} }
private void LogResult(ProcessSpec processSpec, int exitCode)
{
var processName = processSpec.ShortDisplayName();
if (exitCode == 0)
{
_logger.LogInformation("{execName} exit code: {code}", processName, exitCode);
}
else
{
_logger.LogError("{execName} exit code: {code}", processName, exitCode);
}
}
private static async Task ConsumeStreamAsync(StreamReader reader, Action<string> consume) private static async Task ConsumeStreamAsync(StreamReader reader, Action<string> consume)
{ {
string line; string line;

View File

@ -14,9 +14,7 @@
</packageTypes> </packageTypes>
<dependencies> <dependencies>
<group targetFramework=".NETCoreApp1.0"> <group targetFramework=".NETCoreApp1.0">
<dependency id="Microsoft.Extensions.Logging" version="$dep_1$" /> <dependency id="Microsoft.NETCore.App" version="$dep_1$" />
<dependency id="Microsoft.Extensions.Logging.Console" version="$dep_2$" />
<dependency id="Microsoft.NETCore.App" version="$dep_3$" />
</group> </group>
</dependencies> </dependencies>
</metadata> </metadata>

View File

@ -2,20 +2,18 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Diagnostics; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.DotNet.Watcher.Internal; using Microsoft.DotNet.Watcher.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher namespace Microsoft.DotNet.Watcher
{ {
public class Program public class Program
{ {
private const string LoggerName = "DotNetWatcher";
private readonly IConsole _console; private readonly IConsole _console;
private readonly string _workingDir; private readonly string _workingDir;
@ -39,44 +37,18 @@ namespace Microsoft.DotNet.Watcher
public async Task<int> RunAsync(string[] args) public async Task<int> RunAsync(string[] args)
{ {
using (CancellationTokenSource ctrlCTokenSource = new CancellationTokenSource()) CommandLineOptions options;
try
{ {
_console.CancelKeyPress += (sender, ev) => options = CommandLineOptions.Parse(args, _console);
{ }
if (!ctrlCTokenSource.IsCancellationRequested) catch (CommandParsingException ex)
{ {
_console.Out.WriteLine($"[{LoggerName}] Shutdown requested. Press Ctrl+C again to force exit."); CreateReporter(verbose: true, quiet: false, console: _console)
ev.Cancel = true; .Error(ex.Message);
} return 1;
else
{
ev.Cancel = false;
}
ctrlCTokenSource.Cancel();
};
try
{
return await MainInternalAsync(args, ctrlCTokenSource.Token);
}
catch (Exception ex)
{
if (ex is TaskCanceledException || ex is OperationCanceledException)
{
// swallow when only exception is the CTRL+C forced an exit
return 0;
}
_console.Error.WriteLine(ex.ToString());
_console.Error.WriteLine($"[{LoggerName}] An unexpected error occurred".Bold().Red());
return 1;
}
} }
}
private async Task<int> MainInternalAsync(string[] args, CancellationToken cancellationToken)
{
var options = CommandLineOptions.Parse(args, _console);
if (options == null) if (options == null)
{ {
// invalid args syntax // invalid args syntax
@ -88,58 +60,122 @@ namespace Microsoft.DotNet.Watcher
return 2; return 2;
} }
var loggerFactory = new LoggerFactory(); var reporter = CreateReporter(options.IsVerbose, options.IsQuiet, _console);
var commandProvider = new CommandOutputProvider
{
LogLevel = ResolveLogLevel(options)
};
loggerFactory.AddProvider(commandProvider);
var logger = loggerFactory.CreateLogger(LoggerName);
using (CancellationTokenSource ctrlCTokenSource = new CancellationTokenSource())
{
_console.CancelKeyPress += (sender, ev) =>
{
if (!ctrlCTokenSource.IsCancellationRequested)
{
reporter.Output("Shutdown requested. Press Ctrl+C again to force exit.");
ev.Cancel = true;
}
else
{
ev.Cancel = false;
}
ctrlCTokenSource.Cancel();
};
try
{
return await MainInternalAsync(reporter, options.Project, options.RemainingArguments, ctrlCTokenSource.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 async Task<int> MainInternalAsync(
IReporter reporter,
string project,
ICollection<string> args,
CancellationToken cancellationToken)
{
// TODO multiple projects should be easy enough to add here // TODO multiple projects should be easy enough to add here
string projectFile; string projectFile;
try try
{ {
projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, options.Project); projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, project);
} }
catch (FileNotFoundException ex) catch (FileNotFoundException ex)
{ {
_console.Error.WriteLine(ex.Message.Bold().Red()); reporter.Error(ex.Message);
return 1; return 1;
} }
var fileSetFactory = new MsBuildFileSetFactory(logger, projectFile); var fileSetFactory = new MsBuildFileSetFactory(reporter, projectFile);
var processInfo = new ProcessSpec var processInfo = new ProcessSpec
{ {
Executable = DotNetMuxer.MuxerPathOrDefault(), Executable = DotNetMuxer.MuxerPathOrDefault(),
WorkingDirectory = Path.GetDirectoryName(projectFile), WorkingDirectory = Path.GetDirectoryName(projectFile),
Arguments = options.RemainingArguments Arguments = args
}; };
await new DotNetWatcher(logger) await new DotNetWatcher(reporter)
.WatchAsync(processInfo, fileSetFactory, cancellationToken); .WatchAsync(processInfo, fileSetFactory, cancellationToken);
return 0; return 0;
} }
private LogLevel ResolveLogLevel(CommandLineOptions options) private static IReporter CreateReporter(bool verbose, bool quiet, IConsole console)
{ {
if (options.IsQuiet) const string prefix = "watch : ";
{ var colorPrefix = new ColorFormatter(ConsoleColor.DarkGray).Format(prefix);
return LogLevel.Warning;
}
bool globalVerbose; return new ReporterBuilder()
bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE"), out globalVerbose); .WithConsole(console)
.Verbose(f =>
{
if (console.IsOutputRedirected)
{
f.WithPrefix(prefix);
}
else
{
f.WithColor(ConsoleColor.DarkGray).WithPrefix(colorPrefix);
}
if (options.IsVerbose // dotnet watch --verbose f.When(() => verbose || CliContext.IsGlobalVerbose());
|| globalVerbose) // dotnet --verbose watch })
{ .Output(f => f
return LogLevel.Debug; .WithPrefix(console.IsOutputRedirected ? prefix : colorPrefix)
} .When(() => !quiet))
.Warn(f =>
return LogLevel.Information; {
if (console.IsOutputRedirected)
{
f.WithPrefix(prefix);
}
else
{
f.WithColor(ConsoleColor.Yellow).WithPrefix(colorPrefix);
}
})
.Error(f =>
{
if (console.IsOutputRedirected)
{
f.WithPrefix(prefix);
}
else
{
f.WithColor(ConsoleColor.Red).WithPrefix(colorPrefix);
}
})
.Build();
} }
} }
} }

View File

@ -32,8 +32,6 @@
] ]
}, },
"dependencies": { "dependencies": {
"Microsoft.Extensions.Logging": "1.0.0",
"Microsoft.Extensions.Logging.Console": "1.0.0",
"Microsoft.Extensions.Process.Sources": { "Microsoft.Extensions.Process.Sources": {
"type": "build", "type": "build",
"version": "1.0.0" "version": "1.0.0"

View File

@ -14,11 +14,8 @@
</packageTypes> </packageTypes>
<dependencies> <dependencies>
<group targetFramework=".NETCoreApp1.0"> <group targetFramework=".NETCoreApp1.0">
<dependency id="Microsoft.Extensions.CommandLineUtils" version="$dep_1$" /> <dependency id="Microsoft.NETCore.App" version="$dep_1$" />
<dependency id="Microsoft.Extensions.Logging" version="$dep_2$" /> <dependency id="System.Data.SqlClient" version="$dep_2$" />
<dependency id="Microsoft.Extensions.Logging.Console" version="$dep_3$" />
<dependency id="Microsoft.NETCore.App" version="$dep_4$" />
<dependency id="System.Data.SqlClient" version="$dep_5$" />
</group> </group>
</dependencies> </dependencies>
</metadata> </metadata>

View File

@ -6,7 +6,7 @@ using System.Data;
using System.Data.SqlClient; using System.Data.SqlClient;
using System.Reflection; using System.Reflection;
using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.Caching.SqlConfig.Tools namespace Microsoft.Extensions.Caching.SqlConfig.Tools
{ {
@ -15,54 +15,61 @@ namespace Microsoft.Extensions.Caching.SqlConfig.Tools
private string _connectionString = null; private string _connectionString = null;
private string _schemaName = null; private string _schemaName = null;
private string _tableName = null; private string _tableName = null;
private readonly IConsole _console;
private readonly ILogger _logger; public Program(IConsole console)
public Program()
{ {
var loggerFactory = new LoggerFactory(); Ensure.NotNull(console, nameof(console));
loggerFactory.AddConsole();
_logger = loggerFactory.CreateLogger<Program>(); _console = console;
} }
public static int Main(string[] args) public static int Main(string[] args)
{ {
return new Program().Run(args); return new Program(PhysicalConsole.Singleton).Run(args);
} }
public int Run(string[] args) public int Run(string[] args)
{ {
DebugHelper.HandleDebugSwitch(ref args);
try try
{ {
var description = "Creates table and indexes in Microsoft SQL Server database " +
"to be used for distributed caching";
var app = new CommandLineApplication var app = new CommandLineApplication
{ {
Name = "dotnet sql-cache", Name = "dotnet sql-cache",
FullName = "SQL Server Cache Command Line Tool", FullName = "SQL Server Cache Command Line Tool",
Description = description, Description =
"Creates table and indexes in Microsoft SQL Server database to be used for distributed caching",
}; };
app.HelpOption("-?|-h|--help");
app.HelpOption();
app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly); app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly);
var verbose = app.VerboseOption();
app.Command("create", command => app.Command("create", command =>
{ {
command.Description = description; command.Description = app.Description;
var connectionStringArg = command.Argument( var connectionStringArg = command.Argument(
"[connectionString]", "[connectionString]", "The connection string to connect to the database.");
"The connection string to connect to the database.");
var schemaNameArg = command.Argument("[schemaName]", "Name of the table schema."); var schemaNameArg = command.Argument(
var tableNameArg = command.Argument("[tableName]", "Name of the table to be created."); "[schemaName]", "Name of the table schema.");
command.HelpOption("-?|-h|--help");
var tableNameArg = command.Argument(
"[tableName]", "Name of the table to be created.");
command.HelpOption();
command.OnExecute(() => command.OnExecute(() =>
{ {
var reporter = CreateReporter(verbose.HasValue());
if (string.IsNullOrEmpty(connectionStringArg.Value) if (string.IsNullOrEmpty(connectionStringArg.Value)
|| string.IsNullOrEmpty(schemaNameArg.Value) || string.IsNullOrEmpty(schemaNameArg.Value)
|| string.IsNullOrEmpty(tableNameArg.Value)) || string.IsNullOrEmpty(tableNameArg.Value))
{ {
_logger.LogWarning("Invalid input"); reporter.Error("Invalid input");
app.ShowHelp(); app.ShowHelp();
return 2; return 2;
} }
@ -71,7 +78,7 @@ namespace Microsoft.Extensions.Caching.SqlConfig.Tools
_schemaName = schemaNameArg.Value; _schemaName = schemaNameArg.Value;
_tableName = tableNameArg.Value; _tableName = tableNameArg.Value;
return CreateTableAndIndexes(); return CreateTableAndIndexes(reporter);
}); });
}); });
@ -86,12 +93,41 @@ namespace Microsoft.Extensions.Caching.SqlConfig.Tools
} }
catch (Exception exception) catch (Exception exception)
{ {
_logger.LogCritical("An error occurred. {ErrorMessage}", exception.Message); CreateReporter(verbose: false).Error($"An error occurred. {exception.Message}");
return 1; return 1;
} }
} }
private int CreateTableAndIndexes() private IReporter CreateReporter(bool verbose)
{
return new ReporterBuilder()
.WithConsole(_console)
.Verbose(f =>
{
f.When(() => verbose);
if (!_console.IsOutputRedirected)
{
f.WithColor(ConsoleColor.DarkGray);
}
})
.Warn(f =>
{
if (!_console.IsOutputRedirected)
{
f.WithColor(ConsoleColor.Yellow);
}
})
.Error(f =>
{
if (!_console.IsErrorRedirected)
{
f.WithColor(ConsoleColor.Red);
}
})
.Build();
}
private int CreateTableAndIndexes(IReporter reporter)
{ {
ValidateConnectionString(); ValidateConnectionString();
@ -106,7 +142,7 @@ namespace Microsoft.Extensions.Caching.SqlConfig.Tools
{ {
if (reader.Read()) if (reader.Read())
{ {
_logger.LogWarning( reporter.Warn(
$"Table with schema '{_schemaName}' and name '{_tableName}' already exists. " + $"Table with schema '{_schemaName}' and name '{_tableName}' already exists. " +
"Provide a different table name and try again."); "Provide a different table name and try again.");
return 1; return 1;
@ -118,23 +154,26 @@ namespace Microsoft.Extensions.Caching.SqlConfig.Tools
try try
{ {
command = new SqlCommand(sqlQueries.CreateTable, connection, transaction); command = new SqlCommand(sqlQueries.CreateTable, connection, transaction);
reporter.Verbose($"Executing {command.CommandText}");
command.ExecuteNonQuery(); command.ExecuteNonQuery();
command = new SqlCommand( command = new SqlCommand(
sqlQueries.CreateNonClusteredIndexOnExpirationTime, sqlQueries.CreateNonClusteredIndexOnExpirationTime,
connection, connection,
transaction); transaction);
reporter.Verbose($"Executing {command.CommandText}");
command.ExecuteNonQuery(); command.ExecuteNonQuery();
transaction.Commit(); transaction.Commit();
_logger.LogInformation("Table and index were created successfully."); reporter.Output("Table and index were created successfully.");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError( reporter.Error(
"An error occurred while trying to create the table and index. {ErrorMessage}", $"An error occurred while trying to create the table and index. {ex.Message}");
ex.Message);
transaction.Rollback(); transaction.Rollback();
return 1; return 1;
@ -154,7 +193,7 @@ namespace Microsoft.Extensions.Caching.SqlConfig.Tools
catch (Exception ex) catch (Exception ex)
{ {
throw new ArgumentException( throw new ArgumentException(
$"Invalid Sql server connection string '{_connectionString}'. {ex.Message}", ex); $"Invalid SQL Server connection string '{_connectionString}'. {ex.Message}", ex);
} }
} }
} }

View File

@ -5,7 +5,8 @@
"emitEntryPoint": true, "emitEntryPoint": true,
"warningsAsErrors": true, "warningsAsErrors": true,
"keyFile": "../../tools/Key.snk", "keyFile": "../../tools/Key.snk",
"compile": "../Shared/CommandLineApplicationExtensions.cs" "compile": "../Shared/**/*.cs"
}, },
"description": "Command line tool to create tables and indexes in a Microsoft SQL Server database for distributed caching.", "description": "Command line tool to create tables and indexes in a Microsoft SQL Server database for distributed caching.",
"packOptions": { "packOptions": {
@ -20,9 +21,6 @@
] ]
}, },
"dependencies": { "dependencies": {
"Microsoft.Extensions.CommandLineUtils": "1.0.0",
"Microsoft.Extensions.Logging": "1.0.0",
"Microsoft.Extensions.Logging.Console": "1.0.0",
"Microsoft.NETCore.App": { "Microsoft.NETCore.App": {
"version": "1.0.1", "version": "1.0.1",
"type": "platform" "type": "platform"

View File

@ -31,8 +31,7 @@ namespace Microsoft.Extensions.SecretManager.Tools
app.HelpOption(); app.HelpOption();
app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly); app.VersionOptionFromAssemblyAttributes(typeof(Program).GetTypeInfo().Assembly);
var optionVerbose = app.Option("-v|--verbose", "Verbose output", var optionVerbose = app.VerboseOption();
CommandOptionType.NoValue, inherited: true);
var optionProject = app.Option("-p|--project <PROJECT>", "Path to project, default is current directory", var optionProject = app.Option("-p|--project <PROJECT>", "Path to project, default is current directory",
CommandOptionType.SingleValue, inherited: true); CommandOptionType.SingleValue, inherited: true);
@ -48,7 +47,7 @@ namespace Microsoft.Extensions.SecretManager.Tools
var options = new CommandLineOptions(); var options = new CommandLineOptions();
app.Command("set", c => SetCommand.Configure(c, options, console)); app.Command("set", c => SetCommand.Configure(c, options, console));
app.Command("remove", c => RemoveCommand.Configure(c, options, console)); app.Command("remove", c => RemoveCommand.Configure(c, options));
app.Command("list", c => ListCommand.Configure(c, options)); app.Command("list", c => ListCommand.Configure(c, options));
app.Command("clear", c => ClearCommand.Configure(c, options)); app.Command("clear", c => ClearCommand.Configure(c, options));

View File

@ -1,62 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
namespace Microsoft.Extensions.SecretManager.Tools
{
/// <summary>
/// Logger to print formatted command output.
/// </summary>
public class CommandOutputLogger : ILogger
{
private readonly CommandOutputProvider _provider;
private readonly AnsiConsole _outConsole;
public CommandOutputLogger(CommandOutputProvider commandOutputProvider, bool useConsoleColor)
{
_provider = commandOutputProvider;
_outConsole = AnsiConsole.GetOutput(useConsoleColor);
}
public IDisposable BeginScope<TState>(TState state)
{
throw new NotImplementedException();
}
public bool IsEnabled(LogLevel logLevel)
{
if (logLevel < _provider.LogLevel)
{
return false;
}
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (IsEnabled(logLevel))
{
_outConsole.WriteLine(string.Format("{0}: {1}", Caption(logLevel), formatter(state, exception)));
}
}
private string Caption(LogLevel logLevel)
{
switch (logLevel)
{
case LogLevel.Trace: return "\x1b[35mtrace\x1b[39m";
case LogLevel.Debug: return "\x1b[35mdebug\x1b[39m";
case LogLevel.Information: return "\x1b[32minfo\x1b[39m";
case LogLevel.Warning: return "\x1b[33mwarn\x1b[39m";
case LogLevel.Error: return "\x1b[31mfail\x1b[39m";
case LogLevel.Critical: return "\x1b[31mcritical\x1b[39m";
}
throw new Exception("Unknown LogLevel");
}
}
}

View File

@ -1,23 +0,0 @@
// Copyright (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.InteropServices;
using Microsoft.Extensions.Logging;
namespace Microsoft.Extensions.SecretManager.Tools
{
public class CommandOutputProvider : ILoggerProvider
{
public ILogger CreateLogger(string name)
{
var useConsoleColor = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
return new CommandOutputLogger(this, useConsoleColor);
}
public void Dispose()
{
}
public LogLevel LogLevel { get; set; } = LogLevel.Information;
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.SecretManager.Tools.Internal namespace Microsoft.Extensions.SecretManager.Tools.Internal
@ -10,16 +9,16 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
{ {
public CommandContext( public CommandContext(
SecretsStore store, SecretsStore store,
ILogger logger, IReporter reporter,
IConsole console) IConsole console)
{ {
SecretStore = store; SecretStore = store;
Logger = logger; Reporter = reporter;
Console = console; Console = console;
} }
public IConsole Console { get; } public IConsole Console { get; }
public ILogger Logger { get; } public IReporter Reporter { get; }
public SecretsStore SecretStore { get; } public SecretsStore SecretStore { get; }
} }
} }

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -41,13 +40,13 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
if (context.SecretStore.Count == 0) if (context.SecretStore.Count == 0)
{ {
context.Logger.LogInformation(Resources.Error_No_Secrets_Found); context.Reporter.Output(Resources.Error_No_Secrets_Found);
} }
else else
{ {
foreach (var secret in context.SecretStore.AsEnumerable()) foreach (var secret in context.SecretStore.AsEnumerable())
{ {
context.Logger.LogInformation(Resources.FormatMessage_Secret_Value_Format(secret.Key, secret.Value)); context.Reporter.Output(Resources.FormatMessage_Secret_Value_Format(secret.Key, secret.Value));
} }
} }
} }
@ -60,10 +59,9 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
jObject[item.Key] = item.Value; jObject[item.Key] = item.Value;
} }
// TODO logger would prefix each line. context.Reporter.Output("//BEGIN");
context.Console.Out.WriteLine("//BEGIN"); context.Reporter.Output(jObject.ToString(Formatting.Indented));
context.Console.Out.WriteLine(jObject.ToString(Formatting.Indented)); context.Reporter.Output("//END");
context.Console.Out.WriteLine("//END");
} }
} }
} }

View File

@ -7,7 +7,6 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.SecretManager.Tools.Internal namespace Microsoft.Extensions.SecretManager.Tools.Internal
@ -15,27 +14,32 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
public class ProjectIdResolver : IDisposable public class ProjectIdResolver : IDisposable
{ {
private const string TargetsFileName = "FindUserSecretsProperty.targets"; private const string TargetsFileName = "FindUserSecretsProperty.targets";
private readonly ILogger _logger; private const string DefaultConfig = "Debug";
private readonly IReporter _reporter;
private readonly string _workingDirectory; private readonly string _workingDirectory;
private readonly List<string> _tempFiles = new List<string>(); private readonly List<string> _tempFiles = new List<string>();
public ProjectIdResolver(ILogger logger, string workingDirectory) public ProjectIdResolver(IReporter reporter, string workingDirectory)
{ {
_workingDirectory = workingDirectory; _workingDirectory = workingDirectory;
_logger = logger; _reporter = reporter;
} }
public string Resolve(string project, string configuration = "Debug") public string Resolve(string project, string configuration)
{ {
var finder = new MsBuildProjectFinder(_workingDirectory); var finder = new MsBuildProjectFinder(_workingDirectory);
var projectFile = finder.FindMsBuildProject(project); var projectFile = finder.FindMsBuildProject(project);
_logger.LogDebug(Resources.Message_Project_File_Path, projectFile); _reporter.Verbose(Resources.FormatMessage_Project_File_Path(projectFile));
var targetFile = GetTargetFile(); var targetFile = GetTargetFile();
var outputFile = Path.GetTempFileName(); var outputFile = Path.GetTempFileName();
_tempFiles.Add(outputFile); _tempFiles.Add(outputFile);
configuration = !string.IsNullOrEmpty(configuration)
? configuration
: DefaultConfig;
var args = new[] var args = new[]
{ {
"msbuild", "msbuild",
@ -53,13 +57,18 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true RedirectStandardError = true
}; };
#if DEBUG
_reporter.Verbose($"Invoking '{psi.FileName} {psi.Arguments}'");
#endif
var process = Process.Start(psi); var process = Process.Start(psi);
process.WaitForExit(); process.WaitForExit();
if (process.ExitCode != 0) if (process.ExitCode != 0)
{ {
_logger.LogDebug(process.StandardOutput.ReadToEnd()); _reporter.Verbose(process.StandardOutput.ReadToEnd());
_logger.LogDebug(process.StandardError.ReadToEnd()); _reporter.Verbose(process.StandardError.ReadToEnd());
throw new InvalidOperationException(Resources.FormatError_ProjectFailedToLoad(projectFile)); throw new InvalidOperationException(Resources.FormatError_ProjectFailedToLoad(projectFile));
} }

View File

@ -1,9 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.SecretManager.Tools.Internal namespace Microsoft.Extensions.SecretManager.Tools.Internal
@ -12,7 +10,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
{ {
private readonly string _keyName; private readonly string _keyName;
public static void Configure(CommandLineApplication command, CommandLineOptions options, IConsole console) public static void Configure(CommandLineApplication command, CommandLineOptions options)
{ {
command.Description = "Removes the specified user secret"; command.Description = "Removes the specified user secret";
command.HelpOption(); command.HelpOption();
@ -22,12 +20,10 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
{ {
if (keyArg.Value == null) if (keyArg.Value == null)
{ {
console.Error.WriteLine(Resources.FormatError_MissingArgument("name").Red()); throw new CommandParsingException(command, Resources.FormatError_MissingArgument("name"));
return 1;
} }
options.Command = new RemoveCommand(keyArg.Value); options.Command = new RemoveCommand(keyArg.Value);
return 0;
}); });
} }
@ -41,7 +37,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
{ {
if (!context.SecretStore.ContainsKey(_keyName)) if (!context.SecretStore.ContainsKey(_keyName))
{ {
context.Logger.LogWarning(Resources.Error_Missing_Secret, _keyName); context.Reporter.Warn(Resources.FormatError_Missing_Secret(_keyName));
} }
else else
{ {

View File

@ -8,7 +8,6 @@ using System.Linq;
using System.Text; using System.Text;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.UserSecrets; using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -19,7 +18,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
private readonly string _secretsFilePath; private readonly string _secretsFilePath;
private IDictionary<string, string> _secrets; private IDictionary<string, string> _secrets;
public SecretsStore(string userSecretsId, ILogger logger) public SecretsStore(string userSecretsId, IReporter reporter)
{ {
Ensure.NotNull(userSecretsId, nameof(userSecretsId)); Ensure.NotNull(userSecretsId, nameof(userSecretsId));
@ -29,7 +28,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
var secretDir = Path.GetDirectoryName(_secretsFilePath); var secretDir = Path.GetDirectoryName(_secretsFilePath);
Directory.CreateDirectory(secretDir); Directory.CreateDirectory(secretDir);
logger.LogDebug(Resources.Message_Secret_File_Path, _secretsFilePath); reporter.Verbose(Resources.FormatMessage_Secret_File_Path(_secretsFilePath));
_secrets = Load(userSecretsId); _secrets = Load(userSecretsId);
} }

View File

@ -1,11 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // 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.IO;
using System.Text; using System.Text;
using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.SecretManager.Tools.Internal namespace Microsoft.Extensions.SecretManager.Tools.Internal
@ -34,23 +32,21 @@ Examples:
if (console.IsInputRedirected && nameArg.Value == null) if (console.IsInputRedirected && nameArg.Value == null)
{ {
options.Command = new FromStdInStrategy(); options.Command = new FromStdInStrategy();
return 0;
} }
else
if (string.IsNullOrEmpty(nameArg.Value))
{ {
console.Error.WriteLine(Resources.FormatError_MissingArgument("name").Red()); if (string.IsNullOrEmpty(nameArg.Value))
return 1; {
} throw new CommandParsingException(command, Resources.FormatError_MissingArgument("name"));
}
if (valueArg.Value == null) if (valueArg.Value == null)
{ {
console.Error.WriteLine(Resources.FormatError_MissingArgument("value").Red()); throw new CommandParsingException(command, Resources.FormatError_MissingArgument("value"));
return 1; }
}
options.Command = new ForOneValueStrategy(nameArg.Value, valueArg.Value); options.Command = new ForOneValueStrategy(nameArg.Value, valueArg.Value);
return 0; }
}); });
} }
@ -76,7 +72,7 @@ Examples:
context.SecretStore.Set(k.Key, k.Value); context.SecretStore.Set(k.Key, k.Value);
} }
context.Logger.LogInformation(Resources.Message_Saved_Secrets, provider.CurrentData.Count); context.Reporter.Output(Resources.FormatMessage_Saved_Secrets(provider.CurrentData.Count));
context.SecretStore.Save(); context.SecretStore.Save();
} }
@ -97,7 +93,7 @@ Examples:
{ {
context.SecretStore.Set(_keyName, _keyValue); context.SecretStore.Set(_keyName, _keyValue);
context.SecretStore.Save(); context.SecretStore.Save();
context.Logger.LogInformation(Resources.Message_Saved_Secret, _keyName, _keyValue); context.Reporter.Output(Resources.FormatMessage_Saved_Secret(_keyName, _keyValue));
} }
} }
} }

View File

@ -17,8 +17,7 @@
<group targetFramework=".NETCoreApp1.0"> <group targetFramework=".NETCoreApp1.0">
<!-- MUST BE alphabetical --> <!-- MUST BE alphabetical -->
<dependency id="Microsoft.Extensions.Configuration.UserSecrets" version="$dep_1$" /> <dependency id="Microsoft.Extensions.Configuration.UserSecrets" version="$dep_1$" />
<dependency id="Microsoft.Extensions.Logging" version="$dep_2$" /> <dependency id="Microsoft.NETCore.App" version="$dep_2$" />
<dependency id="Microsoft.NETCore.App" version="$dep_3$" />
</group> </group>
</dependencies> </dependencies>
</metadata> </metadata>

View File

@ -3,7 +3,7 @@
using System; using System;
using System.IO; using System.IO;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.SecretManager.Tools.Internal; using Microsoft.Extensions.SecretManager.Tools.Internal;
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
@ -11,8 +11,6 @@ namespace Microsoft.Extensions.SecretManager.Tools
{ {
public class Program public class Program
{ {
private ILogger _logger;
private CommandOutputProvider _loggerProvider;
private readonly IConsole _console; private readonly IConsole _console;
private readonly string _workingDirectory; private readonly string _workingDirectory;
@ -29,23 +27,6 @@ namespace Microsoft.Extensions.SecretManager.Tools
{ {
_console = console; _console = console;
_workingDirectory = workingDirectory; _workingDirectory = workingDirectory;
var loggerFactory = new LoggerFactory();
CommandOutputProvider = new CommandOutputProvider();
loggerFactory.AddProvider(CommandOutputProvider);
Logger = loggerFactory.CreateLogger<Program>();
}
public ILogger Logger
{
get { return _logger; }
set { _logger = Ensure.NotNull(value, nameof(value)); }
}
public CommandOutputProvider CommandOutputProvider
{
get { return _loggerProvider; }
set { _loggerProvider = Ensure.NotNull(value, nameof(value)); }
} }
public bool TryRun(string[] args, out int returnCode) public bool TryRun(string[] args, out int returnCode)
@ -57,8 +38,9 @@ namespace Microsoft.Extensions.SecretManager.Tools
} }
catch (Exception exception) catch (Exception exception)
{ {
Logger.LogDebug(exception.ToString()); var reporter = CreateReporter(verbose: true);
Logger.LogCritical(Resources.Error_Command_Failed, exception.Message); reporter.Verbose(exception.ToString());
reporter.Error(Resources.FormatError_Command_Failed(exception.Message));
returnCode = 1; returnCode = 1;
return false; return false;
} }
@ -66,7 +48,16 @@ namespace Microsoft.Extensions.SecretManager.Tools
internal int RunInternal(params string[] args) internal int RunInternal(params string[] args)
{ {
var options = CommandLineOptions.Parse(args, _console); CommandLineOptions options;
try
{
options = CommandLineOptions.Parse(args, _console);
}
catch (CommandParsingException ex)
{
CreateReporter(verbose: false).Error(ex.Message);
return 1;
}
if (options == null) if (options == null)
{ {
@ -78,36 +69,62 @@ namespace Microsoft.Extensions.SecretManager.Tools
return 2; return 2;
} }
if (options.IsVerbose) var reporter = CreateReporter(options.IsVerbose);
{
CommandOutputProvider.LogLevel = LogLevel.Debug;
}
string userSecretsId; string userSecretsId;
try try
{ {
userSecretsId = ResolveId(options); userSecretsId = ResolveId(options, reporter);
} }
catch (Exception ex) when (ex is InvalidOperationException || ex is FileNotFoundException) catch (Exception ex) when (ex is InvalidOperationException || ex is FileNotFoundException)
{ {
_logger.LogError(ex.Message); reporter.Error(ex.Message);
return 1; return 1;
} }
var store = new SecretsStore(userSecretsId, Logger); var store = new SecretsStore(userSecretsId, reporter);
var context = new Internal.CommandContext(store, Logger, _console); var context = new Internal.CommandContext(store, reporter, _console);
options.Command.Execute(context); options.Command.Execute(context);
return 0; return 0;
} }
internal string ResolveId(CommandLineOptions options) private IReporter CreateReporter(bool verbose)
{
return new ReporterBuilder()
.WithConsole(_console)
.Verbose(f =>
{
f.When(() => verbose);
if (!_console.IsOutputRedirected)
{
f.WithColor(ConsoleColor.DarkGray);
}
})
.Warn(f =>
{
if (!_console.IsOutputRedirected)
{
f.WithColor(ConsoleColor.Yellow);
}
})
.Error(f =>
{
if (!_console.IsErrorRedirected)
{
f.WithColor(ConsoleColor.Red);
}
})
.Build();
}
internal string ResolveId(CommandLineOptions options, IReporter reporter)
{ {
if (!string.IsNullOrEmpty(options.Id)) if (!string.IsNullOrEmpty(options.Id))
{ {
return options.Id; return options.Id;
} }
using (var resolver = new ProjectIdResolver(Logger, _workingDirectory)) using (var resolver = new ProjectIdResolver(reporter, _workingDirectory))
{ {
return resolver.Resolve(options.Project, options.Configuration); return resolver.Resolve(options.Project, options.Configuration);
} }

View File

@ -39,7 +39,6 @@
}, },
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration.UserSecrets": "1.0.0", "Microsoft.Extensions.Configuration.UserSecrets": "1.0.0",
"Microsoft.Extensions.Logging": "1.0.0",
"Microsoft.NETCore.App": { "Microsoft.NETCore.App": {
"version": "1.0.1", "version": "1.0.1",
"type": "platform" "type": "platform"

21
src/Shared/CliContext.cs Normal file
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

@ -11,6 +11,9 @@ namespace Microsoft.Extensions.CommandLineUtils
public static CommandOption HelpOption(this CommandLineApplication app) public static CommandOption HelpOption(this CommandLineApplication app)
=> app.HelpOption("-?|-h|--help"); => 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) public static void OnExecute(this CommandLineApplication app, Action action)
=> app.OnExecute(() => => app.OnExecute(() =>
{ {

View File

@ -13,5 +13,7 @@ namespace Microsoft.Extensions.Tools.Internal
TextWriter Error { get; } TextWriter Error { get; }
TextReader In { get; } TextReader In { get; }
bool IsInputRedirected { get; } bool IsInputRedirected { get; }
bool IsOutputRedirected { get; }
bool IsErrorRedirected { get; }
} }
} }

View File

@ -23,5 +23,7 @@ namespace Microsoft.Extensions.Tools.Internal
public TextReader In => Console.In; public TextReader In => Console.In;
public TextWriter Out => Console.Out; public TextWriter Out => Console.Out;
public bool IsInputRedirected => Console.IsInputRedirected; public bool IsInputRedirected => Console.IsInputRedirected;
public bool IsOutputRedirected => Console.IsOutputRedirected;
public bool IsErrorRedirected => Console.IsErrorRedirected;
} }
} }

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.Collections.Generic;
namespace Microsoft.Extensions.Tools.Internal
{
public class ColorFormatter : IFormatter
{
// resets foreground color only
private const string ResetColor = "\x1B[39m";
private static readonly IDictionary<ConsoleColor, int> AnsiColorCodes
= new Dictionary<ConsoleColor, int>
{
{ConsoleColor.Black, 30},
{ConsoleColor.DarkRed, 31},
{ConsoleColor.DarkGreen, 32},
{ConsoleColor.DarkYellow, 33},
{ConsoleColor.DarkBlue, 34},
{ConsoleColor.DarkMagenta, 35},
{ConsoleColor.DarkCyan, 36},
{ConsoleColor.Gray, 37},
{ConsoleColor.DarkGray, 90},
{ConsoleColor.Red, 91},
{ConsoleColor.Green, 92},
{ConsoleColor.Yellow, 93},
{ConsoleColor.Blue, 94},
{ConsoleColor.Magenta, 95},
{ConsoleColor.Cyan, 96},
{ConsoleColor.White, 97},
};
private readonly string _prefix;
public ColorFormatter(ConsoleColor color)
{
_prefix = GetAnsiCode(color);
}
public string Format(string text)
=> text?.Length > 0
? $"{_prefix}{text}{ResetColor}"
: text;
private static string GetAnsiCode(ConsoleColor color)
{
int code;
if (!AnsiColorCodes.TryGetValue(color, out code))
{
throw new ArgumentOutOfRangeException(nameof(color), color, null);
}
return $"\x1B[{code}m";
}
}
}

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.
namespace Microsoft.Extensions.Tools.Internal
{
public class CompositeFormatter : IFormatter
{
private readonly IFormatter[] _formatters;
public CompositeFormatter(IFormatter[] formatters)
{
Ensure.NotNull(formatters, nameof(formatters));
_formatters = formatters;
}
public string Format(string text)
{
foreach (var formatter in _formatters)
{
text = formatter.Format(text);
}
return text;
}
}
}

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;
namespace Microsoft.Extensions.Tools.Internal
{
public class ConditionalFormatter : IFormatter
{
private readonly Func<bool> _predicate;
public ConditionalFormatter(Func<bool> predicate)
{
Ensure.NotNull(predicate, nameof(predicate));
_predicate = predicate;
}
public string Format(string text)
=> _predicate()
? text
: null;
}
}

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.Extensions.Tools.Internal
{
public class DefaultFormatter : IFormatter
{
public static readonly IFormatter Instance = new DefaultFormatter();
private DefaultFormatter() {}
public string Format(string text)
=> text;
}
}

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;
namespace Microsoft.Extensions.Tools.Internal
{
public class FormatterBuilder
{
private readonly List<IFormatter> _formatters = new List<IFormatter>();
public FormatterBuilder WithColor(ConsoleColor color)
{
_formatters.Add(new ColorFormatter(color));
return this;
}
public FormatterBuilder WithPrefix(string prefix)
{
_formatters.Add(new PrefixFormatter(prefix));
return this;
}
public FormatterBuilder When(Func<bool> predicate)
{
_formatters.Add(new ConditionalFormatter(predicate));
return this;
}
public IFormatter Build()
{
if (_formatters.Count == 0)
{
return DefaultFormatter.Instance;
}
if (_formatters.Count == 1)
{
return _formatters[0];
}
return new CompositeFormatter(_formatters.ToArray());
}
}
}

View File

@ -0,0 +1,62 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
namespace Microsoft.Extensions.Tools.Internal
{
public class FormattingReporter : IReporter
{
private readonly object _writelock = new object();
private readonly IConsole _console;
private readonly IFormatter _verbose;
private readonly IFormatter _warn;
private readonly IFormatter _output;
private readonly IFormatter _error;
public FormattingReporter(IConsole console,
IFormatter verbose,
IFormatter output,
IFormatter warn,
IFormatter error)
{
Ensure.NotNull(console, nameof(console));
Ensure.NotNull(verbose, nameof(verbose));
Ensure.NotNull(output, nameof(output));
Ensure.NotNull(warn, nameof(warn));
Ensure.NotNull(error, nameof(error));
_console = console;
_verbose = verbose;
_output = output;
_warn = warn;
_error = error;
}
public void Verbose(string message)
=> Write(_console.Out, _verbose.Format(message));
public void Output(string message)
=> Write(_console.Out, _output.Format(message));
public void Warn(string message)
=> Write(_console.Out, _warn.Format(message));
public void Error(string message)
=> Write(_console.Error, _error.Format(message));
private void Write(TextWriter writer, string message)
{
if (message == null)
{
return;
}
lock (_writelock)
{
writer.WriteLine(message);
}
}
}
}

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.Tools.Internal
{
public interface IFormatter
{
string Format(string text);
}
}

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,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.
namespace Microsoft.Extensions.Tools.Internal
{
public class PrefixFormatter : IFormatter
{
private readonly string _prefix;
public PrefixFormatter(string prefix)
{
Ensure.NotNullOrEmpty(prefix, nameof(prefix));
_prefix = prefix;
}
public string Format(string text)
=> _prefix + text;
}
}

View File

@ -0,0 +1,65 @@
// Copyright (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 class ReporterBuilder
{
private readonly FormatterBuilder _verbose = new FormatterBuilder();
private readonly FormatterBuilder _output = new FormatterBuilder();
private readonly FormatterBuilder _warn = new FormatterBuilder();
private readonly FormatterBuilder _error = new FormatterBuilder();
private IConsole _console;
public ReporterBuilder WithConsole(IConsole console)
{
_console = console;
return this;
}
public FormatterBuilder Verbose() => _verbose;
public FormatterBuilder Output() => _output;
public FormatterBuilder Warn() => _warn;
public FormatterBuilder Error() => _error;
public ReporterBuilder Verbose(Action<FormatterBuilder> configure)
{
configure(_verbose);
return this;
}
public ReporterBuilder Output(Action<FormatterBuilder> configure)
{
configure(_output);
return this;
}
public ReporterBuilder Warn(Action<FormatterBuilder> configure)
{
configure(_warn);
return this;
}
public ReporterBuilder Error(Action<FormatterBuilder> configure)
{
configure(_error);
return this;
}
public IReporter Build()
{
if (_console == null)
{
throw new InvalidOperationException($"Cannot build without first calling {nameof(WithConsole)}");
}
return new FormattingReporter(_console,
_verbose.Build(),
_output.Build(),
_warn.Build(),
_error.Build());
}
}
}

View File

@ -1,35 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace System
{
public static class StringExtensions
{
public static string Black(this string text)
=> "\x1B[30m" + text + "\x1B[39m";
public static string Red(this string text)
=> "\x1B[31m" + text + "\x1B[39m";
public static string Green(this string text)
=> "\x1B[32m" + text + "\x1B[39m";
public static string Yellow(this string text)
=> "\x1B[33m" + text + "\x1B[39m";
public static string Blue(this string text)
=> "\x1B[34m" + text + "\x1B[39m";
public static string Magenta(this string text)
=> "\x1B[35m" + text + "\x1B[39m";
public static string Cyan(this string text)
=> "\x1B[36m" + text + "\x1B[39m";
public static string White(this string text)
=> "\x1B[37m" + text + "\x1B[39m";
public static string Bold(this string text)
=> "\x1B[1m" + text + "\x1B[22m";
}
}

View File

@ -4,6 +4,7 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -14,14 +15,12 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
{ {
private readonly IConsole _console; private readonly IConsole _console;
private readonly StringBuilder _stdout = new StringBuilder(); private readonly StringBuilder _stdout = new StringBuilder();
private readonly StringBuilder _stderr = new StringBuilder();
public CommandLineOptionsTests(ITestOutputHelper output) public CommandLineOptionsTests(ITestOutputHelper output)
{ {
_console = new TestConsole(output) _console = new TestConsole(output)
{ {
Out = new StringWriter(_stdout), Out = new StringWriter(_stdout),
Error = new StringWriter(_stderr),
}; };
} }
@ -57,8 +56,8 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
[Fact] [Fact]
public void CannotHaveQuietAndVerbose() public void CannotHaveQuietAndVerbose()
{ {
Assert.Null(CommandLineOptions.Parse(new[] { "--quiet", "--verbose" }, _console)); var ex = Assert.Throws<CommandParsingException>(() => CommandLineOptions.Parse(new[] { "--quiet", "--verbose" }, _console));
Assert.Contains(Resources.Error_QuietAndVerboseSpecified, _stderr.ToString()); Assert.Equal(Resources.Error_QuietAndVerboseSpecified, ex.Message);
} }
} }
} }

View File

@ -7,7 +7,7 @@ using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.DotNet.Watcher.Internal; using Microsoft.DotNet.Watcher.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Tools.Internal;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -17,11 +17,11 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
public class MsBuildFileSetFactoryTest : IDisposable public class MsBuildFileSetFactoryTest : IDisposable
{ {
private ILogger _logger; private readonly IReporter _reporter;
private readonly TemporaryDirectory _tempDir; private readonly TemporaryDirectory _tempDir;
public MsBuildFileSetFactoryTest(ITestOutputHelper output) public MsBuildFileSetFactoryTest(ITestOutputHelper output)
{ {
_logger = new XunitLogger(output); _reporter = new TestReporter(output);
_tempDir = new TemporaryDirectory(); _tempDir = new TemporaryDirectory();
} }
@ -255,7 +255,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
graph.Find("A").WithProjectReference(graph.Find("W"), watch: false); graph.Find("A").WithProjectReference(graph.Find("W"), watch: false);
var output = new OutputSink(); var output = new OutputSink();
var filesetFactory = new MsBuildFileSetFactory(_logger, graph.GetOrCreate("A").Path, output) var filesetFactory = new MsBuildFileSetFactory(_reporter, graph.GetOrCreate("A").Path, output)
{ {
// enables capturing markers to know which projects have been visited // enables capturing markers to know which projects have been visited
BuildFlags = { "/p:_DotNetWatchTraceOutput=true" } BuildFlags = { "/p:_DotNetWatchTraceOutput=true" }
@ -263,7 +263,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
var fileset = await GetFileSet(filesetFactory); var fileset = await GetFileSet(filesetFactory);
_logger.LogInformation(string.Join( _reporter.Output(string.Join(
Environment.NewLine, Environment.NewLine,
output.Current.Lines.Select(l => "Sink output: " + l))); output.Current.Lines.Select(l => "Sink output: " + l)));
@ -297,7 +297,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
} }
private Task<IFileSet> GetFileSet(TemporaryCSharpProject target) private Task<IFileSet> GetFileSet(TemporaryCSharpProject target)
=> GetFileSet(new MsBuildFileSetFactory(_logger, target.Path)); => GetFileSet(new MsBuildFileSetFactory(_reporter, target.Path));
private async Task<IFileSet> GetFileSet(MsBuildFileSetFactory filesetFactory) private async Task<IFileSet> GetFileSet(MsBuildFileSetFactory filesetFactory)
{ {
_tempDir.Create(); _tempDir.Create();

View File

@ -1,35 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace Microsoft.DotNet.Watcher.Tools.Tests
{
internal class XunitLogger : ILogger
{
private readonly ITestOutputHelper _output;
public XunitLogger(ITestOutputHelper output)
{
_output = output;
}
public IDisposable BeginScope<TState>(TState state)
=> NullScope.Instance;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
_output.WriteLine($"{logLevel}: {formatter(state, exception)}");
}
private class NullScope : IDisposable
{
private NullScope() { }
public static NullScope Instance = new NullScope();
public void Dispose() { }
}
}
}

View File

@ -7,7 +7,6 @@ using System.IO;
using System.Text; using System.Text;
using Microsoft.Extensions.Configuration.UserSecrets; using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.Configuration.UserSecrets.Tests; using Microsoft.Extensions.Configuration.UserSecrets.Tests;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -16,23 +15,24 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
{ {
public class SecretManagerTests : IClassFixture<UserSecretsTestFixture> public class SecretManagerTests : IClassFixture<UserSecretsTestFixture>
{ {
private readonly TestLogger _logger;
private readonly TestConsole _console; private readonly TestConsole _console;
private readonly UserSecretsTestFixture _fixture; private readonly UserSecretsTestFixture _fixture;
private readonly StringBuilder _output = new StringBuilder();
public SecretManagerTests(UserSecretsTestFixture fixture, ITestOutputHelper output) public SecretManagerTests(UserSecretsTestFixture fixture, ITestOutputHelper output)
{ {
_fixture = fixture; _fixture = fixture;
_logger = new TestLogger(output);
_console = new TestConsole(output); _console = new TestConsole(output)
{
Error = new StringWriter(_output),
Out = new StringWriter(_output),
};
} }
private Program CreateProgram() private Program CreateProgram()
{ {
return new Program(_console, Directory.GetCurrentDirectory()) return new Program(_console, Directory.GetCurrentDirectory());
{
Logger = _logger
};
} }
[Theory] [Theory]
@ -44,7 +44,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var secretManager = CreateProgram(); var secretManager = CreateProgram();
secretManager.RunInternal("list", "-p", project); secretManager.RunInternal("list", "-p", project);
Assert.Contains(Resources.FormatError_ProjectMissingId(project), _logger.Messages); Assert.Contains(Resources.FormatError_ProjectMissingId(project), _output.ToString());
} }
[Fact] [Fact]
@ -54,7 +54,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var secretManager = CreateProgram(); var secretManager = CreateProgram();
secretManager.RunInternal("list", "-p", project); secretManager.RunInternal("list", "-p", project);
Assert.Contains(Resources.FormatError_ProjectFailedToLoad(project), _logger.Messages); Assert.Contains(Resources.FormatError_ProjectFailedToLoad(project), _output.ToString());
} }
[Fact] [Fact]
@ -64,7 +64,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var secretManager = CreateProgram(); var secretManager = CreateProgram();
secretManager.RunInternal("list", "--project", projectPath); secretManager.RunInternal("list", "--project", projectPath);
Assert.Contains(Resources.FormatError_ProjectPath_NotFound(projectPath), _logger.Messages); Assert.Contains(Resources.FormatError_ProjectPath_NotFound(projectPath), _output.ToString());
} }
[Fact] [Fact]
@ -73,12 +73,11 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var projectPath = _fixture.GetTempSecretProject(); var projectPath = _fixture.GetTempSecretProject();
var cwd = Path.Combine(projectPath, "nested1"); var cwd = Path.Combine(projectPath, "nested1");
Directory.CreateDirectory(cwd); Directory.CreateDirectory(cwd);
var secretManager = new Program(_console, cwd) { Logger = _logger, CommandOutputProvider = _logger.CommandOutputProvider }; var secretManager = new Program(_console, cwd);
secretManager.CommandOutputProvider.LogLevel = LogLevel.Debug;
secretManager.RunInternal("list", "-p", ".." + Path.DirectorySeparatorChar, "--verbose"); secretManager.RunInternal("list", "-p", ".." + Path.DirectorySeparatorChar, "--verbose");
Assert.Contains(Resources.FormatMessage_Project_File_Path(Path.Combine(cwd, "..", "TestProject.csproj")), _logger.Messages); Assert.Contains(Resources.FormatMessage_Project_File_Path(Path.Combine(cwd, "..", "TestProject.csproj")), _output.ToString());
} }
[Theory] [Theory]
@ -98,7 +97,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var dir = fromCurrentDirectory var dir = fromCurrentDirectory
? projectPath ? projectPath
: Path.GetTempPath(); : Path.GetTempPath();
var secretManager = new Program(_console, dir) { Logger = _logger }; var secretManager = new Program(_console, dir);
foreach (var secret in secrets) foreach (var secret in secrets)
{ {
@ -108,30 +107,27 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
secretManager.RunInternal(parameters); secretManager.RunInternal(parameters);
} }
Assert.Equal(4, _logger.Messages.Count);
foreach (var keyValue in secrets) foreach (var keyValue in secrets)
{ {
Assert.Contains( Assert.Contains(
string.Format("Successfully saved {0} = {1} to the secret store.", keyValue.Key, keyValue.Value), string.Format("Successfully saved {0} = {1} to the secret store.", keyValue.Key, keyValue.Value),
_logger.Messages); _output.ToString());
} }
_logger.Messages.Clear(); _output.Clear();
var args = fromCurrentDirectory var args = fromCurrentDirectory
? new string[] { "list" } ? new string[] { "list" }
: new string[] { "list", "-p", projectPath }; : new string[] { "list", "-p", projectPath };
secretManager.RunInternal(args); secretManager.RunInternal(args);
Assert.Equal(4, _logger.Messages.Count);
foreach (var keyValue in secrets) foreach (var keyValue in secrets)
{ {
Assert.Contains( Assert.Contains(
string.Format("{0} = {1}", keyValue.Key, keyValue.Value), string.Format("{0} = {1}", keyValue.Key, keyValue.Value),
_logger.Messages); _output.ToString());
} }
// Remove secrets. // Remove secrets.
_logger.Messages.Clear(); _output.Clear();
foreach (var secret in secrets) foreach (var secret in secrets)
{ {
var parameters = fromCurrentDirectory ? var parameters = fromCurrentDirectory ?
@ -141,13 +137,12 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
} }
// Verify secrets are removed. // Verify secrets are removed.
_logger.Messages.Clear(); _output.Clear();
args = fromCurrentDirectory args = fromCurrentDirectory
? new string[] { "list" } ? new string[] { "list" }
: new string[] { "list", "-p", projectPath }; : new string[] { "list", "-p", projectPath };
secretManager.RunInternal(args); secretManager.RunInternal(args);
Assert.Equal(1, _logger.Messages.Count); Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages);
} }
[Fact] [Fact]
@ -157,17 +152,14 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var secretManager = CreateProgram(); var secretManager = CreateProgram();
secretManager.RunInternal("set", "secret1", "value1", "-p", projectPath); secretManager.RunInternal("set", "secret1", "value1", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count); Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _output.ToString());
Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _logger.Messages);
secretManager.RunInternal("set", "secret1", "value2", "-p", projectPath); secretManager.RunInternal("set", "secret1", "value2", "-p", projectPath);
Assert.Equal(2, _logger.Messages.Count); Assert.Contains("Successfully saved secret1 = value2 to the secret store.", _output.ToString());
Assert.Contains("Successfully saved secret1 = value2 to the secret store.", _logger.Messages);
_logger.Messages.Clear(); _output.Clear();
secretManager.RunInternal("list", "-p", projectPath); secretManager.RunInternal("list", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count); Assert.Contains("secret1 = value2", _output.ToString());
Assert.Contains("secret1 = value2", _logger.Messages);
} }
[Fact] [Fact]
@ -175,22 +167,19 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
{ {
string secretId; string secretId;
var projectPath = _fixture.GetTempSecretProject(out secretId); var projectPath = _fixture.GetTempSecretProject(out secretId);
_logger.SetLevel(LogLevel.Debug);
var secretManager = CreateProgram(); var secretManager = CreateProgram();
secretManager.RunInternal("-v", "set", "secret1", "value1", "-p", projectPath); secretManager.RunInternal("-v", "set", "secret1", "value1", "-p", projectPath);
Assert.Equal(3, _logger.Messages.Count); Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _output.ToString());
Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _logger.Messages); Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _output.ToString());
Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _logger.Messages); Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _output.ToString());
Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _logger.Messages); _output.Clear();
_logger.Messages.Clear();
secretManager.RunInternal("-v", "list", "-p", projectPath); secretManager.RunInternal("-v", "list", "-p", projectPath);
Assert.Equal(3, _logger.Messages.Count); Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _output.ToString());
Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _logger.Messages); Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _output.ToString());
Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _logger.Messages); Assert.Contains("secret1 = value1", _output.ToString());
Assert.Contains("secret1 = value1", _logger.Messages);
} }
[Fact] [Fact]
@ -199,8 +188,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var projectPath = _fixture.GetTempSecretProject(); var projectPath = _fixture.GetTempSecretProject();
var secretManager = CreateProgram(); var secretManager = CreateProgram();
secretManager.RunInternal("remove", "secret1", "-p", projectPath); secretManager.RunInternal("remove", "secret1", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count); Assert.Contains("Cannot find 'secret1' in the secret store.", _output.ToString());
Assert.Contains("Cannot find 'secret1' in the secret store.", _logger.Messages);
} }
[Fact] [Fact]
@ -210,14 +198,13 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var secretManager = CreateProgram(); var secretManager = CreateProgram();
secretManager.RunInternal("set", "SeCreT1", "value", "-p", projectPath); secretManager.RunInternal("set", "SeCreT1", "value", "-p", projectPath);
secretManager.RunInternal("list", "-p", projectPath); secretManager.RunInternal("list", "-p", projectPath);
Assert.Contains("SeCreT1 = value", _logger.Messages); Assert.Contains("SeCreT1 = value", _output.ToString());
secretManager.RunInternal("remove", "secret1", "-p", projectPath); secretManager.RunInternal("remove", "secret1", "-p", projectPath);
Assert.Equal(2, _logger.Messages.Count); _output.Clear();
_logger.Messages.Clear();
secretManager.RunInternal("list", "-p", projectPath); secretManager.RunInternal("list", "-p", projectPath);
Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages); Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
} }
[Fact] [Fact]
@ -230,23 +217,20 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8); File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
var secretManager = CreateProgram(); var secretManager = CreateProgram();
secretManager.RunInternal("list", "-p", projectPath); secretManager.RunInternal("list", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count); Assert.Contains("AzureAd:ClientSecret = abcd郩˙î", _output.ToString());
Assert.Contains("AzureAd:ClientSecret = abcd郩˙î", _logger.Messages);
} }
[Fact] [Fact]
public void List_Json() public void List_Json()
{ {
var output = new StringBuilder();
_console.Out = new StringWriter(output);
string id; string id;
var projectPath = _fixture.GetTempSecretProject(out id); var projectPath = _fixture.GetTempSecretProject(out id);
var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id); var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id);
Directory.CreateDirectory(Path.GetDirectoryName(secretsFile)); Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8); File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
var secretManager = new Program(_console, Path.GetDirectoryName(projectPath)) { Logger = _logger }; var secretManager = new Program(_console, Path.GetDirectoryName(projectPath));
secretManager.RunInternal("list", "--id", id, "--json"); secretManager.RunInternal("list", "--id", id, "--json");
var stdout = output.ToString(); var stdout = _output.ToString();
Assert.Contains("//BEGIN", stdout); Assert.Contains("//BEGIN", stdout);
Assert.Contains(@"""AzureAd:ClientSecret"": ""abcd郩˙î""", stdout); Assert.Contains(@"""AzureAd:ClientSecret"": ""abcd郩˙î""", stdout);
Assert.Contains("//END", stdout); Assert.Contains("//END", stdout);
@ -262,11 +246,9 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8); File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
var secretManager = CreateProgram(); var secretManager = CreateProgram();
secretManager.RunInternal("set", "AzureAd:ClientSecret", "¡™£¢∞", "-p", projectPath); secretManager.RunInternal("set", "AzureAd:ClientSecret", "¡™£¢∞", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count);
secretManager.RunInternal("list", "-p", projectPath); secretManager.RunInternal("list", "-p", projectPath);
Assert.Equal(2, _logger.Messages.Count); Assert.Contains("AzureAd:ClientSecret = ¡™£¢∞", _output.ToString());
Assert.Contains("AzureAd:ClientSecret = ¡™£¢∞", _logger.Messages);
var fileContents = File.ReadAllText(secretsFile, Encoding.UTF8); var fileContents = File.ReadAllText(secretsFile, Encoding.UTF8);
Assert.Equal(@"{ Assert.Equal(@"{
""AzureAd:ClientSecret"": ""¡£¢"" ""AzureAd:ClientSecret"": ""¡£¢""
@ -280,8 +262,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var projectPath = _fixture.GetTempSecretProject(); var projectPath = _fixture.GetTempSecretProject();
var secretManager = CreateProgram(); var secretManager = CreateProgram();
secretManager.RunInternal("list", "-p", projectPath); secretManager.RunInternal("list", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count); Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages);
} }
[Theory] [Theory]
@ -295,7 +276,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
? projectPath ? projectPath
: Path.GetTempPath(); : Path.GetTempPath();
var secretManager = new Program(_console, dir) { Logger = _logger }; var secretManager = new Program(_console, dir);
var secrets = new KeyValuePair<string, string>[] var secrets = new KeyValuePair<string, string>[]
{ {
@ -313,39 +294,34 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
secretManager.RunInternal(parameters); secretManager.RunInternal(parameters);
} }
Assert.Equal(4, _logger.Messages.Count);
foreach (var keyValue in secrets) foreach (var keyValue in secrets)
{ {
Assert.Contains( Assert.Contains(
string.Format("Successfully saved {0} = {1} to the secret store.", keyValue.Key, keyValue.Value), string.Format("Successfully saved {0} = {1} to the secret store.", keyValue.Key, keyValue.Value),
_logger.Messages); _output.ToString());
} }
// Verify secrets are persisted. // Verify secrets are persisted.
_logger.Messages.Clear(); _output.Clear();
var args = fromCurrentDirectory ? var args = fromCurrentDirectory ?
new string[] { "list" } : new string[] { "list" } :
new string[] { "list", "-p", projectPath }; new string[] { "list", "-p", projectPath };
secretManager.RunInternal(args); secretManager.RunInternal(args);
Assert.Equal(4, _logger.Messages.Count);
foreach (var keyValue in secrets) foreach (var keyValue in secrets)
{ {
Assert.Contains( Assert.Contains(
string.Format("{0} = {1}", keyValue.Key, keyValue.Value), string.Format("{0} = {1}", keyValue.Key, keyValue.Value),
_logger.Messages); _output.ToString());
} }
// Clear secrets. // Clear secrets.
_logger.Messages.Clear(); _output.Clear();
args = fromCurrentDirectory ? new string[] { "clear" } : new string[] { "clear", "-p", projectPath }; args = fromCurrentDirectory ? new string[] { "clear" } : new string[] { "clear", "-p", projectPath };
secretManager.RunInternal(args); secretManager.RunInternal(args);
Assert.Equal(0, _logger.Messages.Count);
args = fromCurrentDirectory ? new string[] { "list" } : new string[] { "list", "-p", projectPath }; args = fromCurrentDirectory ? new string[] { "list" } : new string[] { "list", "-p", projectPath };
secretManager.RunInternal(args); secretManager.RunInternal(args);
Assert.Equal(1, _logger.Messages.Count); Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages);
} }
} }
} }

View File

@ -1,11 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // 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.IO;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.Extensions.SecretManager.Tools.Internal; using Microsoft.Extensions.SecretManager.Tools.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -36,10 +34,10 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
IsInputRedirected = true, IsInputRedirected = true,
In = new StringReader(input) In = new StringReader(input)
}; };
var secretStore = new TestSecretsStore(); var secretStore = new TestSecretsStore(_output);
var command = new SetCommand.FromStdInStrategy(); var command = new SetCommand.FromStdInStrategy();
command.Execute(new CommandContext(secretStore, NullLogger.Instance, testConsole)); command.Execute(new CommandContext(secretStore, new TestReporter(_output), testConsole));
Assert.Equal(3, secretStore.Count); Assert.Equal(3, secretStore.Count);
Assert.Equal("str value", secretStore["Key1"]); Assert.Equal("str value", secretStore["Key1"]);
@ -63,10 +61,10 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
IsInputRedirected = true, IsInputRedirected = true,
In = new StringReader(input) In = new StringReader(input)
}; };
var secretStore = new TestSecretsStore(); var secretStore = new TestSecretsStore(_output);
var command = new SetCommand.FromStdInStrategy(); var command = new SetCommand.FromStdInStrategy();
command.Execute(new CommandContext(secretStore, NullLogger.Instance, testConsole)); command.Execute(new CommandContext(secretStore, new TestReporter(_output), testConsole));
Assert.Equal(3, secretStore.Count); Assert.Equal(3, secretStore.Count);
Assert.True(secretStore.ContainsKey("Key1:nested")); Assert.True(secretStore.ContainsKey("Key1:nested"));
@ -89,8 +87,8 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
private class TestSecretsStore : SecretsStore private class TestSecretsStore : SecretsStore
{ {
public TestSecretsStore() public TestSecretsStore(ITestOutputHelper output)
: base("xyz", NullLogger.Instance) : base("xyz", new TestReporter(output))
{ {
} }
@ -105,25 +103,4 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
} }
} }
} }
public class NullLogger : ILogger
{
public static NullLogger Instance = new NullLogger();
private class NullScope : IDisposable
{
public void Dispose()
{
}
}
public IDisposable BeginScope<TState>(TState state)
=> new NullScope();
public bool IsEnabled(LogLevel logLevel)
=> true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
}
}
} }

View File

@ -1,50 +0,0 @@
// Copyright (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 Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace Microsoft.Extensions.SecretManager.Tools.Tests
{
public class TestLogger : ILogger
{
private readonly ILogger _wrapped;
private readonly ITestOutputHelper _output;
public TestLogger(ITestOutputHelper output = null)
{
CommandOutputProvider = new CommandOutputProvider();
_wrapped = CommandOutputProvider.CreateLogger("");
_output = output;
}
public CommandOutputProvider CommandOutputProvider { get;}
public void SetLevel(LogLevel level)
{
CommandOutputProvider.LogLevel = level;
}
public List<string> Messages { get; set; } = new List<string>();
public IDisposable BeginScope<TState>(TState state)
{
throw new NotImplementedException();
}
public bool IsEnabled(LogLevel logLevel)
{
return _wrapped.IsEnabled(logLevel);
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (IsEnabled(logLevel))
{
Messages.Add(formatter(state, exception));
_output?.WriteLine(formatter(state, exception));
}
}
}
}

View File

@ -4,7 +4,7 @@
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
using Xunit; using Xunit;
namespace Microsoft.DotNet.Watcher.Tools.Tests namespace Microsoft.Extensions.Tools.Tests
{ {
public class ArgumentEscaperTests public class ArgumentEscaperTests
{ {

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>{A24BF1D1-4326-4455-A528-09F1E20EDC83}</ProjectGuid>
<ProjectTypeGuids>{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}</ProjectTypeGuids>
<RootNamespace>Microsoft.Extensions.Tools.Tests</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,103 @@
// Copyright (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;
[Theory]
[InlineData(ConsoleColor.DarkGray, "\x1B[90m")]
[InlineData(ConsoleColor.Red, "\x1B[91m")]
[InlineData(ConsoleColor.Yellow, "\x1B[93m")]
public void WrapsWithAnsiColorCode(ConsoleColor color, string code)
{
Assert.Equal($"{code}sample\x1B[39m", new ColorFormatter(color).Format("sample"));
}
[Fact]
public void SkipsColorCodesForEmptyOrNullInput()
{
var formatter = new ColorFormatter(ConsoleColor.Blue);
Assert.Empty(formatter.Format(string.Empty));
Assert.Null(formatter.Format(null));
}
[Fact]
public void WritesToStandardStreams()
{
var testConsole = new TestConsole();
var reporter = new FormattingReporter(testConsole,
DefaultFormatter.Instance, DefaultFormatter.Instance,
DefaultFormatter.Instance, DefaultFormatter.Instance);
// 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();
}
[Fact]
public void FailsToBuildWithoutConsole()
{
Assert.Throws<InvalidOperationException>(
() => new ReporterBuilder().Build());
}
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 TextWriter Out { get; }
public TextWriter Error { get; }
public TextReader In { get; }
public bool IsInputRedirected { get; }
public bool IsOutputRedirected { get; }
public bool IsErrorRedirected { get; }
}
}
}

View File

@ -0,0 +1,23 @@
{
"buildOptions": {
"warningsAsErrors": true,
"keyFile": "../../tools/Key.snk",
"compile": "../../src/Shared/**/*.cs"
},
"dependencies": {
"dotnet-test-xunit": "2.2.0-preview2-build1029",
"Microsoft.DotNet.InternalAbstractions": "1.0.500-preview2-1-003177",
"xunit": "2.2.0-beta3-build3402"
},
"testRunner": "xunit",
"frameworks": {
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.1",
"type": "platform"
}
}
}
}
}

View File

@ -25,6 +25,8 @@ namespace Microsoft.Extensions.Tools.Internal
public TextWriter Out { get; set; } public TextWriter Out { get; set; }
public TextReader In { get; set; } = new StringReader(string.Empty); public TextReader In { get; set; } = new StringReader(string.Empty);
public bool IsInputRedirected { get; set; } = false; public bool IsInputRedirected { get; set; } = false;
public bool IsOutputRedirected { get; } = false;
public bool IsErrorRedirected { get; } = false;
public ConsoleCancelEventArgs ConsoleCancelKey() public ConsoleCancelEventArgs ConsoleCancelKey()
{ {

View File

@ -0,0 +1,37 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit.Abstractions;
namespace Microsoft.Extensions.Tools.Internal
{
public class TestReporter : IReporter
{
private readonly ITestOutputHelper _output;
public TestReporter(ITestOutputHelper output)
{
_output = output;
}
public void Verbose(string message)
{
_output.WriteLine("verbose: " + message);
}
public void Output(string message)
{
_output.WriteLine("output: " + message);
}
public void Warn(string message)
{
_output.WriteLine("warn: " + message);
}
public void Error(string message)
{
_output.WriteLine("error: " + message);
}
}
}