Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/OneWare.Terminal/Provider/PseudoTerminalConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ public void SendData(byte[] data)
_ = terminal.WriteAsync(data, 0, data.Length);
}

public void KillProcess()
{
try
{
if (!terminal.Process.HasExited)
terminal.Process.Kill(true);
}
catch
{
// Best effort: the process may already have exited or be unkillable.
}
}

public void SuppressOutput(byte[] sequence)
{
if (sequence.Length == 0) return;
Expand Down
7 changes: 7 additions & 0 deletions src/OneWare.Terminal/ViewModels/TerminalViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,13 @@ public void SendInterrupt()
if (Connection?.IsConnected ?? false) Connection.SendData([0x03]);
}

public void KillProcess()
{
// Forcibly terminate the shell and any child processes. Used as a last
// resort when an interrupt (Ctrl+C) fails to free a stuck command.
if (Connection is PseudoTerminalConnection ptc) ptc.KillProcess();
}

public void SuppressEcho(byte[] data)
{
if (Connection is IOutputSuppressor suppressor)
Expand Down
30 changes: 24 additions & 6 deletions src/OneWare.TerminalManager/ViewModels/TerminalManagerViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,18 @@ void OnDataReceived(object? sender, VtNetCore.Avalonia.DataReceivedEventArgs arg
{
var partialOutput = output.ToString();
// The command exceeded its timeout or was cancelled but is still running
// in the shell. Interrupt it so the shell returns to a usable prompt;
// otherwise the foreground command keeps running and every subsequent
// command sent to this (reused) terminal hangs too.
await TryRecoverPromptAsync(terminal, resultTcs.Task);
// in the shell. First try a gentle interrupt (Ctrl+C) so the shell returns
// to a usable prompt and the terminal stays reusable.
var recovered = await TryRecoverPromptAsync(terminal, resultTcs.Task);
if (!recovered)
{
// The interrupt did not free the shell (the process ignores SIGINT or is
// itself hung). Forcibly kill the process tree and discard this terminal
// so it is never reused in a stuck state by a subsequent command.
terminal.KillProcess();
DiscardAutomationTerminal(terminal);
}

result = new TerminalExecutionResult(partialOutput, -1, true);
}
finally
Expand All @@ -185,7 +193,7 @@ void OnDataReceived(object? sender, VtNetCore.Avalonia.DataReceivedEventArgs arg
return result;
}

private static async Task TryRecoverPromptAsync(TerminalViewModel terminal,
private static async Task<bool> TryRecoverPromptAsync(TerminalViewModel terminal,
Task<TerminalExecutionResult> resultTask)
{
terminal.SendInterrupt();
Expand All @@ -194,13 +202,23 @@ private static async Task TryRecoverPromptAsync(TerminalViewModel terminal,
// Give the shell a moment to process the interrupt and emit a fresh
// prompt marker so the terminal can be reused by the next command.
await resultTask.WaitAsync(TimeSpan.FromSeconds(3));
return true;
}
catch (TimeoutException)
{
// Best-effort recovery: fall through even if the shell did not respond.
return false;
}
}

private void DiscardAutomationTerminal(TerminalViewModel terminal)
{
Dispatcher.UIThread.Post(() =>
{
var tab = Terminals.FirstOrDefault(t => ReferenceEquals(t.Terminal, terminal));
tab?.Close();
});
}

public void ExecScriptInTerminal(string scriptPath, bool elevated, string title)
{
try
Expand Down
Loading