Skip to content

Instantly share code, notes, and snippets.

@DamianEdwards
Created April 17, 2026 03:18
Show Gist options
  • Select an option

  • Save DamianEdwards/be1ab7a28fc0093e97b6b464a2e27a4e to your computer and use it in GitHub Desktop.

Select an option

Save DamianEdwards/be1ab7a28fc0093e97b6b464a2e27a4e to your computer and use it in GitHub Desktop.
#!/usr/bin/env dotnet
#:property SuppressTrimAnalysisWarnings=false
#:property TrimmerSingleWarn=false
#:package System.CommandLine@2.0.6
#:package Spectre.Console@0.55.0
#:package Hex1b@0.128.0
using System.CommandLine;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO.Pipelines;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Hex1b;
using Hex1b.Automation;
using Hex1b.Diagnostics;
using Hex1b.Input;
using Hex1b.Tokens;
using Hex1b.Widgets;
using Spectre.Console;
const string TargetCommand = "copilot";
const string TargetPrompt = "We're just testing remote sessions here in the context of a proof regarding the spawning and management of background instances of `copilot`. Stay running and await further instructions.";
const string TargetWorkingDirectory = @"D:\src\GitHub\DamianEdwards\kusto-cli";
const string DotnetHost = "dotnet";
const int MaxTrackedInstances = 5;
const string ControllerLogFileName = "hex1b-daemon.log";
const int WorkloadLogTailCharacterLimit = 4000;
TimeSpan JoinSocketReadyTimeout = TimeSpan.FromSeconds(30);
string TargetArguments = $"--yolo --remote -i \"{TargetPrompt}\"";
string[] TargetArgumentList = ["--yolo", "--remote", "-i", TargetPrompt];
TimeSpan SecondaryCtrlCDelay = TimeSpan.FromSeconds(1);
TimeSpan StartupTrackingDelay = TimeSpan.FromSeconds(3);
TimeSpan GracefulShutdownTimeout = TimeSpan.FromSeconds(15);
if (!OperatingSystem.IsWindows())
{
AnsiConsole.MarkupLine("[red]This sample is only supported on Windows.[/]");
return 1;
}
return await BuildRootCommand().Parse(args).InvokeAsync();
RootCommand BuildRootCommand()
{
RootCommand root = new("Daemon-style controller for copilot sessions hosted through Hex1b PTY terminals.");
Option<bool> backgroundOption = new("--background")
{
Description = "Run without keyboard input.",
Hidden = true,
};
Command runCommand = new("run", "Run the controller in the foreground and listen for keyboard input.");
runCommand.Add(backgroundOption);
runCommand.SetAction((parseResult, cancellationToken) => RunAsync(parseResult.GetValue(backgroundOption), cancellationToken));
root.Add(runCommand);
Command startCommand = new("start", "Start a background run instance for the current directory.");
startCommand.SetAction((_, cancellationToken) => StartAsync(cancellationToken));
root.Add(startCommand);
Command stopCommand = new("stop", "Stop the background run instance for the current directory.");
stopCommand.SetAction((_, cancellationToken) => StopAsync(cancellationToken));
root.Add(stopCommand);
Command joinCommand = new("join", "Join the running background instance in the current terminal.");
joinCommand.SetAction((_, cancellationToken) => JoinAsync(cancellationToken));
root.Add(joinCommand);
root.SetAction(_ =>
{
RenderBanner();
AnsiConsole.MarkupLine("[yellow]Choose one of:[/] [aqua]run[/], [aqua]start[/], [aqua]join[/], or [aqua]stop[/].");
return 1;
});
return root;
}
async Task<int> RunAsync(bool backgroundMode, CancellationToken cancellationToken)
{
string currentDirectory = Path.GetFullPath(Environment.CurrentDirectory);
string pidFilePath = GetPidFilePath(currentDirectory);
string controllerLogPath = GetControllerLogPath(currentDirectory);
string stopEventName = GetStopEventName(currentDirectory);
using SingleInstanceLock? runLock = TryAcquireRunLock(currentDirectory);
if (runLock is null)
{
AnsiConsole.MarkupLine($"[red]A run instance is already active for[/] [aqua]{Escape(currentDirectory)}[/].");
AnsiConsole.MarkupLine("[yellow]Use[/] [aqua]dotnet hex1bapp.cs -- stop[/] [yellow]to stop it first.[/]");
return 1;
}
CleanupStalePidFile(currentDirectory, pidFilePath);
using EventWaitHandle stopEvent = new(false, EventResetMode.AutoReset, stopEventName);
RenderBanner();
AnsiConsole.MarkupLine($"[green]Hex1b controller running in[/] [aqua]{Escape(currentDirectory)}[/].");
AnsiConsole.MarkupLine($"[grey]Controller log:[/] [aqua]{Escape(controllerLogPath)}[/]");
AppendControllerLog(controllerLogPath, $"Controller started in {currentDirectory} (backgroundMode={backgroundMode}).");
AppendControllerLog(controllerLogPath, $"Target command: {TargetCommand} {TargetArguments}");
AppendControllerLog(controllerLogPath, $"Target working directory: {TargetWorkingDirectory}");
if (backgroundMode)
{
AnsiConsole.MarkupLine("[grey]Background mode is active. Use[/] [aqua]dotnet hex1bapp.cs -- stop[/] [grey]to stop this controller.[/]");
}
else
{
AnsiConsole.MarkupLine($"[grey]Press[/] [aqua]S[/] [grey]to start[/] [aqua]{Escape(TargetCommand)} {Escape(TargetArguments)}[/] [grey]in[/] [aqua]{Escape(TargetWorkingDirectory)}[/][grey],[/] [aqua]1[/]-[aqua]5[/] [grey]to stop an instance, or[/] [aqua]Ctrl+C[/] [grey]to stop everything and exit.[/]");
}
List<TrackedSession> trackedSessions = [];
int nextSessionToken = 1;
using CancellationTokenSource keyboardCts = new();
using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, keyboardCts.Token);
using CancellationTokenSource stopMonitorCts = new();
int shutdownRequested = 0;
void RequestShutdown(string message)
{
if (Interlocked.Exchange(ref shutdownRequested, 1) != 0)
{
return;
}
AppendControllerLog(controllerLogPath, $"Shutdown requested: {Markup.Remove(message)}");
AnsiConsole.MarkupLine(message);
keyboardCts.Cancel();
}
Task stopSignalTask = Task.Run(() =>
{
int signaledIndex = WaitHandle.WaitAny([stopEvent, stopMonitorCts.Token.WaitHandle]);
if (signaledIndex == 0)
{
RequestShutdown("[yellow]Stop signal received. Stopping tracked instances before exit...[/]");
}
}, CancellationToken.None);
ConsoleCancelEventHandler? cancelHandler = null;
cancelHandler = (_, eventArgs) =>
{
eventArgs.Cancel = true;
RequestShutdown("[yellow]Ctrl+C received. Stopping tracked instances before exit...[/]");
};
Console.CancelKeyPress += cancelHandler;
try
{
if (backgroundMode)
{
try
{
TrackedSession session = await StartTrackedSessionAsync(nextSessionToken++, controllerLogPath, cancellationToken, enableDiagnostics: true);
trackedSessions.Add(session);
AnsiConsole.MarkupLine($"[green]Auto-started instance[/] [aqua]1[/] [green]for background mode.[/]");
AnsiConsole.MarkupLine($"[grey]Workload log:[/] [aqua]{Escape(session.WorkloadLogPath)}[/]");
AppendControllerLog(controllerLogPath, $"Auto-started session token {session.SessionToken}. Workload log: {session.WorkloadLogPath}");
AppendControllerLog(controllerLogPath, $"Attach socket: {GetDiagnosticsSocketPath(Environment.ProcessId)}");
}
catch (Exception ex) when (ex is Win32Exception or InvalidOperationException or DirectoryNotFoundException)
{
AnsiConsole.MarkupLine($"[red]Failed to auto-start {Escape(TargetCommand)}:[/] {Escape(ex.Message)}");
AppendControllerLog(controllerLogPath, $"Failed to auto-start {TargetCommand}: {ex}");
return 1;
}
RenderTrackedSessions(trackedSessions);
try
{
while (!linkedCts.IsCancellationRequested)
{
await ReconcileExitedSessionsAsync(trackedSessions, controllerLogPath, echoToConsole: false);
await Task.Delay(TimeSpan.FromSeconds(1), linkedCts.Token);
}
}
catch (OperationCanceledException) when (keyboardCts.IsCancellationRequested)
{
}
}
else
{
RenderTrackedSessions(trackedSessions);
while (!linkedCts.IsCancellationRequested)
{
await ReconcileExitedSessionsAsync(trackedSessions, controllerLogPath, echoToConsole: true);
ConsoleKeyInfo? keyInfo;
try
{
keyInfo = await AnsiConsole.Console.Input.ReadKeyAsync(true, linkedCts.Token);
}
catch (OperationCanceledException) when (keyboardCts.IsCancellationRequested)
{
break;
}
await ReconcileExitedSessionsAsync(trackedSessions, controllerLogPath, echoToConsole: true);
if (keyInfo is null)
{
continue;
}
ConsoleKeyInfo key = keyInfo.Value;
char keyChar = key.KeyChar;
if (char.ToUpperInvariant(keyChar) == 'S')
{
if (trackedSessions.Count >= MaxTrackedInstances)
{
AnsiConsole.MarkupLine($"[yellow]Already tracking the maximum of {MaxTrackedInstances} instances.[/]");
RenderTrackedSessions(trackedSessions);
continue;
}
try
{
TrackedSession session = await StartTrackedSessionAsync(nextSessionToken++, controllerLogPath, cancellationToken, enableDiagnostics: false);
trackedSessions.Add(session);
AnsiConsole.MarkupLine($"[green]Started instance[/] [aqua]{trackedSessions.Count}[/] [grey](session token {session.SessionToken})[/].");
AnsiConsole.MarkupLine($"[grey]Workload log:[/] [aqua]{Escape(session.WorkloadLogPath)}[/]");
AppendControllerLog(controllerLogPath, $"Started tracked session token {session.SessionToken}. Workload log: {session.WorkloadLogPath}");
}
catch (Exception ex) when (ex is Win32Exception or InvalidOperationException or DirectoryNotFoundException)
{
AnsiConsole.MarkupLine($"[red]Failed to start {Escape(TargetCommand)}:[/] {Escape(ex.Message)}");
AppendControllerLog(controllerLogPath, $"Failed to start {TargetCommand}: {ex}");
}
RenderTrackedSessions(trackedSessions);
continue;
}
if (char.IsDigit(keyChar))
{
int ordinal = keyChar - '0';
if (ordinal < 1 || ordinal > trackedSessions.Count)
{
AnsiConsole.MarkupLine($"[yellow]No tracked instance matches key[/] [aqua]{ordinal}[/].");
RenderTrackedSessions(trackedSessions);
continue;
}
TrackedSession session = trackedSessions[ordinal - 1];
AppendControllerLog(controllerLogPath, $"User requested shutdown of instance {ordinal} (session token {session.SessionToken}).");
StopProcessOutcome outcome = await StopTrackedSessionAsync(session, $"instance {ordinal}", cancellationToken, controllerLogPath);
trackedSessions.RemoveAt(ordinal - 1);
if (outcome == StopProcessOutcome.AlreadyExited)
{
AnsiConsole.MarkupLine($"[grey]Instance {ordinal} was already stopped.[/]");
}
RenderTrackedSessions(trackedSessions);
continue;
}
if (key.Key == ConsoleKey.Escape)
{
AnsiConsole.MarkupLine("[grey]Use Ctrl+C to shut down the controller and all tracked instances.[/]");
continue;
}
AnsiConsole.MarkupLine($"[grey]Ignored key[/] [aqua]{Escape(DescribeKey(key))}[/][grey].[/]");
}
}
}
finally
{
Console.CancelKeyPress -= cancelHandler;
stopMonitorCts.Cancel();
await stopSignalTask;
await ReconcileExitedSessionsAsync(trackedSessions, controllerLogPath, echoToConsole: false);
if (trackedSessions.Count > 0)
{
AnsiConsole.MarkupLine($"[yellow]Stopping {trackedSessions.Count} tracked instance(s)...[/]");
AppendControllerLog(controllerLogPath, $"Stopping {trackedSessions.Count} tracked session(s) during controller shutdown.");
await Task.WhenAll(trackedSessions.Select(session => StopTrackedSessionAsync(session, $"session {session.SessionToken}", CancellationToken.None, controllerLogPath)));
trackedSessions.Clear();
}
CleanupOwnedPidFile(currentDirectory, pidFilePath);
AppendControllerLog(controllerLogPath, "Controller stopped.");
RenderTrackedSessions(trackedSessions);
AnsiConsole.MarkupLine("[green]Controller stopped.[/]");
}
return 0;
}
async Task<int> StartAsync(CancellationToken cancellationToken)
{
string currentDirectory = Path.GetFullPath(Environment.CurrentDirectory);
string pidFilePath = GetPidFilePath(currentDirectory);
if (TryReadDaemonState(pidFilePath, out DaemonState? existingState) &&
TryGetVerifiedProcess(existingState!, out Process? existingProcess))
{
using (existingProcess)
{
AnsiConsole.MarkupLine($"[yellow]A background controller is already running for[/] [aqua]{Escape(currentDirectory)}[/] [grey](PID {existingState!.Pid})[/].");
return 1;
}
}
CleanupStalePidFile(currentDirectory, pidFilePath);
SelfLaunchSpec selfLaunchSpec = GetSelfLaunchSpec();
ProcessStartInfo startInfo = new()
{
FileName = selfLaunchSpec.FileName,
WorkingDirectory = currentDirectory,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
foreach (string argument in selfLaunchSpec.ArgumentPrefix)
{
startInfo.ArgumentList.Add(argument);
}
startInfo.ArgumentList.Add("run");
startInfo.ArgumentList.Add("--background");
using Process process = Process.Start(startInfo)
?? throw new InvalidOperationException("Failed to start the background controller process.");
await Task.Delay(500, cancellationToken);
if (process.HasExited)
{
string output = (await process.StandardOutput.ReadToEndAsync(cancellationToken)).Trim();
string error = (await process.StandardError.ReadToEndAsync(cancellationToken)).Trim();
string details = string.Join(Environment.NewLine, new[] { output, error }.Where(value => !string.IsNullOrWhiteSpace(value)));
if (string.IsNullOrWhiteSpace(details))
{
details = "The background controller exited before it could finish starting.";
}
AnsiConsole.MarkupLine($"[red]{Escape(details)}[/]");
return 1;
}
DaemonState state = new()
{
Pid = process.Id,
StartedAtUtc = process.StartTime.ToUniversalTime(),
};
WriteDaemonState(pidFilePath, state);
AnsiConsole.MarkupLine($"[green]Started background controller[/] [grey](PID {process.Id})[/] [green]for[/] [aqua]{Escape(currentDirectory)}[/].");
AnsiConsole.MarkupLine($"[grey]State file:[/] [aqua]{Escape(pidFilePath)}[/]");
return 0;
}
async Task<int> StopAsync(CancellationToken cancellationToken)
{
string currentDirectory = Path.GetFullPath(Environment.CurrentDirectory);
string pidFilePath = GetPidFilePath(currentDirectory);
if (!TryReadDaemonState(pidFilePath, out DaemonState? state))
{
AnsiConsole.MarkupLine($"[yellow]No pid.json was found in[/] [aqua]{Escape(currentDirectory)}[/].");
return 1;
}
if (!TryGetVerifiedProcess(state!, out Process? process))
{
CleanupStalePidFile(currentDirectory, pidFilePath);
AnsiConsole.MarkupLine("[yellow]The recorded background controller is no longer running. Removed stale pid.json.[/]");
return 1;
}
using (process)
{
AnsiConsole.MarkupLine($"[yellow]Sending stop signal to background controller[/] [grey](PID {state!.Pid})[/][yellow]...[/]");
AppendControllerLog(GetControllerLogPath(currentDirectory), $"Stop command requested shutdown of background controller PID {state.Pid}.");
bool signalSent = SignalStopEvent(currentDirectory);
if (!signalSent)
{
AnsiConsole.MarkupLine("[yellow]The controller stop signal was unavailable. Waiting for exit before forcing a kill.[/]");
}
bool exitedGracefully = await WaitForExitAsync(process, GracefulShutdownTimeout, cancellationToken);
if (!exitedGracefully)
{
AnsiConsole.MarkupLine("[red]Background controller did not exit within 15 seconds; killing it.[/]");
process.Kill(entireProcessTree: true);
await process.WaitForExitAsync(cancellationToken);
}
}
DeletePidFileIfPresent(pidFilePath);
AnsiConsole.MarkupLine("[green]Background controller stopped.[/]");
return 0;
}
async Task<int> JoinAsync(CancellationToken cancellationToken)
{
string currentDirectory = Path.GetFullPath(Environment.CurrentDirectory);
string pidFilePath = GetPidFilePath(currentDirectory);
if (!TryReadDaemonState(pidFilePath, out DaemonState? state))
{
AnsiConsole.MarkupLine($"[yellow]No pid.json was found in[/] [aqua]{Escape(currentDirectory)}[/].");
return 1;
}
if (!TryGetVerifiedProcess(state!, out Process? process))
{
CleanupStalePidFile(currentDirectory, pidFilePath);
AnsiConsole.MarkupLine("[yellow]The recorded background controller is no longer running. Removed stale pid.json.[/]");
return 1;
}
using (process)
{
string socketPath = GetDiagnosticsSocketPath(state!.Pid);
bool socketReady = await WaitForDiagnosticsSocketAsync(socketPath, JoinSocketReadyTimeout, cancellationToken);
if (!socketReady)
{
AnsiConsole.MarkupLine($"[red]No attachable background Hex1b session became available for PID {state.Pid}.[/]");
AnsiConsole.MarkupLine($"[grey]Expected diagnostics socket:[/] [aqua]{Escape(socketPath)}[/]");
return 1;
}
AnsiConsole.MarkupLine("[grey]Attached. Use[/] [aqua]Ctrl+][/][grey] then[/] [aqua]D[/] [grey]to detach,[/] [aqua]L[/] [grey]to take resize leadership, or[/] [aqua]Q[/] [grey]to request shutdown of the remote session.[/]");
await using SocketAttachTransport transport = new(socketPath);
await using JoinSessionClient joinSession = new(transport);
return await joinSession.RunAsync(cancellationToken);
}
}
async Task<TrackedSession> StartTrackedSessionAsync(int sessionToken, string controllerLogPath, CancellationToken cancellationToken, bool enableDiagnostics)
{
if (!Directory.Exists(TargetWorkingDirectory))
{
throw new DirectoryNotFoundException($"The configured repo path does not exist: {TargetWorkingDirectory}");
}
string workloadLogPath = CreateWorkloadLogPath(sessionToken);
string sessionDiagnosticLogPath = CreateSessionDiagnosticLogPath(sessionToken);
AppendSessionDiagnostic(sessionDiagnosticLogPath, $"Preparing Hex1b PTY host for {TargetCommand} at {TargetWorkingDirectory}.");
Hex1bTerminal terminal = CreateTargetTerminal(workloadLogPath, enableDiagnostics);
CancellationTokenSource runCts = new();
Task<TrackedSessionOutcome> completion = RunTrackedSessionAsync(sessionToken, terminal, runCts.Token, sessionDiagnosticLogPath, controllerLogPath);
TrackedSession session = new(sessionToken, terminal, runCts, completion, workloadLogPath, sessionDiagnosticLogPath, DateTimeOffset.UtcNow);
AppendControllerLog(controllerLogPath, $"Created Hex1b terminal for session token {sessionToken}. Workload log: {workloadLogPath}");
await Task.Delay(StartupTrackingDelay, cancellationToken);
if (completion.IsCompleted)
{
TrackedSessionOutcome outcome = await completion;
throw new InvalidOperationException(BuildStartupFailureMessage($"{TargetCommand} exited during startup.", session, outcome));
}
return session;
}
async Task<TrackedSessionOutcome> RunTrackedSessionAsync(int sessionToken, Hex1bTerminal terminal, CancellationToken cancellationToken, string sessionDiagnosticLogPath, string controllerLogPath)
{
try
{
int exitCode = await terminal.RunAsync(cancellationToken);
AppendControllerLog(controllerLogPath, $"Session token {sessionToken} exited with code {exitCode}.");
AppendSessionDiagnostic(sessionDiagnosticLogPath, $"Hex1b terminal completed with exit code {exitCode}.");
return new TrackedSessionOutcome(exitCode, false, null, DateTimeOffset.UtcNow);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
AppendControllerLog(controllerLogPath, $"Session token {sessionToken} was cancelled.");
AppendSessionDiagnostic(sessionDiagnosticLogPath, "Hex1b terminal cancellation requested.");
return new TrackedSessionOutcome(null, true, null, DateTimeOffset.UtcNow);
}
catch (Exception ex)
{
AppendControllerLog(controllerLogPath, $"Session token {sessionToken} faulted: {ex}");
AppendSessionDiagnostic(sessionDiagnosticLogPath, $"Hex1b terminal faulted: {ex}");
return new TrackedSessionOutcome(null, false, ex, DateTimeOffset.UtcNow);
}
finally
{
await terminal.DisposeAsync();
}
}
async Task<StopProcessOutcome> StopTrackedSessionAsync(TrackedSession session, string label, CancellationToken cancellationToken, string controllerLogPath)
{
if (session.Completion.IsCompleted)
{
AppendControllerLog(controllerLogPath, $"Requested stop for {label} (session token {session.SessionToken}), but it had already exited.");
AppendSessionDiagnostic(session.SessionDiagnosticLogPath, $"Controller requested stop for {label}, but the session had already exited.");
return StopProcessOutcome.AlreadyExited;
}
int signalAttempt = 0;
foreach (TimeSpan waitWindow in new[] { SecondaryCtrlCDelay, GracefulShutdownTimeout - SecondaryCtrlCDelay })
{
if (waitWindow <= TimeSpan.Zero)
{
continue;
}
signalAttempt++;
AppendControllerLog(controllerLogPath, $"Sending Hex1b Ctrl+C attempt {signalAttempt} to {label} (session token {session.SessionToken}).");
AppendSessionDiagnostic(session.SessionDiagnosticLogPath, $"Controller sending Hex1b Ctrl+C attempt {signalAttempt} to {label}.");
AnsiConsole.MarkupLine($"[yellow]Sending Ctrl+C to[/] [aqua]{Escape(label)}[/] [grey](session token {session.SessionToken})[/][yellow]...[/]");
await SendCtrlCThroughHex1bAsync(session.Terminal, cancellationToken);
bool exitedGracefully = await WaitForSessionExitAsync(session, waitWindow, cancellationToken);
if (exitedGracefully)
{
AppendControllerLog(controllerLogPath, $"Stopped {label} (session token {session.SessionToken}) gracefully.");
AppendSessionDiagnostic(session.SessionDiagnosticLogPath, $"Controller observed graceful shutdown after Ctrl+C.");
AnsiConsole.MarkupLine($"[green]Stopped[/] [aqua]{Escape(label)}[/] [grey](session token {session.SessionToken})[/] [green]gracefully.[/]");
return StopProcessOutcome.Graceful;
}
}
AppendControllerLog(controllerLogPath, $"{label} (session token {session.SessionToken}) did not exit after two Ctrl+C attempts; disposing terminal.");
AppendSessionDiagnostic(session.SessionDiagnosticLogPath, "Session did not exit after two Ctrl+C attempts; disposing terminal.");
AnsiConsole.MarkupLine($"[red]{Escape(label)} did not exit within 15 seconds after two Ctrl+C attempts; disposing terminal.[/]");
session.RunCts.Cancel();
await session.Terminal.DisposeAsync();
await WaitForSessionExitAsync(session, TimeSpan.FromSeconds(5), CancellationToken.None);
AnsiConsole.MarkupLine($"[green]Disposed[/] [aqua]{Escape(label)}[/] [grey](session token {session.SessionToken})[/].");
AppendControllerLog(controllerLogPath, $"Disposed {label} (session token {session.SessionToken}) after timeout.");
return StopProcessOutcome.Killed;
}
async Task SendCtrlCThroughHex1bAsync(Hex1bTerminal terminal, CancellationToken cancellationToken)
{
Hex1bTerminalInputSequence sequence = new Hex1bTerminalInputSequenceBuilder()
.Key(Hex1bKey.C, Hex1bModifiers.Control)
.Build();
await sequence.ApplyAsync(terminal, cancellationToken);
}
static async Task<bool> WaitForSessionExitAsync(TrackedSession session, TimeSpan timeout, CancellationToken cancellationToken)
{
if (session.Completion.IsCompleted)
{
return true;
}
Task completedTask = await Task.WhenAny(session.Completion, Task.Delay(timeout, cancellationToken));
return completedTask == session.Completion;
}
static async Task<bool> WaitForExitAsync(Process process, TimeSpan timeout, CancellationToken cancellationToken)
{
if (process.HasExited)
{
return true;
}
using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(timeout);
try
{
await process.WaitForExitAsync(timeoutCts.Token);
return true;
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
return process.HasExited;
}
}
async Task ReconcileExitedSessionsAsync(List<TrackedSession> trackedSessions, string controllerLogPath, bool echoToConsole)
{
List<TrackedSession> removedSessions = [];
for (int index = trackedSessions.Count - 1; index >= 0; index--)
{
TrackedSession session = trackedSessions[index];
if (!session.Completion.IsCompleted)
{
continue;
}
trackedSessions.RemoveAt(index);
removedSessions.Add(session);
}
removedSessions.Reverse();
if (removedSessions.Count == 0)
{
return;
}
foreach (TrackedSession removedSession in removedSessions)
{
TrackedSessionOutcome outcome = await removedSession.Completion;
string statusText = DescribeOutcome(outcome);
AppendControllerLog(controllerLogPath, $"Tracked session token {removedSession.SessionToken} exited on its own. Outcome: {statusText}. Workload log: {removedSession.WorkloadLogPath}");
if (echoToConsole)
{
AnsiConsole.MarkupLine($"[yellow]Tracked instance exited on its own[/] [grey](session token {removedSession.SessionToken})[/][yellow]. Outcome:[/] {Escape(statusText)}");
AnsiConsole.MarkupLine($"[grey]Workload log:[/] [aqua]{Escape(removedSession.WorkloadLogPath)}[/]");
}
string workloadLogTail = ReadWorkloadLogTail(removedSession.WorkloadLogPath);
if (!string.IsNullOrWhiteSpace(workloadLogTail))
{
AppendControllerLog(controllerLogPath, $"Last workload output for session token {removedSession.SessionToken}:{Environment.NewLine}{workloadLogTail}");
if (echoToConsole)
{
AnsiConsole.MarkupLine($"[grey]Last workload output:[/]{Environment.NewLine}{Escape(workloadLogTail)}");
}
}
}
AppendControllerLog(controllerLogPath, $"Tracked instance count is now {trackedSessions.Count}.");
if (echoToConsole)
{
RenderTrackedSessions(trackedSessions);
}
}
void RenderBanner()
{
Panel panel = new(new Markup("[bold aqua]Hex1b Graceful Process Controller[/]"))
{
Border = BoxBorder.Rounded,
Header = new PanelHeader("daemon-style CLI"),
Expand = false,
};
AnsiConsole.Write(panel);
}
void RenderTrackedSessions(IReadOnlyList<TrackedSession> trackedSessions)
{
if (trackedSessions.Count == 0)
{
AnsiConsole.Write(new Panel(new Markup("[grey]No tracked instances are running.[/]"))
{
Header = new PanelHeader("tracked instances"),
Border = BoxBorder.Rounded,
});
return;
}
Table table = new();
table.Border = TableBorder.Rounded;
table.Title = new TableTitle("Tracked running instances");
table.AddColumn("#");
table.AddColumn("Session token");
table.AddColumn("Started (local)");
for (int index = 0; index < trackedSessions.Count; index++)
{
TrackedSession session = trackedSessions[index];
table.AddRow(
(index + 1).ToString(CultureInfo.InvariantCulture),
session.SessionToken.ToString(CultureInfo.InvariantCulture),
session.StartedAtUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture));
}
AnsiConsole.Write(table);
}
static bool TryReadDaemonState(string pidFilePath, out DaemonState? state)
{
state = null;
if (!File.Exists(pidFilePath))
{
return false;
}
try
{
string json = File.ReadAllText(pidFilePath).Trim();
state = JsonSerializer.Deserialize(json, Hex1bAppJsonSerializerContext.Default.DaemonState);
if (state is not null)
{
return true;
}
if (int.TryParse(json, NumberStyles.Integer, CultureInfo.InvariantCulture, out int legacyPid))
{
state = new DaemonState
{
Pid = legacyPid,
StartedAtUtc = default,
};
return true;
}
return false;
}
catch (JsonException)
{
string text = File.ReadAllText(pidFilePath).Trim();
if (int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int legacyPid))
{
state = new DaemonState
{
Pid = legacyPid,
StartedAtUtc = default,
};
return true;
}
return false;
}
}
static void WriteDaemonState(string pidFilePath, DaemonState state)
{
string json = JsonSerializer.Serialize(state, Hex1bAppJsonSerializerContext.Default.DaemonState);
File.WriteAllText(pidFilePath, json);
}
void CleanupStalePidFile(string currentDirectory, string pidFilePath)
{
if (!TryReadDaemonState(pidFilePath, out DaemonState? state))
{
return;
}
if (TryGetVerifiedProcess(state!, out Process? process))
{
process.Dispose();
return;
}
DeletePidFileIfPresent(pidFilePath);
AnsiConsole.MarkupLine($"[grey]Removed stale pid.json from[/] [aqua]{Escape(currentDirectory)}[/][grey].[/]");
}
void CleanupOwnedPidFile(string currentDirectory, string pidFilePath)
{
if (!TryReadDaemonState(pidFilePath, out DaemonState? state))
{
return;
}
if (state!.Pid != Environment.ProcessId)
{
return;
}
try
{
using Process current = Process.GetCurrentProcess();
DateTimeOffset actualStartedAtUtc = current.StartTime.ToUniversalTime();
if (state.StartedAtUtc == default || state.StartedAtUtc == actualStartedAtUtc)
{
DeletePidFileIfPresent(pidFilePath);
AnsiConsole.MarkupLine($"[grey]Removed pid.json for[/] [aqua]{Escape(currentDirectory)}[/][grey].[/]");
}
}
catch (Win32Exception)
{
}
}
static void DeletePidFileIfPresent(string pidFilePath)
{
if (File.Exists(pidFilePath))
{
File.Delete(pidFilePath);
}
}
static bool SignalStopEvent(string currentDirectory)
{
if (!OperatingSystem.IsWindows())
{
return false;
}
try
{
using EventWaitHandle stopEvent = EventWaitHandle.OpenExisting(GetStopEventName(currentDirectory));
return stopEvent.Set();
}
catch (WaitHandleCannotBeOpenedException)
{
return false;
}
}
static async Task<bool> WaitForDiagnosticsSocketAsync(string socketPath, TimeSpan timeout, CancellationToken cancellationToken)
{
DateTimeOffset deadline = DateTimeOffset.UtcNow + timeout;
while (DateTimeOffset.UtcNow < deadline)
{
cancellationToken.ThrowIfCancellationRequested();
if (await TryProbeDiagnosticsSocketAsync(socketPath, cancellationToken))
{
return true;
}
await Task.Delay(250, cancellationToken);
}
return false;
}
static async Task<bool> TryProbeDiagnosticsSocketAsync(string socketPath, CancellationToken cancellationToken)
{
if (!File.Exists(socketPath))
{
return false;
}
try
{
using Socket socket = new(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromSeconds(2));
await socket.ConnectAsync(new UnixDomainSocketEndPoint(socketPath), timeoutCts.Token);
await using NetworkStream stream = new(socket, ownsSocket: false);
using StreamReader reader = new(stream, Encoding.UTF8, leaveOpen: true);
await using StreamWriter writer = new(stream, Encoding.UTF8, leaveOpen: true) { AutoFlush = true };
AttachSocketRequest request = new() { Method = "info" };
string requestJson = JsonSerializer.Serialize(request, Hex1bAppJsonSerializerContext.Default.AttachSocketRequest);
await writer.WriteLineAsync(requestJson.AsMemory(), timeoutCts.Token);
string? responseLine = await reader.ReadLineAsync(timeoutCts.Token);
if (string.IsNullOrWhiteSpace(responseLine))
{
return false;
}
AttachSocketResponse? response = JsonSerializer.Deserialize(responseLine, Hex1bAppJsonSerializerContext.Default.AttachSocketResponse);
return response?.Success == true;
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
return false;
}
catch (IOException)
{
return false;
}
catch (SocketException)
{
return false;
}
catch (JsonException)
{
return false;
}
}
static bool TryGetVerifiedProcess(DaemonState state, [NotNullWhen(true)] out Process? process) =>
TryGetVerifiedProcessById(state.Pid, state.StartedAtUtc, out process);
static bool TryGetVerifiedProcessById(int pid, DateTimeOffset startedAtUtc, [NotNullWhen(true)] out Process? process)
{
process = null;
try
{
process = Process.GetProcessById(pid);
if (process.HasExited)
{
process.Dispose();
process = null;
return false;
}
if (startedAtUtc != default && process.StartTime.ToUniversalTime() != startedAtUtc.UtcDateTime)
{
process.Dispose();
process = null;
return false;
}
return true;
}
catch (ArgumentException)
{
process?.Dispose();
process = null;
return false;
}
catch (InvalidOperationException)
{
process?.Dispose();
process = null;
return false;
}
catch (Win32Exception)
{
process?.Dispose();
process = null;
return false;
}
}
static string GetPidFilePath(string currentDirectory) => Path.Combine(currentDirectory, "pid.json");
static string GetControllerLogPath(string currentDirectory) => Path.Combine(currentDirectory, ControllerLogFileName);
static string GetStopEventName(string currentDirectory) => $@"Local\Hex1bGracefulProcessShutdown-Stop-{ComputeStableHash(currentDirectory)}";
static string GetDiagnosticsSocketPath(int pid)
{
string userProfilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(userProfilePath, ".hex1b", "sockets", $"{pid}.diagnostics.socket");
}
static SelfLaunchSpec GetSelfLaunchSpec()
{
string? processPath = Environment.ProcessPath;
if (!string.IsNullOrWhiteSpace(processPath) &&
File.Exists(processPath) &&
!IsDotnetHostPath(processPath))
{
return new SelfLaunchSpec(processPath, []);
}
return new SelfLaunchSpec(DotnetHost, [GetScriptPath(), "--"]);
}
static bool IsDotnetHostPath(string path)
{
string fileName = Path.GetFileName(path);
return string.Equals(fileName, "dotnet", StringComparison.OrdinalIgnoreCase) ||
string.Equals(fileName, "dotnet.exe", StringComparison.OrdinalIgnoreCase);
}
static SingleInstanceLock? TryAcquireRunLock(string currentDirectory)
{
try
{
string lockDirectoryPath = Path.Combine(Path.GetTempPath(), "Hex1bGracefulProcessShutdown");
Directory.CreateDirectory(lockDirectoryPath);
string lockPath = Path.Combine(lockDirectoryPath, $"{ComputeStableHash(currentDirectory)}.lock");
FileStream lockStream = new(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
return new SingleInstanceLock(lockStream);
}
catch (IOException)
{
return null;
}
}
static string ComputeStableHash(string value)
{
byte[] bytes = SHA256.HashData(Encoding.UTF8.GetBytes(value.ToUpperInvariant()));
return Convert.ToHexString(bytes);
}
Hex1bTerminal CreateTargetTerminal(string workloadLogPath, bool enableDiagnostics)
{
Hex1bTerminalBuilder builder = Hex1bTerminal.CreateBuilder()
.WithDimensions(120, 40)
.WithHeadless()
.WithWorkloadLogging(workloadLogPath);
if (enableDiagnostics)
{
builder = builder.WithDiagnostics($"hex1bapp-background-{Environment.ProcessId}", forceEnable: true);
}
return builder
.WithPtyProcess(options =>
{
options.FileName = TargetCommand;
options.Arguments = new List<string>(TargetArgumentList);
options.WorkingDirectory = TargetWorkingDirectory;
options.WindowsPtyMode = WindowsPtyMode.RequireProxy;
options.InheritEnvironment = true;
})
.Build();
}
static string CreateWorkloadLogPath(int sessionToken)
{
string logDirectoryPath = Path.Combine(Path.GetTempPath(), "Hex1bGracefulProcessShutdown", "logs");
Directory.CreateDirectory(logDirectoryPath);
return Path.Combine(logDirectoryPath, $"copilot-session-{sessionToken}-{DateTimeOffset.UtcNow:yyyyMMddHHmmssfff}.log");
}
static string CreateSessionDiagnosticLogPath(int sessionToken)
{
string logDirectoryPath = Path.Combine(Path.GetTempPath(), "Hex1bGracefulProcessShutdown", "logs");
Directory.CreateDirectory(logDirectoryPath);
return Path.Combine(logDirectoryPath, $"copilot-session-{sessionToken}-{DateTimeOffset.UtcNow:yyyyMMddHHmmssfff}.controller.log");
}
static string DescribeOutcome(TrackedSessionOutcome outcome)
{
if (outcome.Failure is not null)
{
return $"faulted: {outcome.Failure.Message}";
}
if (outcome.Cancelled)
{
return "cancelled";
}
return $"exit code {outcome.ExitCode ?? 0}";
}
static string BuildStartupFailureMessage(string prefix, TrackedSession session, TrackedSessionOutcome outcome)
{
string details = DescribeOutcome(outcome);
string workloadLogTail = ReadWorkloadLogTail(session.WorkloadLogPath);
string sessionDiagnosticLogTail = ReadWorkloadLogTail(session.SessionDiagnosticLogPath);
if (!string.IsNullOrWhiteSpace(sessionDiagnosticLogTail))
{
details = $"{details}{Environment.NewLine}Controller session log:{Environment.NewLine}{sessionDiagnosticLogTail}";
}
if (string.IsNullOrWhiteSpace(workloadLogTail))
{
return $"{prefix} Outcome: {details}";
}
return $"{prefix} Outcome: {details}{Environment.NewLine}Workload log:{Environment.NewLine}{workloadLogTail}";
}
static void AppendControllerLog(string controllerLogPath, string message)
{
string line = $"[{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}{Environment.NewLine}";
TryWriteLogFile(controllerLogPath, line, append: true);
}
static void AppendSessionDiagnostic(string sessionDiagnosticLogPath, string message)
{
string line = $"[{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}{Environment.NewLine}";
TryWriteLogFile(sessionDiagnosticLogPath, line, append: true);
}
static bool TryWriteLogFile(string path, string content, bool append)
{
string? directoryPath = Path.GetDirectoryName(path);
if (!string.IsNullOrWhiteSpace(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
for (int attempt = 0; attempt < 5; attempt++)
{
try
{
FileMode fileMode = append ? FileMode.Append : FileMode.Create;
using FileStream stream = new(path, fileMode, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete);
using StreamWriter writer = new(stream, Encoding.UTF8);
writer.Write(content);
writer.Flush();
return true;
}
catch (IOException)
{
if (attempt == 4)
{
return false;
}
Thread.Sleep(25);
}
catch (UnauthorizedAccessException)
{
if (attempt == 4)
{
return false;
}
Thread.Sleep(25);
}
}
return false;
}
static string ReadWorkloadLogTail(string workloadLogPath)
{
try
{
if (!File.Exists(workloadLogPath))
{
return string.Empty;
}
string logContent = File.ReadAllText(workloadLogPath).Trim();
if (string.IsNullOrWhiteSpace(logContent))
{
return string.Empty;
}
if (logContent.Length <= WorkloadLogTailCharacterLimit)
{
return logContent;
}
return $"...{logContent[^WorkloadLogTailCharacterLimit..]}";
}
catch (IOException)
{
return string.Empty;
}
catch (UnauthorizedAccessException)
{
return string.Empty;
}
}
static string Escape(string value) => Markup.Escape(value);
static string DescribeKey(ConsoleKeyInfo key) =>
key.KeyChar == '\0' ? key.Key.ToString() : key.KeyChar.ToString();
static string GetScriptPath([CallerFilePath] string path = "") => path;
sealed record DaemonState
{
public required int Pid { get; init; }
public required DateTimeOffset StartedAtUtc { get; init; }
}
sealed record AttachSocketRequest
{
[JsonPropertyName("method")]
public required string Method { get; init; }
}
sealed record AttachSocketResponse
{
[JsonPropertyName("success")]
public bool Success { get; init; }
[JsonPropertyName("width")]
public int? Width { get; init; }
[JsonPropertyName("height")]
public int? Height { get; init; }
[JsonPropertyName("leader")]
public bool? Leader { get; init; }
[JsonPropertyName("data")]
public string? Data { get; init; }
[JsonPropertyName("error")]
public string? Error { get; init; }
}
sealed class TrackedSession(
int sessionToken,
Hex1bTerminal terminal,
CancellationTokenSource runCts,
Task<TrackedSessionOutcome> completion,
string workloadLogPath,
string sessionDiagnosticLogPath,
DateTimeOffset startedAtUtc)
{
public int SessionToken { get; } = sessionToken;
public Hex1bTerminal Terminal { get; } = terminal;
public CancellationTokenSource RunCts { get; } = runCts;
public Task<TrackedSessionOutcome> Completion { get; } = completion;
public string WorkloadLogPath { get; } = workloadLogPath;
public string SessionDiagnosticLogPath { get; } = sessionDiagnosticLogPath;
public DateTimeOffset StartedAtUtc { get; } = startedAtUtc;
}
sealed record TrackedSessionOutcome(int? ExitCode, bool Cancelled, Exception? Failure, DateTimeOffset EndedAtUtc);
sealed record SelfLaunchSpec(string FileName, IReadOnlyList<string> ArgumentPrefix);
[JsonSerializable(typeof(DaemonState))]
[JsonSerializable(typeof(AttachSocketRequest))]
[JsonSerializable(typeof(AttachSocketResponse))]
sealed partial class Hex1bAppJsonSerializerContext : JsonSerializerContext;
enum StopProcessOutcome
{
Graceful,
Killed,
AlreadyExited,
}
sealed class SingleInstanceLock(FileStream lockStream) : IDisposable
{
private readonly FileStream _lockStream = lockStream;
private bool _disposed;
public void Dispose()
{
if (_disposed)
{
return;
}
_lockStream.Dispose();
_disposed = true;
}
}
enum AttachTransportFrameKind
{
Output,
Resize,
LeaderChanged,
Exit,
}
readonly record struct AttachTransportFrame(AttachTransportFrameKind Kind, ReadOnlyMemory<byte> Data)
{
public static AttachTransportFrame Output(ReadOnlyMemory<byte> data) => new(AttachTransportFrameKind.Output, data);
public static AttachTransportFrame Resize(int width, int height)
{
byte[] bytes = new byte[8];
BitConverter.TryWriteBytes(bytes.AsSpan(0, 4), width);
BitConverter.TryWriteBytes(bytes.AsSpan(4, 4), height);
return new AttachTransportFrame(AttachTransportFrameKind.Resize, bytes);
}
public static AttachTransportFrame LeaderChanged(bool isLeader) =>
new(AttachTransportFrameKind.LeaderChanged, new byte[] { isLeader ? (byte)1 : (byte)0 });
public static AttachTransportFrame Exit() => new(AttachTransportFrameKind.Exit, default);
public (int Width, int Height) GetResize()
{
ReadOnlySpan<byte> span = Data.Span;
return (BitConverter.ToInt32(span[..4]), BitConverter.ToInt32(span[4..8]));
}
public bool GetIsLeader() => !Data.IsEmpty && Data.Span[0] != 0;
}
sealed record AttachTransportResult(
bool Success,
int Width,
int Height,
bool IsLeader,
string? InitialScreen,
string? Error);
sealed class SocketAttachTransport(string socketPath) : IAsyncDisposable
{
private readonly string _socketPath = socketPath;
private Socket? _socket;
private NetworkStream? _stream;
private StreamReader? _reader;
private StreamWriter? _writer;
public async Task<AttachTransportResult> ConnectAsync(CancellationToken cancellationToken)
{
try
{
_socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
await _socket.ConnectAsync(new UnixDomainSocketEndPoint(_socketPath), cancellationToken);
_stream = new NetworkStream(_socket, ownsSocket: false);
_reader = new StreamReader(_stream, Encoding.UTF8, leaveOpen: true);
_writer = new StreamWriter(_stream, Encoding.UTF8, bufferSize: 1024, leaveOpen: true) { AutoFlush = true };
AttachSocketRequest request = new() { Method = "attach" };
string requestJson = JsonSerializer.Serialize(request, Hex1bAppJsonSerializerContext.Default.AttachSocketRequest);
await _writer.WriteLineAsync(requestJson.AsMemory(), cancellationToken);
string? responseLine = await _reader.ReadLineAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(responseLine))
{
return new AttachTransportResult(false, 0, 0, false, null, "Empty response from terminal host.");
}
AttachSocketResponse? response = JsonSerializer.Deserialize(responseLine, Hex1bAppJsonSerializerContext.Default.AttachSocketResponse);
if (response is null)
{
return new AttachTransportResult(false, 0, 0, false, null, "Failed to deserialize attach response.");
}
if (!response.Success)
{
return new AttachTransportResult(false, 0, 0, false, null, response.Error ?? "Attach failed.");
}
return new AttachTransportResult(true, response.Width ?? 0, response.Height ?? 0, response.Leader ?? false, response.Data, null);
}
catch (Exception ex) when (ex is IOException or SocketException or JsonException)
{
return new AttachTransportResult(false, 0, 0, false, null, ex.Message);
}
}
public Task SendInputAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken) =>
WriteLineAsync($"i:{Convert.ToBase64String(data.Span)}", cancellationToken);
public Task SendResizeAsync(int width, int height, CancellationToken cancellationToken) =>
WriteLineAsync($"r:{width},{height}", cancellationToken);
public Task ClaimLeadAsync(CancellationToken cancellationToken) =>
WriteLineAsync("lead", cancellationToken);
public Task DetachAsync(CancellationToken cancellationToken) =>
WriteLineAsync("detach", cancellationToken);
public Task ShutdownAsync(CancellationToken cancellationToken) =>
WriteLineAsync("shutdown", cancellationToken);
public async IAsyncEnumerable<AttachTransportFrame> ReadFramesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
if (_reader is null)
{
yield break;
}
while (!cancellationToken.IsCancellationRequested)
{
string? line;
try
{
line = await _reader.ReadLineAsync(cancellationToken);
}
catch (OperationCanceledException)
{
yield break;
}
catch (IOException)
{
yield break;
}
if (line is null)
{
yield break;
}
if (line.StartsWith("o:", StringComparison.Ordinal))
{
byte[] outputBytes = Convert.FromBase64String(line[2..]);
yield return AttachTransportFrame.Output(outputBytes);
continue;
}
if (line.StartsWith("r:", StringComparison.Ordinal))
{
string[] parts = line[2..].Split(',');
if (parts.Length == 2 &&
int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int width) &&
int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int height))
{
yield return AttachTransportFrame.Resize(width, height);
}
continue;
}
if (line.StartsWith("leader:", StringComparison.Ordinal))
{
yield return AttachTransportFrame.LeaderChanged(string.Equals(line[7..], "true", StringComparison.OrdinalIgnoreCase));
continue;
}
if (string.Equals(line, "exit", StringComparison.OrdinalIgnoreCase))
{
yield return AttachTransportFrame.Exit();
yield break;
}
}
}
public async ValueTask DisposeAsync()
{
if (_writer is not null)
{
await _writer.DisposeAsync();
_writer = null;
}
_reader?.Dispose();
_reader = null;
if (_stream is not null)
{
await _stream.DisposeAsync();
_stream = null;
}
_socket?.Dispose();
_socket = null;
}
private Task WriteLineAsync(string line, CancellationToken cancellationToken)
{
if (_writer is null)
{
return Task.CompletedTask;
}
return _writer.WriteLineAsync(line.AsMemory(), cancellationToken);
}
}
sealed class JoinSessionClient(SocketAttachTransport transport) : IAsyncDisposable
{
private readonly SocketAttachTransport _transport = transport;
private readonly Pipe _outputPipe = new();
private bool _isLeader;
private bool _shutdownRequested;
private int _remoteWidth;
private int _remoteHeight;
private int _displayWidth;
private int _displayHeight;
private Hex1bApp? _app;
private Hex1bTerminal? _embeddedTerminal;
private TerminalWidgetHandle? _handle;
private CancellationTokenSource? _appCts;
public async Task<int> RunAsync(CancellationToken cancellationToken)
{
AttachTransportResult connectResult = await _transport.ConnectAsync(cancellationToken);
if (!connectResult.Success)
{
AnsiConsole.MarkupLine($"[red]Failed to join background instance:[/] {Markup.Escape(connectResult.Error ?? "Unknown error.")}");
return 1;
}
_isLeader = connectResult.IsLeader;
_remoteWidth = connectResult.Width;
_remoteHeight = connectResult.Height;
bool preSizedToDisplay = await TryApplyInitialResizeAsync(cancellationToken);
InputInterceptStream inputIntercept = new(this);
StreamWorkloadAdapter workload = new(_outputPipe.Reader.AsStream(), inputIntercept)
{
Width = _remoteWidth,
Height = _remoteHeight,
};
_embeddedTerminal = Hex1bTerminal.CreateBuilder()
.WithDimensions(_remoteWidth, _remoteHeight)
.WithWorkload(workload)
.WithTerminalWidget(out TerminalWidgetHandle handle)
.Build();
_handle = handle;
_appCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
Task<int> embeddedRunTask = _embeddedTerminal.RunAsync(_appCts.Token);
Task networkOutputTask = PumpNetworkOutputAsync(_appCts.Token);
if (!preSizedToDisplay && !string.IsNullOrEmpty(connectResult.InitialScreen))
{
byte[] initialBytes = Encoding.UTF8.GetBytes(connectResult.InitialScreen);
await _outputPipe.Writer.WriteAsync(initialBytes, cancellationToken);
await _outputPipe.Writer.FlushAsync(cancellationToken);
}
await using Hex1bTerminal displayTerminal = Hex1bTerminal.CreateBuilder()
.AddPresentationFilter(new ResizeFilter(this))
.WithHex1bApp((app, _) =>
{
_app = app;
return ctx => ctx.Terminal(handle).Fill();
})
.Build();
try
{
await displayTerminal.RunAsync(_appCts.Token);
}
catch (OperationCanceledException)
{
}
finally
{
if (_appCts is not null)
{
await _appCts.CancelAsync();
}
if (_shutdownRequested)
{
try
{
await _transport.ShutdownAsync(CancellationToken.None);
}
catch
{
}
}
else
{
try
{
await _transport.DetachAsync(CancellationToken.None);
}
catch
{
}
}
_outputPipe.Writer.Complete();
try
{
await networkOutputTask;
}
catch
{
}
try
{
await embeddedRunTask;
}
catch
{
}
}
return 0;
}
public async ValueTask DisposeAsync()
{
if (_appCts is not null)
{
await _appCts.CancelAsync();
_appCts.Dispose();
_appCts = null;
}
if (_embeddedTerminal is not null)
{
await _embeddedTerminal.DisposeAsync();
_embeddedTerminal = null;
}
}
internal async Task SendInputAsync(byte[] buffer, int offset, int count)
{
await _transport.SendInputAsync(buffer.AsMemory(offset, count), CancellationToken.None);
}
internal async Task HandleCommandByteAsync(byte commandByte)
{
switch (commandByte)
{
case (byte)'d':
case (byte)'D':
_app?.RequestStop();
break;
case (byte)'l':
case (byte)'L':
await _transport.ClaimLeadAsync(CancellationToken.None);
break;
case (byte)'q':
case (byte)'Q':
_shutdownRequested = true;
_app?.RequestStop();
break;
case 0x1D:
await _transport.SendInputAsync(new byte[] { 0x1D }, CancellationToken.None);
break;
}
}
private async Task PumpNetworkOutputAsync(CancellationToken cancellationToken)
{
try
{
await foreach (AttachTransportFrame frame in _transport.ReadFramesAsync(cancellationToken))
{
switch (frame.Kind)
{
case AttachTransportFrameKind.Output:
await _outputPipe.Writer.WriteAsync(frame.Data, cancellationToken);
await _outputPipe.Writer.FlushAsync(cancellationToken);
_app?.Invalidate();
break;
case AttachTransportFrameKind.Resize:
(int width, int height) = frame.GetResize();
_remoteWidth = width;
_remoteHeight = height;
_handle?.Resize(width, height);
_embeddedTerminal?.Resize(width, height);
_app?.Invalidate();
break;
case AttachTransportFrameKind.LeaderChanged:
_isLeader = frame.GetIsLeader();
if (_isLeader)
{
await SendResizeForCurrentDisplayAsync();
}
_app?.Invalidate();
break;
case AttachTransportFrameKind.Exit:
_app?.RequestStop();
return;
}
}
}
catch (OperationCanceledException)
{
}
catch
{
_app?.RequestStop();
}
finally
{
_outputPipe.Writer.Complete();
}
}
private async Task HandleDisplayResizeAsync(int displayWidth, int displayHeight)
{
_displayWidth = displayWidth;
_displayHeight = displayHeight;
if (_isLeader)
{
await SendResizeForCurrentDisplayAsync();
}
}
private async Task<bool> TryApplyInitialResizeAsync(CancellationToken cancellationToken)
{
if (!_isLeader)
{
return false;
}
(int Width, int Height)? displaySize = TryGetCurrentDisplaySize();
if (displaySize is null)
{
return false;
}
_displayWidth = displaySize.Value.Width;
_displayHeight = displaySize.Value.Height;
if (_displayWidth <= 0 || _displayHeight <= 0)
{
return false;
}
if (_displayWidth == _remoteWidth && _displayHeight == _remoteHeight)
{
return false;
}
_remoteWidth = _displayWidth;
_remoteHeight = _displayHeight;
await _transport.SendResizeAsync(_remoteWidth, _remoteHeight, cancellationToken);
return true;
}
private async Task SendResizeForCurrentDisplayAsync()
{
if (_displayWidth <= 0 || _displayHeight <= 0)
{
return;
}
if (_displayWidth == _remoteWidth && _displayHeight == _remoteHeight)
{
return;
}
_remoteWidth = _displayWidth;
_remoteHeight = _displayHeight;
_handle?.Resize(_remoteWidth, _remoteHeight);
_embeddedTerminal?.Resize(_remoteWidth, _remoteHeight);
await _transport.SendResizeAsync(_remoteWidth, _remoteHeight, CancellationToken.None);
_app?.Invalidate();
}
private static (int Width, int Height)? TryGetCurrentDisplaySize()
{
try
{
int width = Console.WindowWidth;
int height = Console.WindowHeight;
return width > 0 && height > 0 ? (width, height) : null;
}
catch (IOException)
{
return null;
}
catch (InvalidOperationException)
{
return null;
}
}
private sealed class ResizeFilter(JoinSessionClient client) : IHex1bTerminalPresentationFilter
{
public ValueTask OnSessionStartAsync(int width, int height, DateTimeOffset timestamp, CancellationToken ct = default) =>
new(client.HandleDisplayResizeAsync(width, height));
public ValueTask<IReadOnlyList<AnsiToken>> OnOutputAsync(IReadOnlyList<AppliedToken> appliedTokens, TimeSpan elapsed, CancellationToken ct = default) =>
ValueTask.FromResult<IReadOnlyList<AnsiToken>>([.. appliedTokens.Select(token => token.Token)]);
public ValueTask OnInputAsync(IReadOnlyList<AnsiToken> tokens, TimeSpan elapsed, CancellationToken ct = default) => default;
public ValueTask OnResizeAsync(int width, int height, TimeSpan elapsed, CancellationToken ct = default) =>
new(client.HandleDisplayResizeAsync(width, height));
public ValueTask OnSessionEndAsync(TimeSpan elapsed, CancellationToken ct = default) => default;
}
private sealed class InputInterceptStream(JoinSessionClient client) : Stream
{
private bool _inCommandMode;
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override void Flush()
{
}
public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) =>
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
const byte CtrlRightBracket = 0x1D;
int start = offset;
for (int index = offset; index < offset + count; index++)
{
if (_inCommandMode)
{
_inCommandMode = false;
await client.HandleCommandByteAsync(buffer[index]);
start = index + 1;
continue;
}
if (buffer[index] == CtrlRightBracket)
{
if (index > start)
{
await client.SendInputAsync(buffer, start, index - start);
}
_inCommandMode = true;
start = index + 1;
}
}
if (start < offset + count && !_inCommandMode)
{
await client.SendInputAsync(buffer, start, offset + count - start);
}
}
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
byte[] copy = buffer.ToArray();
await WriteAsync(copy, 0, copy.Length, cancellationToken);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment