From 6fedea162c53c8b65e6db52982109e42527c26af Mon Sep 17 00:00:00 2001 From: crl33 <153405669+crl33@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:39:48 -0700 Subject: [PATCH] fix: make Rdx usable headless for bot/server pipelines Non-interactive mode crashed after upload with InvalidCastException because GetFullRows yields List, not IRenderable[]. Also: - Always record results in m_dialogs so --output-file is not empty - Guard empty redirected stdin (DEVNULL IndexOutOfRange) - Skip "Press any key" when stdin is redirected - Skip CanvasImage preview when not interactive Enables Docker/Telegram bots that call SmartImage with --interactive false and a delimited results file. --- .../Commands/Search/SearchCommand.cs | 25 ++++++++++--------- SmartImage.Rdx/Program.cs | 18 ++++++++----- SmartImage.Rdx/Shell/ConsoleUtil.cs | 10 ++++++-- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/SmartImage.Rdx/Commands/Search/SearchCommand.cs b/SmartImage.Rdx/Commands/Search/SearchCommand.cs index 82c209ed..b57cfad4 100644 --- a/SmartImage.Rdx/Commands/Search/SearchCommand.cs +++ b/SmartImage.Rdx/Commands/Search/SearchCommand.cs @@ -98,8 +98,12 @@ private async Task InitQueryAsync(ProgressContext ctx) if (!url) { throw new SmartImageException($"Could not upload {Query}"); } - - m_queryCanvasImg = new CanvasImage(Query.Source.GetSource()); + + // CanvasImage preview is only needed for interactive UI. Building it + // headless has thrown on some image sources; skip when not interactive. + if (CommandSettings.Interactive) { + m_queryCanvasImg = new CanvasImage(Query.Source.GetSource()); + } p.Increment(Elements.COMPLETE / 2); @@ -171,28 +175,25 @@ private async Task RunSearchLiveAsync(LiveDisplayContext c, CancellationToken ct while (!ct.IsCancellationRequested && await Client.ResultChannel.Reader.WaitToReadAsync(ct)) { var result = await Client.ResultChannel.Reader.ReadAsync(ct); + // Always collect for WriteOutputFile (interactive and headless). + // Previously non-interactive mode never filled m_dialogs, so -o files + // were empty even when the search succeeded. + m_dialogs.TryAdd(result, ResultViewState.Create(result)); if (CommandSettings.Interactive) { - - m_dialogs.TryAdd(result, ResultViewState.Create(result)); - m_mainTable.AddRow(result.GetMainRows()); Elements.Prm_SearchResult.AddChoice(result); - } else { - + // GetMainRows returns IList (a List), not IRenderable[]. + // The old cast threw InvalidCastException after upload on Linux headless. var fullRows = result.GetFullRows(); - foreach (var list in fullRows) { - var row = (IRenderable[]) list; - m_mainTable.AddRow(row); + m_mainTable.AddRow(list.ToArray()); } } - c.Refresh(); - } await searchTask; diff --git a/SmartImage.Rdx/Program.cs b/SmartImage.Rdx/Program.cs index a01ea191..eba58c2c 100644 --- a/SmartImage.Rdx/Program.cs +++ b/SmartImage.Rdx/Program.cs @@ -104,7 +104,9 @@ public static async Task Main(string[] args) } finally { - if (x != Common.EC_OK) { + // Never block on a prompt when stdin is redirected / non-interactive + // (bots, Docker, CI). ConfirmAsync crashes with Failed to read input. + if (x != Common.EC_OK && !Console.IsInputRedirected) { await AnsiConsole.ConfirmAsync("Press any key to continue"); } } @@ -186,13 +188,17 @@ private static void HandleArgs(ref string[] args) var pipeInput = ConsoleUtil.ParseInputStream(); - var newArgs = new string[args.Length + 1]; - newArgs[0] = pipeInput; - args.CopyTo(newArgs, 1); + // Ignore empty redirects (DEVNULL / closed pipe) so headless bots + // can pass the query solely via argv. + if (!String.IsNullOrWhiteSpace(pipeInput)) { + var newArgs = new string[args.Length + 1]; + newArgs[0] = pipeInput; + args.CopyTo(newArgs, 1); - args = newArgs; + args = newArgs; - AnsiConsole.WriteLine($"Received input from stdin"); + AnsiConsole.WriteLine($"Received input from stdin"); + } } #endif diff --git a/SmartImage.Rdx/Shell/ConsoleUtil.cs b/SmartImage.Rdx/Shell/ConsoleUtil.cs index 2e2a90f8..0d4d5a67 100644 --- a/SmartImage.Rdx/Shell/ConsoleUtil.cs +++ b/SmartImage.Rdx/Shell/ConsoleUtil.cs @@ -36,7 +36,8 @@ public static string ParseInputStream(int bufSize = 4096, int maxSize = 10_000_0 while ((bytesRead = stdin.Read(buffer, 0, buffer.Length)) > 0) { if (iter == 0) { - if (buffer[0] == s_utf8BomSig[0] + if (bytesRead >= 3 + && buffer[0] == s_utf8BomSig[0] && buffer[1] == s_utf8BomSig[1] && buffer[2] == s_utf8BomSig[2]) { @@ -53,7 +54,12 @@ public static string ParseInputStream(int bufSize = 4096, int maxSize = 10_000_0 // prog?.Report(b2pos); } - if (buffer2[(b2pos - 1)] == '\n' && buffer2[(b2pos - 2)] == '\r') { + // Empty redirected stdin (e.g. DEVNULL) used to IndexOutOfRange on b2pos-1. + if (b2pos == 0) { + return null; + } + + if (b2pos >= 2 && buffer2[(b2pos - 1)] == '\n' && buffer2[(b2pos - 2)] == '\r') { b2pos -= 2; }