fix fourth core dump on export option
This commit is contained in:
+104
-132
@@ -10,19 +10,13 @@ namespace HanaTui.Tui;
|
||||
/// Left: System stats (CPU, RAM, Swap, elapsed)
|
||||
/// Right: Streaming operation log
|
||||
///
|
||||
/// Abort behavior:
|
||||
/// First Q -> warning shown in log
|
||||
/// Second Q (within 3s) -> sends cancellation (SIGTERM -> wait 5s -> SIGKILL)
|
||||
///
|
||||
/// Post-completion:
|
||||
/// Stats freeze, log stays.
|
||||
/// "Returning in 10s... [any key to stay]" countdown.
|
||||
/// If key pressed: stays until Enter/Esc.
|
||||
/// Abort: first Q warns, second Q within 3s sends SIGTERM then SIGKILL.
|
||||
/// Post-completion: 10s countdown, any key cancels and holds; Enter/Esc returns.
|
||||
/// </summary>
|
||||
public static class TaskRunnerScreen
|
||||
{
|
||||
private const int CountdownSeconds = 10;
|
||||
private const double AbortWindowSeconds = 3.0;
|
||||
private const int CountdownSeconds = 10;
|
||||
private const double AbortWindowSeconds = 3.0;
|
||||
|
||||
public static async Task RunAsync(
|
||||
string operationTitle,
|
||||
@@ -30,20 +24,16 @@ public static class TaskRunnerScreen
|
||||
{
|
||||
AnsiConsole.Clear();
|
||||
|
||||
var logPanel = new LogPanel();
|
||||
var stats = new SystemStats();
|
||||
var cts = new CancellationTokenSource();
|
||||
var logPanel = new LogPanel();
|
||||
var stats = new SystemStats();
|
||||
var cts = new CancellationTokenSource();
|
||||
var startTime = DateTime.Now;
|
||||
|
||||
var operationDone = false;
|
||||
var operationDone = false;
|
||||
var operationSuccess = false;
|
||||
DateTime? firstQTime = null;
|
||||
|
||||
// Abort state machine
|
||||
DateTime? firstQPressTime = null;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Key listener task — runs concurrently with the Live render loop
|
||||
// -----------------------------------------------------------------------
|
||||
// Key listener
|
||||
var keyTask = Task.Run(async () =>
|
||||
{
|
||||
while (!operationDone)
|
||||
@@ -51,25 +41,24 @@ public static class TaskRunnerScreen
|
||||
if (Console.KeyAvailable)
|
||||
{
|
||||
var key = Console.ReadKey(intercept: true);
|
||||
if (key.Key == ConsoleKey.Q || key.KeyChar == 'q' || key.KeyChar == 'Q')
|
||||
if (key.Key == ConsoleKey.Q || key.KeyChar is 'q' or 'Q')
|
||||
{
|
||||
if (cts.IsCancellationRequested) break; // already cancelled
|
||||
if (cts.IsCancellationRequested) break;
|
||||
|
||||
if (firstQPressTime is null)
|
||||
if (firstQTime is null)
|
||||
{
|
||||
firstQPressTime = DateTime.Now;
|
||||
logPanel.Add("[WARN] Press Q again within 3 seconds to abort the operation.");
|
||||
firstQTime = DateTime.Now;
|
||||
logPanel.Add("[WARN] Press Q again within 3 seconds to abort.");
|
||||
}
|
||||
else if ((DateTime.Now - firstQPressTime.Value).TotalSeconds <= AbortWindowSeconds)
|
||||
else if ((DateTime.Now - firstQTime.Value).TotalSeconds <= AbortWindowSeconds)
|
||||
{
|
||||
logPanel.Add("[WARN] Aborting operation...");
|
||||
await cts.CancelAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Window expired, treat as first press again
|
||||
firstQPressTime = DateTime.Now;
|
||||
logPanel.Add("[WARN] Press Q again within 3 seconds to abort the operation.");
|
||||
firstQTime = DateTime.Now;
|
||||
logPanel.Add("[WARN] Press Q again within 3 seconds to abort.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,16 +66,12 @@ public static class TaskRunnerScreen
|
||||
}
|
||||
}, CancellationToken.None);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Main operation task
|
||||
// -----------------------------------------------------------------------
|
||||
// Operation task
|
||||
var operationTask = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
operationSuccess = await operation(
|
||||
line => logPanel.Add(line),
|
||||
cts.Token);
|
||||
operationSuccess = await operation(line => logPanel.Add(line), cts.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -99,93 +84,77 @@ public static class TaskRunnerScreen
|
||||
}
|
||||
}, CancellationToken.None);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Live render loop
|
||||
// -----------------------------------------------------------------------
|
||||
await AnsiConsole.Live(BuildLayout(logPanel, stats, startTime, "[yellow]Running...[/]"))
|
||||
await AnsiConsole.Live(BuildLayout(operationTitle, logPanel, stats, startTime, RunState.Running))
|
||||
.AutoClear(false)
|
||||
.StartAsync(async ctx =>
|
||||
{
|
||||
while (!operationDone)
|
||||
{
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
ctx.UpdateTarget(
|
||||
BuildLayout(logPanel, stats, startTime, "[yellow]Running...[/]"));
|
||||
ctx.UpdateTarget(BuildLayout(operationTitle, logPanel, stats, startTime, RunState.Running));
|
||||
await Task.Delay(800, CancellationToken.None);
|
||||
}
|
||||
|
||||
// Final render with result
|
||||
var elapsed2 = DateTime.Now - startTime;
|
||||
var statusMsg = cts.IsCancellationRequested
|
||||
? "[red]Aborted[/]"
|
||||
: operationSuccess
|
||||
? "[green]Completed successfully[/]"
|
||||
: "[red]Failed[/]";
|
||||
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
RunState finalState;
|
||||
if (cts.IsCancellationRequested)
|
||||
logPanel.Add($"[WARN] Operation aborted after {FormatElapsed(elapsed2)}.");
|
||||
{
|
||||
finalState = RunState.Aborted;
|
||||
logPanel.Add($"[WARN] Operation aborted after {FormatElapsed(elapsed)}.");
|
||||
}
|
||||
else if (operationSuccess)
|
||||
logPanel.Add($"[DONE] Operation completed in {FormatElapsed(elapsed2)}.");
|
||||
{
|
||||
finalState = RunState.Success;
|
||||
logPanel.Add($"[DONE] Operation completed in {FormatElapsed(elapsed)}.");
|
||||
}
|
||||
else
|
||||
logPanel.Add($"[ERR ] Operation failed after {FormatElapsed(elapsed2)}.");
|
||||
{
|
||||
finalState = RunState.Failed;
|
||||
logPanel.Add($"[ERR ] Operation failed after {FormatElapsed(elapsed)}.");
|
||||
}
|
||||
|
||||
ctx.UpdateTarget(BuildLayout(logPanel, stats, startTime, statusMsg));
|
||||
ctx.UpdateTarget(BuildLayout(operationTitle, logPanel, stats, startTime, finalState));
|
||||
});
|
||||
|
||||
// Clean up
|
||||
stats.Dispose();
|
||||
await operationTask; // ensure it's fully done
|
||||
await operationTask;
|
||||
operationDone = true;
|
||||
await keyTask;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Post-completion: countdown or hold
|
||||
// -----------------------------------------------------------------------
|
||||
await PostCompletionWaitAsync(logPanel, stats, startTime, operationSuccess, cts.IsCancellationRequested);
|
||||
await PostCompletionWaitAsync(operationTitle, logPanel, stats, startTime,
|
||||
cts.IsCancellationRequested ? RunState.Aborted :
|
||||
operationSuccess ? RunState.Success : RunState.Failed);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Post-completion hold screen
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private enum RunState { Running, Success, Failed, Aborted }
|
||||
|
||||
private static async Task PostCompletionWaitAsync(
|
||||
LogPanel logPanel,
|
||||
SystemStats stats,
|
||||
DateTime startTime,
|
||||
bool success,
|
||||
bool aborted)
|
||||
string title, LogPanel logPanel, SystemStats stats, DateTime startTime, RunState state)
|
||||
{
|
||||
var statusMsg = aborted ? "[red]Aborted[/]" :
|
||||
success ? "[green]Completed[/]" : "[red]Failed[/]";
|
||||
|
||||
var countdownCancelled = false;
|
||||
var returnNow = false;
|
||||
var staying = false;
|
||||
var startCountdown = DateTime.Now;
|
||||
|
||||
// Start 10-second countdown in background
|
||||
var countdownTask = Task.Run(async () =>
|
||||
{
|
||||
for (int i = CountdownSeconds; i > 0 && !countdownCancelled; i--)
|
||||
{
|
||||
await Task.Delay(1000, CancellationToken.None);
|
||||
}
|
||||
if (!countdownCancelled)
|
||||
returnNow = true;
|
||||
if (!countdownCancelled) returnNow = true;
|
||||
}, CancellationToken.None);
|
||||
|
||||
await AnsiConsole.Live(BuildLayout(logPanel, stats, startTime, statusMsg))
|
||||
await AnsiConsole.Live(BuildLayout(title, logPanel, stats, startTime, state))
|
||||
.AutoClear(false)
|
||||
.StartAsync(async ctx =>
|
||||
{
|
||||
var secondsLeft = CountdownSeconds;
|
||||
var staying = false;
|
||||
|
||||
while (!returnNow)
|
||||
{
|
||||
if (Console.KeyAvailable)
|
||||
{
|
||||
var key = Console.ReadKey(intercept: true);
|
||||
countdownCancelled = true;
|
||||
staying = true;
|
||||
|
||||
if (key.Key is ConsoleKey.Enter or ConsoleKey.Escape)
|
||||
{
|
||||
@@ -193,27 +162,26 @@ public static class TaskRunnerScreen
|
||||
break;
|
||||
}
|
||||
|
||||
// Any other key: cancel countdown, show "hold" message
|
||||
logPanel.Add("[INFO] Countdown cancelled. Press [Enter] or [Esc] to return to menu.");
|
||||
if (!staying)
|
||||
{
|
||||
staying = true;
|
||||
logPanel.Add("[INFO] Countdown cancelled. Press Enter or Esc to return to menu.");
|
||||
}
|
||||
}
|
||||
|
||||
// Update footer countdown
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
string footer;
|
||||
string footerText;
|
||||
if (!staying)
|
||||
{
|
||||
secondsLeft = CountdownSeconds -
|
||||
(int)(DateTime.Now - startTime).TotalSeconds; // approximate
|
||||
// recompute properly
|
||||
footer = $"[dim]Returning to menu in {Math.Max(0, secondsLeft)}s... " +
|
||||
"[any key to stay][/]";
|
||||
var secsLeft = Math.Max(0, CountdownSeconds -
|
||||
(int)(DateTime.Now - startCountdown).TotalSeconds);
|
||||
footerText = $"Returning to menu in {secsLeft}s... (any key to stay)";
|
||||
}
|
||||
else
|
||||
{
|
||||
footer = "[dim]Press [Enter] or [Esc] to return to menu.[/]";
|
||||
footerText = "Press Enter or Esc to return to menu.";
|
||||
}
|
||||
|
||||
ctx.UpdateTarget(BuildLayoutWithFooter(logPanel, stats, startTime, statusMsg, footer));
|
||||
ctx.UpdateTarget(BuildLayout(title, logPanel, stats, startTime, state, footerText));
|
||||
await Task.Delay(200, CancellationToken.None);
|
||||
}
|
||||
});
|
||||
@@ -222,74 +190,78 @@ public static class TaskRunnerScreen
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Layout builders
|
||||
// Layout
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private static IRenderable BuildLayout(
|
||||
string title,
|
||||
LogPanel logPanel,
|
||||
SystemStats stats,
|
||||
DateTime startTime,
|
||||
string statusMsg)
|
||||
{
|
||||
return BuildLayoutWithFooter(logPanel, stats, startTime, statusMsg, footer: null);
|
||||
}
|
||||
|
||||
private static IRenderable BuildLayoutWithFooter(
|
||||
LogPanel logPanel,
|
||||
SystemStats stats,
|
||||
DateTime startTime,
|
||||
string statusMsg,
|
||||
string? footer)
|
||||
RunState state,
|
||||
string? footerText = null)
|
||||
{
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
var snap = stats.CurrentSnapshot;
|
||||
var snap = stats.CurrentSnapshot;
|
||||
|
||||
// Stats panel (left) — fixed width ~36 chars
|
||||
var statsPanelWidth = 36;
|
||||
const int statsPanelWidth = 40;
|
||||
var statsRenderable = StatsPanel.Build(snap, elapsed, statsPanelWidth - 4);
|
||||
var logRenderable = logPanel.Build();
|
||||
|
||||
// Log panel (right) — fills remaining space
|
||||
var logRenderable = logPanel.Build();
|
||||
|
||||
var layout = new Layout("root")
|
||||
.SplitColumns(
|
||||
new Layout("stats").Size(statsPanelWidth),
|
||||
new Layout("log"));
|
||||
|
||||
var layout = new Layout("root").SplitColumns(
|
||||
new Layout("stats").Size(statsPanelWidth),
|
||||
new Layout("log"));
|
||||
layout["stats"].Update(statsRenderable);
|
||||
layout["log"].Update(logRenderable);
|
||||
|
||||
// Wrap in a grid so we can add a footer row
|
||||
var outerGrid = new Grid();
|
||||
outerGrid.AddColumn(new GridColumn());
|
||||
|
||||
// Title row
|
||||
outerGrid.AddRow(new Markup(
|
||||
$"[bold dodgerblue1] Running:[/] [yellow]{Markup.Escape(ExtractTitle(statusMsg))}[/] " +
|
||||
$"Status: {statusMsg}"));
|
||||
|
||||
// Title row — state label uses Text objects, not markup interpolation
|
||||
var titleGrid = new Grid();
|
||||
titleGrid.AddColumn(new GridColumn().NoWrap());
|
||||
titleGrid.AddColumn(new GridColumn().NoWrap());
|
||||
titleGrid.AddColumn(new GridColumn().NoWrap());
|
||||
titleGrid.AddColumn(new GridColumn().NoWrap());
|
||||
titleGrid.AddRow(
|
||||
new Markup("[bold dodgerblue1] Operation:[/]"),
|
||||
new Text(" " + title, new Style(Color.Yellow)),
|
||||
new Markup(" [bold dodgerblue1]Status:[/]"),
|
||||
StateLabel(state)
|
||||
);
|
||||
outerGrid.AddRow(titleGrid);
|
||||
outerGrid.AddRow(layout);
|
||||
|
||||
if (footer is not null)
|
||||
outerGrid.AddRow(new Markup($"\n {footer} [dim][[Q]] Abort[/]"));
|
||||
else
|
||||
outerGrid.AddRow(new Markup("\n [dim][[Q]] Press once to warn, twice to abort[/]"));
|
||||
// Footer row — plain Text, no markup parsing of dynamic content
|
||||
var footerGrid = new Grid();
|
||||
footerGrid.AddColumn(new GridColumn().NoWrap());
|
||||
footerGrid.AddColumn(new GridColumn().NoWrap());
|
||||
|
||||
var footerLeft = footerText is not null
|
||||
? new Text(" " + footerText, new Style(Color.Grey, decoration: Decoration.Dim))
|
||||
: (IRenderable)new Text(" Press Q to abort (twice within 3s)", new Style(Color.Grey, decoration: Decoration.Dim));
|
||||
|
||||
var footerRight = new Text(" [Q] abort", new Style(Color.Grey, decoration: Decoration.Dim));
|
||||
|
||||
footerGrid.AddRow(footerLeft, footerRight);
|
||||
outerGrid.AddRow(footerGrid);
|
||||
|
||||
return outerGrid;
|
||||
}
|
||||
|
||||
private static string ExtractTitle(string statusMsg)
|
||||
private static IRenderable StateLabel(RunState state) => state switch
|
||||
{
|
||||
// statusMsg is markup like "[yellow]Running...[/]" — we just return a static title
|
||||
return "HANA Operation";
|
||||
}
|
||||
RunState.Running => new Markup(" [yellow]Running...[/]"),
|
||||
RunState.Success => new Markup(" [green]Completed[/]"),
|
||||
RunState.Failed => new Markup(" [red]Failed[/]"),
|
||||
RunState.Aborted => new Markup(" [red]Aborted[/]"),
|
||||
_ => new Text(""),
|
||||
};
|
||||
|
||||
private static string FormatElapsed(TimeSpan t)
|
||||
{
|
||||
if (t.TotalHours >= 1)
|
||||
return $"{(int)t.TotalHours}h {t.Minutes:D2}m {t.Seconds:D2}s";
|
||||
if (t.TotalMinutes >= 1)
|
||||
return $"{t.Minutes}m {t.Seconds:D2}s";
|
||||
if (t.TotalHours >= 1) return $"{(int)t.TotalHours}h {t.Minutes:D2}m {t.Seconds:D2}s";
|
||||
if (t.TotalMinutes >= 1) return $"{t.Minutes}m {t.Seconds:D2}s";
|
||||
return $"{t.Seconds}s";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user