diff --git a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj index df0ca9fd..c0269e39 100644 --- a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj +++ b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj @@ -17,12 +17,11 @@ - - - - - + + + + + diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 8b9af7a5..06182ab1 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -1,84 +1,122 @@ using ConsoleAppFramework; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using System.Diagnostics; -args = ["hello"]; +// Use SigNoz profiling: (view: http://localhost:8080/ ) +// git clone https://github.com/SigNoz/signoz.git +// cd signoz/deploy/docker +// docker compose up +Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"); // 4317 or 4318 -var builder = ConsoleApp.Create(); +var builder = Host.CreateApplicationBuilder(args); -builder.UseFilter(); -builder.UseFilter(); +builder.Logging.AddOpenTelemetry(logging => +{ + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; +}); -builder.Add(); +builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => + { + resource.AddService("ConsoleAppFramework Telemetry Sample"); + }) + .WithMetrics(metrics => + { + metrics.AddRuntimeInstrumentation() + .AddHttpClientInstrumentation() + .AddOtlpExporter(); + }) + .WithTracing(tracing => + { + tracing.SetSampler(new AlwaysOnSampler()) + .AddHttpClientInstrumentation() + .AddSource(ConsoleAppFrameworkSampleActivitySource.Name) + .AddOtlpExporter(); + }) + .WithLogging(logging => + { + logging.AddOtlpExporter(); + }); -await builder.RunAsync(args); +var app = builder.ToConsoleAppBuilder(); -[ConsoleAppFilter] -[ConsoleAppFilter] -public class MyClass -{ - [ConsoleAppFilter] - [ConsoleAppFilter] - public void Hello() - { - Console.Write("abcde"); - } -} +var consoleAppLoger = ConsoleApp.ServiceProvider.GetRequiredService>(); // already built service provider. +ConsoleApp.Log = msg => consoleAppLoger.LogDebug(msg); +ConsoleApp.LogError = msg => consoleAppLoger.LogError(msg); -internal class NopFilter1(ConsoleAppFilter next) - : ConsoleAppFilter(next) -{ - public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) - { - Console.Write(1); - return Next.InvokeAsync(context, cancellationToken); - } -} +app.UseFilter(); -internal class NopFilter2(ConsoleAppFilter next) - : ConsoleAppFilter(next) +app.Add("", async ([FromServices] ILogger logger/*, CancellationToken cancellationToken*/) => { - public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) - { - Console.Write(2); - return Next.InvokeAsync(context, cancellationToken); - } -} + var cancellationToken = CancellationToken.None; -internal class NopFilter3(ConsoleAppFilter next) - : ConsoleAppFilter(next) -{ - public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) - { - Console.Write(3); - return Next.InvokeAsync(context, cancellationToken); - } -} + using var httpClient = new HttpClient(); + var ms = await httpClient.GetStringAsync("https://www.microsoft.com", cancellationToken); + logger.LogInformation(ms); + var google = await httpClient.GetStringAsync("https://www.google.com", cancellationToken); + logger.LogInformation(google); -internal class NopFilter4(ConsoleAppFilter next) - : ConsoleAppFilter(next) -{ - public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) - { - Console.Write(4); - return Next.InvokeAsync(context, cancellationToken); - } -} + var ms2 = httpClient.GetStringAsync("https://www.microsoft.com", cancellationToken); + var google2 = httpClient.GetStringAsync("https://www.google.com", cancellationToken); + var apple2 = httpClient.GetStringAsync("https://www.apple.com", cancellationToken); + await Task.WhenAll(ms2, google2, apple2); -internal class NopFilter5(ConsoleAppFilter next) - : ConsoleAppFilter(next) + logger.LogInformation(apple2.Result); + + logger.LogInformation("OK"); +}); + +await app.RunAsync(args); + +public static class ConsoleAppFrameworkSampleActivitySource { - public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) - { - Console.Write(5); - return Next.InvokeAsync(context, cancellationToken); - } + public const string Name = "ConsoleAppFrameworkSample"; + + public static ActivitySource Instance { get; } = new ActivitySource(Name); } -internal class NopFilter6(ConsoleAppFilter next) - : ConsoleAppFilter(next) +// Sample Activity filter +internal class CommandTracingFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) { - Console.Write(6); - return Next.InvokeAsync(context, cancellationToken); + using var activity = ConsoleAppFrameworkSampleActivitySource.Instance.StartActivity("CommandStart"); + if (activity != null) + { + activity.SetTag("console_app.command_name", context.CommandName); + activity.AddBaggage("console_app.command_args", string.Join(" ", context.EscapedArguments)); + } + + try + { + await Next.InvokeAsync(context, cancellationToken); + if (activity != null) + { + activity.SetStatus(ActivityStatusCode.Ok); + } + } + catch (Exception ex) + { + if (activity != null) + { + if (ex is OperationCanceledException) + { + activity.SetStatus(ActivityStatusCode.Error, "Canceled"); + } + else + { + activity.AddException(ex); + activity.SetStatus(ActivityStatusCode.Error); + } + } + throw; + } } } diff --git a/sandbox/GeneratorSandbox/Properties/launchSettings.json b/sandbox/GeneratorSandbox/Properties/launchSettings.json index 91cb9250..3ab90ae0 100644 --- a/sandbox/GeneratorSandbox/Properties/launchSettings.json +++ b/sandbox/GeneratorSandbox/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Default": { "commandName": "Project", - "commandLineArgs": "run --project foo.csproj -- --foo 100 --bar bazbaz" + "commandLineArgs": "" }, "ShowVersion": { "commandName": "Project", diff --git a/src/ConsoleAppFramework/ConsoleAppBaseCode.cs b/src/ConsoleAppFramework/ConsoleAppBaseCode.cs index 13698809..ae5e2258 100644 --- a/src/ConsoleAppFramework/ConsoleAppBaseCode.cs +++ b/src/ConsoleAppFramework/ConsoleAppBaseCode.cs @@ -371,9 +371,9 @@ static void ShowVersion() static partial void ShowHelp(int helpId); - static async Task RunWithFilterAsync(string commandName, string[] args, int commandDepth, int escapeIndex, ConsoleAppFilter invoker) + static async Task RunWithFilterAsync(string commandName, string[] args, int commandDepth, int escapeIndex, ConsoleAppFilter invoker, CancellationToken cancellationToken) { - using var posixSignalHandler = PosixSignalHandler.Register(Timeout); + using var posixSignalHandler = PosixSignalHandler.Register(Timeout, cancellationToken); try { await Task.Run(() => invoker.InvokeAsync(new ConsoleAppContext(commandName, args, null, commandDepth, escapeIndex), posixSignalHandler.Token)).WaitAsync(posixSignalHandler.TimeoutToken); @@ -411,16 +411,16 @@ sealed class PosixSignalHandler : IDisposable PosixSignalRegistration? sigQuit; PosixSignalRegistration? sigTerm; - PosixSignalHandler(TimeSpan timeout) + PosixSignalHandler(TimeSpan timeout, CancellationToken cancellationToken) { - this.cancellationTokenSource = new CancellationTokenSource(); + this.cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); this.timeoutCancellationTokenSource = new CancellationTokenSource(); this.timeout = timeout; } - public static PosixSignalHandler Register(TimeSpan timeout) + public static PosixSignalHandler Register(TimeSpan timeout, CancellationToken cancellationToken) { - var handler = new PosixSignalHandler(timeout); + var handler = new PosixSignalHandler(timeout, cancellationToken); Action handleSignal = handler.HandlePosixSignal; @@ -443,6 +443,7 @@ public void Dispose() sigInt?.Dispose(); sigQuit?.Dispose(); sigTerm?.Dispose(); + cancellationTokenSource.Dispose(); timeoutCancellationTokenSource.Dispose(); } } @@ -482,10 +483,10 @@ public void UseFilter() where T : ConsoleAppFilter { } partial void AddCore(string commandName, Delegate command); [MethodImpl(MethodImplOptions.AggressiveInlining)] - partial void RunCore(string[] args); + partial void RunCore(string[] args, CancellationToken cancellationToken); [MethodImpl(MethodImplOptions.AggressiveInlining)] - partial void RunAsyncCore(string[] args, ref Task result); + partial void RunAsyncCore(string[] args, CancellationToken cancellationToken, ref Task result); partial void BuildAndSetServiceProvider(); @@ -546,15 +547,14 @@ internal static partial class ConsoleApp internal partial class ConsoleAppBuilder { public void Run(string[] args) => Run(args, true); + public void Run(string[] args, CancellationToken cancellationToken) => Run(args, true, cancellationToken); - public Task RunAsync(string[] args) => RunAsync(args, true); - - public void Run(string[] args, bool disposeService) + public void Run(string[] args, bool disposeService, CancellationToken cancellationToken = default) { BuildAndSetServiceProvider(); try { - RunCore(args); + RunCore(args, cancellationToken); } finally { @@ -568,13 +568,16 @@ public void Run(string[] args, bool disposeService) } } - public async Task RunAsync(string[] args, bool disposeService) + public Task RunAsync(string[] args) => RunAsync(args, true); + public Task RunAsync(string[] args, CancellationToken cancellationToken) => RunAsync(args, true, cancellationToken); + + public async Task RunAsync(string[] args, bool disposeService, CancellationToken cancellationToken = default) { BuildAndSetServiceProvider(); try { Task? task = null; - RunAsyncCore(args, ref task!); + RunAsyncCore(args, cancellationToken, ref task!); if (task != null) { await task; @@ -621,8 +624,9 @@ internal static partial class ConsoleApp internal partial class ConsoleAppBuilder { public void Run(string[] args) => Run(args, true, true, true); + public void Run(string[] args, CancellationToken cancellationToken) => Run(args, true, true, true, cancellationToken); - public void Run(string[] args, bool startHost, bool stopHost, bool disposeService) + public void Run(string[] args, bool startHost, bool stopHost, bool disposeService, CancellationToken cancellationToken = default) { BuildAndSetServiceProvider(); Microsoft.Extensions.Hosting.IHost? host = ConsoleApp.ServiceProvider?.GetService(typeof(Microsoft.Extensions.Hosting.IHost)) as Microsoft.Extensions.Hosting.IHost; @@ -632,7 +636,7 @@ public void Run(string[] args, bool startHost, bool stopHost, bool disposeServic { host?.StartAsync().GetAwaiter().GetResult(); } - RunCore(args); + RunCore(args, cancellationToken); } finally { @@ -651,8 +655,9 @@ public void Run(string[] args, bool startHost, bool stopHost, bool disposeServic } public Task RunAsync(string[] args) => RunAsync(args, true, true, true); + public Task RunAsync(string[] args, CancellationToken cancellationToken) => RunAsync(args, true, true, true, cancellationToken); - public async Task RunAsync(string[] args, bool startHost, bool stopHost, bool disposeService) + public async Task RunAsync(string[] args, bool startHost, bool stopHost, bool disposeService, CancellationToken cancellationToken = default) { BuildAndSetServiceProvider(); Microsoft.Extensions.Hosting.IHost? host = ConsoleApp.ServiceProvider?.GetService(typeof(Microsoft.Extensions.Hosting.IHost)) as Microsoft.Extensions.Hosting.IHost; @@ -663,7 +668,7 @@ public async Task RunAsync(string[] args, bool startHost, bool stopHost, bool di await host?.StartAsync(); } Task? task = null; - RunAsyncCore(args, ref task!); + RunAsyncCore(args, cancellationToken, ref task!); if (task != null) { await task; diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index 21cc004d..3167a7a8 100644 --- a/src/ConsoleAppFramework/Emitter.cs +++ b/src/ConsoleAppFramework/Emitter.cs @@ -42,7 +42,9 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy } var commandDepthEscapeIndex = emitForBuilder ? ", int commandDepth, int escapeIndex" : ""; - var filterCancellationToken = command.HasFilter ? ", ConsoleAppContext context, CancellationToken cancellationToken" : ""; + var filterCancellationToken = command.HasFilter ? ", ConsoleAppContext context, CancellationToken cancellationToken" + : emitForBuilder ? ", CancellationToken __ExternalCancellationToken__" + : ""; if (!emitForBuilder) { @@ -83,7 +85,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy // prepare argument variables if (hasCancellationToken) { - sb.AppendLine("using var posixSignalHandler = PosixSignalHandler.Register(Timeout);"); + sb.AppendLine("using var posixSignalHandler = PosixSignalHandler.Register(Timeout, __ExternalCancellationToken__);"); } if (hasConsoleAppContext) { @@ -412,7 +414,7 @@ public void EmitBuilder(SourceBuilder sb, CommandWithId[] commandIds, bool emitS if (emitSync) { sb.AppendLine(); - using (sb.BeginBlock("partial void RunCore(string[] args)")) + using (sb.BeginBlock("partial void RunCore(string[] args, CancellationToken cancellationToken)")) { if (hasRootCommand) { @@ -431,7 +433,7 @@ public void EmitBuilder(SourceBuilder sb, CommandWithId[] commandIds, bool emitS if (emitAsync) { sb.AppendLine(); - using (sb.BeginBlock("partial void RunAsyncCore(string[] args, ref Task result)")) + using (sb.BeginBlock("partial void RunAsyncCore(string[] args, CancellationToken cancellationToken, ref Task result)")) { if (hasRootCommand) { @@ -559,16 +561,16 @@ void EmitLeafCommand(CommandWithId? command) { if (!isRunAsync) { - sb.AppendLine($"RunCommand{command.Id}(args, {depth}, args.AsSpan().IndexOf(\"--\"){commandArgs});"); + sb.AppendLine($"RunCommand{command.Id}(args, {depth}, args.AsSpan().IndexOf(\"--\"){commandArgs}, cancellationToken);"); } else { - sb.AppendLine($"result = RunCommand{command.Id}Async(args, {depth}, args.AsSpan().IndexOf(\"--\"){commandArgs});"); + sb.AppendLine($"result = RunCommand{command.Id}Async(args, {depth}, args.AsSpan().IndexOf(\"--\"){commandArgs}, cancellationToken);"); } } else { - var invokeCode = $"RunWithFilterAsync(\"{command.Command.Name}\", args, {depth}, args.AsSpan().IndexOf(\"--\"), new Command{command.Id}Invoker({commandArgs.TrimStart(',', ' ')}).BuildFilter())"; + var invokeCode = $"RunWithFilterAsync(\"{command.Command.Name}\", args, {depth}, args.AsSpan().IndexOf(\"--\"), new Command{command.Id}Invoker({commandArgs.TrimStart(',', ' ')}).BuildFilter(), cancellationToken)"; if (!isRunAsync) { sb.AppendLine($"{invokeCode}.GetAwaiter().GetResult();");