diff --git a/src/Process.Tests.Dummy/AsyncOutputCommand.cs b/src/Process.Tests.Dummy/AsyncOutputCommand.cs new file mode 100644 index 0000000..139652e --- /dev/null +++ b/src/Process.Tests.Dummy/AsyncOutputCommand.cs @@ -0,0 +1,12 @@ +using Spectre.Console.Cli; + +namespace Geekeey.Extensions.Process.Tests.Dummy; + +internal abstract class AsyncOutputCommand : AsyncCommand where T : OutputCommandSettings +{ +} + +internal abstract class OutputCommandSettings : CommandSettings +{ + [CommandOption("--target")] public OutputTarget Target { get; init; } = OutputTarget.StdOut; +} \ No newline at end of file diff --git a/src/Process.Tests.Dummy/Commands/EchoCommand.cs b/src/Process.Tests.Dummy/Commands/EchoCommand.cs index ce55e0e..d38f2fa 100644 --- a/src/Process.Tests.Dummy/Commands/EchoCommand.cs +++ b/src/Process.Tests.Dummy/Commands/EchoCommand.cs @@ -2,20 +2,21 @@ using Spectre.Console.Cli; namespace Geekeey.Extensions.Process.Tests.Dummy.Commands; -internal sealed class EchoCommand : Command +internal sealed class EchoCommand : AsyncOutputCommand { - public sealed class Settings : CommandSettings + public sealed class Settings : OutputCommandSettings { - [CommandOption("--target")] public OutputTarget Target { get; init; } = OutputTarget.StdOut; - [CommandOption("--separator ")] public string Separator { get; init; } = " "; + [CommandOption("--separator ")] public string Separator { get; init; } = " "; [CommandArgument(0, "[line]")] public string[] Items { get; init; } = []; } - public override int Execute(CommandContext context, Settings settings) + public override async Task ExecuteAsync(CommandContext context, Settings settings) { - foreach (var writer in settings.Target.GetWriters()) + using var tty = Terminal.Connect(); + + foreach (var writer in tty.GetWriters(settings.Target)) { - writer.WriteLine(string.Join(settings.Separator, settings.Items)); + await writer.WriteLineAsync(string.Join(settings.Separator, settings.Items)); } return 0; diff --git a/src/Process.Tests.Dummy/Commands/EchoStdinCommand.cs b/src/Process.Tests.Dummy/Commands/EchoStdinCommand.cs index 2680118..8eea31a 100644 --- a/src/Process.Tests.Dummy/Commands/EchoStdinCommand.cs +++ b/src/Process.Tests.Dummy/Commands/EchoStdinCommand.cs @@ -4,30 +4,30 @@ using Spectre.Console.Cli; namespace Geekeey.Extensions.Process.Tests.Dummy.Commands; -internal sealed class EchoStdinCommand : Command +internal sealed class EchoStdinCommand : AsyncOutputCommand { - public sealed class Settings : CommandSettings + public sealed class Settings : OutputCommandSettings { - [CommandOption("--target")] public OutputTarget Target { get; init; } = OutputTarget.StdOut; [CommandOption("--length")] public long Length { get; init; } = long.MaxValue; } - public override int Execute(CommandContext context, Settings settings) + public override async Task ExecuteAsync(CommandContext context, Settings settings) { - using var buffer = MemoryPool.Shared.Rent(81920); + using var tty = Terminal.Connect(); + using var buffer = MemoryPool.Shared.Rent(81920); var count = 0L; while (count < settings.Length) { var bytesWanted = (int)Math.Min(buffer.Memory.Length, settings.Length - count); - var bytesRead = Console.In.Read(buffer.Memory.Span[..bytesWanted]); + var bytesRead = await tty.Stdin.BaseStream.ReadAsync(buffer.Memory[..bytesWanted]); if (bytesRead <= 0) break; - foreach (var writer in settings.Target.GetWriters()) + foreach (var writer in tty.GetWriters(settings.Target)) { - writer.Write(buffer.Memory.Span[..bytesRead]); + await writer.BaseStream.WriteAsync(buffer.Memory[..bytesRead]); } count += bytesRead; diff --git a/src/Process.Tests.Dummy/Commands/EnvironmentCommand.cs b/src/Process.Tests.Dummy/Commands/EnvironmentCommand.cs index 6ab7d15..36d8fb1 100644 --- a/src/Process.Tests.Dummy/Commands/EnvironmentCommand.cs +++ b/src/Process.Tests.Dummy/Commands/EnvironmentCommand.cs @@ -2,18 +2,25 @@ using Spectre.Console.Cli; namespace Geekeey.Extensions.Process.Tests.Dummy.Commands; -internal sealed class EnvironmentCommand : Command +internal sealed class EnvironmentCommand : AsyncOutputCommand { - public sealed class Settings : CommandSettings + public sealed class Settings : OutputCommandSettings { [CommandArgument(0, "")] public string[] Variables { get; init; } = []; } - public override int Execute(CommandContext context, Settings settings) + public override async Task ExecuteAsync(CommandContext context, Settings settings) { + using var tty = Terminal.Connect(); + foreach (var name in settings.Variables) { - Console.Out.WriteLine(Environment.GetEnvironmentVariable(name) ?? string.Empty); + var value = Environment.GetEnvironmentVariable(name) ?? string.Empty; + + foreach (var writer in tty.GetWriters(settings.Target)) + { + await writer.WriteLineAsync(value); + } } return 0; diff --git a/src/Process.Tests.Dummy/Commands/ExitCommand.cs b/src/Process.Tests.Dummy/Commands/ExitCommand.cs index 889aa3c..e4b8884 100644 --- a/src/Process.Tests.Dummy/Commands/ExitCommand.cs +++ b/src/Process.Tests.Dummy/Commands/ExitCommand.cs @@ -2,16 +2,19 @@ using Spectre.Console.Cli; namespace Geekeey.Extensions.Process.Tests.Dummy.Commands; -internal sealed class ExitCommand : Command +internal sealed class ExitCommand : AsyncCommand { public sealed class Settings : CommandSettings { [CommandArgument(1, "")] public int Code { get; init; } } - public override int Execute(CommandContext context, Settings settings) + public override async Task ExecuteAsync(CommandContext context, Settings settings) { - Console.Error.WriteLine($"Exit code set to {settings.Code}"); + using var tty = Terminal.Connect(); + + await tty.Stderr.WriteLineAsync($"Exit code set to {settings.Code}"); + return settings.Code; } } \ No newline at end of file diff --git a/src/Process.Tests.Dummy/Commands/GenerateBlobCommand.cs b/src/Process.Tests.Dummy/Commands/GenerateBlobCommand.cs index a84d211..17add44 100644 --- a/src/Process.Tests.Dummy/Commands/GenerateBlobCommand.cs +++ b/src/Process.Tests.Dummy/Commands/GenerateBlobCommand.cs @@ -5,33 +5,31 @@ using Spectre.Console.Cli; namespace Geekeey.Extensions.Process.Tests.Dummy.Commands; -internal sealed class GenerateBlobCommand : Command +internal sealed class GenerateBlobCommand : AsyncOutputCommand { private readonly Random _random = new(1234567); - public sealed class Settings : CommandSettings + public sealed class Settings : OutputCommandSettings { - [CommandOption("--target")] public OutputTarget Target { get; init; } = OutputTarget.StdOut; [CommandOption("--length")] public long Length { get; init; } = 100_000; [CommandOption("--buffer")] public int BufferSize { get; init; } = 1024; } - public override int Execute(CommandContext context, Settings settings) + public override async Task ExecuteAsync(CommandContext context, Settings settings) { + using var tty = Terminal.Connect(); + using var bytes = MemoryPool.Shared.Rent(settings.BufferSize); - using var chars = MemoryPool.Shared.Rent(settings.BufferSize); var total = 0L; while (total < settings.Length) { _random.NextBytes(bytes.Memory.Span); - Encoding.UTF8.GetChars(bytes.Memory.Span, chars.Memory.Span); - - var count = (int)Math.Min(chars.Memory.Length, settings.Length - total); - foreach (var writer in settings.Target.GetWriters()) + var count = (int)Math.Min(bytes.Memory.Length, settings.Length - total); + foreach (var writer in tty.GetWriters(settings.Target)) { - writer.Write(chars.Memory[..count]); + await writer.BaseStream.WriteAsync(bytes.Memory[..count]); } total += count; diff --git a/src/Process.Tests.Dummy/Commands/GenerateClobCommand.cs b/src/Process.Tests.Dummy/Commands/GenerateClobCommand.cs index 166182f..f8acbc4 100644 --- a/src/Process.Tests.Dummy/Commands/GenerateClobCommand.cs +++ b/src/Process.Tests.Dummy/Commands/GenerateClobCommand.cs @@ -5,20 +5,21 @@ using Spectre.Console.Cli; namespace Geekeey.Extensions.Process.Tests.Dummy.Commands; -internal sealed class GenerateClobCommand : Command +internal sealed class GenerateClobCommand : AsyncOutputCommand { private readonly Random _random = new(1234567); private readonly char[] _chars = Enumerable.Range(32, 94).Select(i => (char)i).ToArray(); - public sealed class Settings : CommandSettings + public sealed class Settings : OutputCommandSettings { - [CommandOption("--target")] public OutputTarget Target { get; init; } = OutputTarget.StdOut; [CommandOption("--length")] public int Length { get; init; } = 100_000; [CommandOption("--lines")] public int LinesCount { get; init; } = 1; } - public override int Execute(CommandContext context, Settings settings) + public override async Task ExecuteAsync(CommandContext context, Settings settings) { + using var tty = Terminal.Connect(); + var buffer = new StringBuilder(settings.Length); for (var line = 0; line < settings.LinesCount; line++) @@ -30,9 +31,9 @@ internal sealed class GenerateClobCommand : Command +internal sealed class LengthCommand : AsyncOutputCommand { - public sealed class Settings : CommandSettings + public sealed class Settings : OutputCommandSettings { } - public override int Execute(CommandContext context, Settings settings) + public override async Task ExecuteAsync(CommandContext context, Settings settings) { + using var tty = Terminal.Connect(); + using var buffer = MemoryPool.Shared.Rent(81920); - using var stdin = Console.OpenStandardInput(); var count = 0L; while (true) { - var bytesRead = stdin.Read(buffer.Memory.Span); + var bytesRead = await tty.Stdin.BaseStream.ReadAsync(buffer.Memory); if (bytesRead <= 0) break; count += bytesRead; } - Console.Out.WriteLine(count.ToString(CultureInfo.InvariantCulture)); + foreach (var writer in tty.GetWriters(settings.Target)) + { + await writer.WriteLineAsync(count.ToString(CultureInfo.InvariantCulture)); + } + return 0; } } \ No newline at end of file diff --git a/src/Process.Tests.Dummy/Commands/SleepCommand.cs b/src/Process.Tests.Dummy/Commands/SleepCommand.cs new file mode 100644 index 0000000..b43f554 --- /dev/null +++ b/src/Process.Tests.Dummy/Commands/SleepCommand.cs @@ -0,0 +1,33 @@ +using Spectre.Console.Cli; + +namespace Geekeey.Extensions.Process.Tests.Dummy.Commands; + +internal sealed class SleepCommand : AsyncCommand +{ + public sealed class Settings : CommandSettings + { + [CommandArgument(0, "[duration]")] public TimeSpan Duration { get; init; } = TimeSpan.FromSeconds(1); + } + + public override async Task ExecuteAsync(CommandContext context, Settings settings) + { + using var tty = Terminal.Connect(); + + try + { + await Console.Out.WriteLineAsync($"Sleeping for {settings.Duration}..."); + await Console.Out.FlushAsync(CancellationToken.None); + + await Task.Delay(settings.Duration, tty.CancellationToken); + } + catch (OperationCanceledException) + { + await Console.Out.WriteLineAsync("Canceled."); + await Console.Out.FlushAsync(CancellationToken.None); + } + + await Console.Out.WriteLineAsync("Done."); + await Console.Out.FlushAsync(CancellationToken.None); + return 0; + } +} \ No newline at end of file diff --git a/src/Process.Tests.Dummy/Commands/WorkingDirectoryCommand.cs b/src/Process.Tests.Dummy/Commands/WorkingDirectoryCommand.cs index c34bfbf..9b76a4e 100644 --- a/src/Process.Tests.Dummy/Commands/WorkingDirectoryCommand.cs +++ b/src/Process.Tests.Dummy/Commands/WorkingDirectoryCommand.cs @@ -2,15 +2,21 @@ using Spectre.Console.Cli; namespace Geekeey.Extensions.Process.Tests.Dummy.Commands; -internal sealed class WorkingDirectoryCommand : Command +internal sealed class WorkingDirectoryCommand : AsyncOutputCommand { - public sealed class Settings : CommandSettings + public sealed class Settings : OutputCommandSettings { } - public override int Execute(CommandContext context, Settings settings) + public override async Task ExecuteAsync(CommandContext context, Settings settings) { - Console.Out.WriteLine(Directory.GetCurrentDirectory()); + using var tty = Terminal.Connect(); + + foreach (var writer in tty.GetWriters(settings.Target)) + { + await writer.WriteLineAsync(Directory.GetCurrentDirectory()); + } + return 0; } } \ No newline at end of file diff --git a/src/Process.Tests.Dummy/OutputTarget.cs b/src/Process.Tests.Dummy/OutputTarget.cs index 6f28ac1..00cec7d 100644 --- a/src/Process.Tests.Dummy/OutputTarget.cs +++ b/src/Process.Tests.Dummy/OutputTarget.cs @@ -10,12 +10,12 @@ public enum OutputTarget internal static class OutputTargetExtensions { - public static IEnumerable GetWriters(this OutputTarget target) + public static IEnumerable GetWriters(this Terminal terminal, OutputTarget target) { if (target.HasFlag(OutputTarget.StdOut)) - yield return Console.Out; + yield return terminal.Stdout; if (target.HasFlag(OutputTarget.StdErr)) - yield return Console.Error; + yield return terminal.Stderr; } } \ No newline at end of file diff --git a/src/Process.Tests.Dummy/Program.cs b/src/Process.Tests.Dummy/Program.cs index b7c4e19..6b537d0 100644 --- a/src/Process.Tests.Dummy/Program.cs +++ b/src/Process.Tests.Dummy/Program.cs @@ -33,6 +33,7 @@ public static class Program configuration.AddCommand("cwd"); configuration.AddCommand("exit"); configuration.AddCommand("length"); + configuration.AddCommand("sleep"); configuration.AddBranch("generate", static generate => { generate.AddCommand("blob"); diff --git a/src/Process.Tests.Dummy/Terminal.cs b/src/Process.Tests.Dummy/Terminal.cs new file mode 100644 index 0000000..c857a36 --- /dev/null +++ b/src/Process.Tests.Dummy/Terminal.cs @@ -0,0 +1,41 @@ +namespace Geekeey.Extensions.Process.Tests.Dummy; + +internal sealed class Terminal : IDisposable +{ + private readonly CancellationTokenSource _cts = new(); + + public Terminal() + { + Console.CancelKeyPress += Cancel; + } + + public StreamReader Stdin { get; } = new(Console.OpenStandardInput(), leaveOpen: false); + + public StreamWriter Stdout { get; } = new(Console.OpenStandardOutput(), leaveOpen: false); + + public StreamWriter Stderr { get; } = new(Console.OpenStandardError(), leaveOpen: false); + + public CancellationToken CancellationToken => _cts.Token; + + public static Terminal Connect() + { + return new Terminal(); + } + + private void Cancel(object? sender, ConsoleCancelEventArgs args) + { + args.Cancel = true; + _cts.Cancel(); + } + + public void Dispose() + { + Stdout.BaseStream.Flush(); + Stdout.Dispose(); + Stderr.BaseStream.Flush(); + Stderr.Dispose(); + Stdin.Dispose(); + Console.CancelKeyPress -= Cancel; + _cts.Dispose(); + } +} \ No newline at end of file