From fd61ca8a89c5e049baa0f4aa982634db1463d6b3 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Thu, 16 Jul 2026 16:06:29 +0200 Subject: [PATCH 1/2] Bound the time a completion request may take. Shells don't interrupt slow completion commands: a completion callback that hangs (for example on a network operation) blocks the user's prompt until they hit ctrl-c. Give up after 5 seconds and exit with a non-zero code, which the generated completion scripts already treat as 'no completions'. The same guard also silences callbacks that throw, which would otherwise dump a stack trace onto the user's prompt. --- src/cli.toit | 4 +++- src/completion_.toit | 26 ++++++++++++++++++++++++++ tests/completion_test.toit | 29 +++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/cli.toit b/src/cli.toit index 29766af..75c3b8f 100644 --- a/src/cli.toit +++ b/src/cli.toit @@ -377,7 +377,9 @@ class Command: completion-args := arguments[1..] if not completion-args.is-empty and completion-args[0] == "--": completion-args = completion-args[1..] - result := complete_ this completion-args + result := complete-with-timeout_ this completion-args + // The completion scripts treat a non-zero exit as "no completions". + if not result: exit 1 result.candidates.do: | candidate/CompletionCandidate_ | print candidate.to-string if result.extensions and not result.extensions.is-empty: diff --git a/src/completion_.toit b/src/completion_.toit index f489236..4a03b91 100644 --- a/src/completion_.toit +++ b/src/completion_.toit @@ -54,6 +54,32 @@ class CompletionResult_: constructor .candidates --.directive=DIRECTIVE-DEFAULT_ --.extensions=null: +/** +The maximum time a completion request may take. + +Completion requests run while the user is waiting for the shell to + react to a tab-press, and shells don't interrupt slow completion + commands. If a completion callback misbehaves (for example by hanging + on a network operation), this limit bounds how long the user's prompt + is blocked. +*/ +COMPLETION-TIMEOUT-MS_ ::= 5_000 + +/** +Computes completion candidates like $complete_, but guards against + misbehaving completion callbacks. + +Returns null if the computation throws or takes longer than + $timeout-ms. The caller should then exit with a non-zero exit code, + which the generated completion scripts treat as "no completions". +*/ +complete-with-timeout_ root/Command arguments/List --timeout-ms/int=COMPLETION-TIMEOUT-MS_ -> CompletionResult_?: + result/CompletionResult_? := null + catch: + with-timeout --ms=timeout-ms: + result = complete_ root arguments + return result + /** Computes completion candidates for the given $arguments. diff --git a/tests/completion_test.toit b/tests/completion_test.toit index 79237f2..af74f54 100644 --- a/tests/completion_test.toit +++ b/tests/completion_test.toit @@ -44,6 +44,8 @@ main: test-command-group-completion test-command-group-after-default-entered test-command-group-with-extensions + test-slow-callback-times-out + test-throwing-callback-yields-null test-empty-input: root := cli.Command "app" @@ -867,3 +869,30 @@ test-command-group-with-extensions: expect-equals 1 result.extensions.size expect (result.extensions.contains ".toit") + +test-slow-callback-times-out: + root := cli.Command "app" + --options=[ + cli.Option "host" --help="Target host." + --completion=:: | context/cli.CompletionContext | + sleep --ms=1_000 + [cli.CompletionCandidate "too-late"], + ] + --run=:: null + result := complete-with-timeout_ root ["--host", ""] --timeout-ms=50 + expect-null result + + // A fast callback is unaffected. + result = complete-with-timeout_ root ["--", ""] --timeout-ms=1_000 + expect-not-null result + +test-throwing-callback-yields-null: + root := cli.Command "app" + --options=[ + cli.Option "host" --help="Target host." + --completion=:: | context/cli.CompletionContext | + throw "callback is broken", + ] + --run=:: null + result := complete-with-timeout_ root ["--host", ""] + expect-null result From 51105487e80d009bba7300f81f66855f0069785c Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Thu, 16 Jul 2026 23:25:54 +0200 Subject: [PATCH 2/2] Silence the default logger during completion requests. Completion candidates are printed to stdout, so any library logging from completion callbacks (for example the file-lock logging of the cache) would corrupt the completion output. --- src/cli.toit | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cli.toit b/src/cli.toit index 75c3b8f..ce3196b 100644 --- a/src/cli.toit +++ b/src/cli.toit @@ -373,6 +373,10 @@ class Command: // Handle __complete requests before any other processing. if add-completion and not arguments.is-empty and arguments[0] == "__complete": + // Silence the default logger: the completion candidates are printed to + // stdout, so any library logging from completion callbacks would + // corrupt the completion output. + log.set-default (log.default.with-level log.FATAL-LEVEL) if add-ui-help: add-ui-options_ completion-args := arguments[1..] if not completion-args.is-empty and completion-args[0] == "--":