Skip to content

Commit

Permalink
Just a few new utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
scottbilas committed Aug 13, 2024
1 parent 2dd28b8 commit 05f1ee6
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 5 deletions.
13 changes: 13 additions & 0 deletions src/Core.Terminal/ControlBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ public static ControlBuilder Print(this ControlBuilder @this, StringBuilder sb)
public static ControlBuilder Print(this ControlBuilder @this, ReadOnlyMemory<char> mem) =>
@this.Print(mem.Span);

public static void PrintLine(this ControlBuilder @this) =>
@this.Print("\r\n");
public static void PrintLine(this ControlBuilder @this, ReadOnlySpan<char> span)
{
@this.Print(span);
@this.PrintLine();
}

public static void ShowCursor(this ControlBuilder @this, bool visible = true) =>
@this.SetCursorVisibility(visible);
public static void HideCursor(this ControlBuilder @this) =>
@this.SetCursorVisibility(false);

// these are affected by current CursorOrigin
public static ControlBuilder MoveCursorHome(this ControlBuilder @this) =>
@this.Print(CSI).Print('H');
Expand Down
15 changes: 12 additions & 3 deletions src/Stale.Cli/LongTasks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ internal set
class LongTasks
{
readonly Dictionary<int, LongTask> _tasks = new();
readonly CancellationToken _stopToken;
readonly CancellationTokenSource _stopTokenSource;

public LongTasks(CancellationToken stopToken)
{
_stopToken = stopToken;
_stopTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken);
}

public IReadOnlyList<LongTask> GetTasksSnapshot()
Expand All @@ -39,11 +39,20 @@ public IReadOnlyList<LongTask> GetTasksSnapshot()
return [.._tasks.Values];
}

public void AbortAll()
{
lock (_tasks)
{
_stopTokenSource.Cancel();
_tasks.Clear();
}
}

// use this for fire-and-forget functions that shouldn't fail, but we definitely want to catch when they do so can
// improve error handling of them. def worse to have them fail quietly in the background.
public LongTask Run(string taskName, Func<CancellationToken, Task> action, CancellationToken? stopToken = null)
{
stopToken ??= _stopToken;
stopToken ??= _stopTokenSource.Token;

// doing this through ctx so that in the future i can do a little nicer handling of a background task
// failure, like adding extra supporting data to a log file or whatever.
Expand Down
2 changes: 2 additions & 0 deletions src/Stale.Cli/Stale.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Core.Terminal\Core.Terminal.csproj" />

<PackageReference Include="Dumpify" />
<PackageReference Include="Spectre.Console" />

Expand Down
60 changes: 58 additions & 2 deletions src/Stale.Cli/TerminalUtils.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,65 @@
using Spectre.Console;
using System.Diagnostics;
using System.Threading.Channels;
using Spectre.Console;
using Spectre.Console.Advanced;
using Spectre.Console.Rendering;
using Vezel.Cathode.Processes;

static class TerminalExtensions
static class TerminalUtils
{
public static string ToAnsi(this IRenderable @this) =>
AnsiConsole.Console.ToAnsi(@this);

public static StaleChildProcess ShellExec(Context ctx, NPath command, IReadOnlyList<string> args)
{
var captures = Channel.CreateUnbounded<LineCapture>(new UnboundedChannelOptions { SingleReader = true });

if (ShellExecUtility.ConfigureProcessExitToAlsoKillChildProcesses() && ctx.IsVerbose)
ctx.VerboseLine("Configuring OS to kill child processes if the current process exits");

// TODO: consider checking if it's a console app
// (see IsWindowsApplication at PowerShell\src\System.Management.Automation\engine\NativeCommandProcessor.cs:1199)

var (commandPath, extraArgs) = ShellExecUtility.ResolveShellCommand(command);
if (extraArgs.Any())
args = [..extraArgs, ..args];

var process = new ChildProcessBuilder()
.WithFileName(commandPath)
.WithArguments(args)
.WithRedirections(false, true, true)
.WithCreateWindow(false)
.WithWindowStyle(ProcessWindowStyle.Hidden)
.WithCancellationToken(ctx.CancelToken)
.WithThrowOnError(false)
.Run();

var open = 0;

async Task CaptureLines(TextReader reader, bool isStdErr, CancellationToken stop)
{
Interlocked.Increment(ref open);

while (!stop.IsCancellationRequested)
{
var line = await reader.ReadLineAsync(stop);
if (line == null)
break;

await captures.Writer.WriteAsync(new LineCapture(isStdErr, DateTime.Now, line), stop);
}

if (Interlocked.Decrement(ref open) == 0)
{
// use the non-throwing TryComplete here to avoid a potential race in this function among the two
// readers and the cancel token causing a double-complete.
captures.Writer.TryComplete();
}
}

ctx.LongTasks.Run($"StandardOut reader for pid {process.Id}", stop => CaptureLines(process.StandardOut.TextReader, false, stop));
ctx.LongTasks.Run($"StandardError reader for pid {process.Id}", stop => CaptureLines(process.StandardError.TextReader, true, stop));

return new(commandPath, args, captures.Reader, process.Id, process.Completion);
}
}

0 comments on commit 05f1ee6

Please sign in to comment.