davsclaus commented on code in PR #27931: URL: https://github.com/apache/camel/pull/27931#discussion_r4290590786
########## dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliRunner.java: ########## @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.apache.camel.dsl.jbang.core.common.CliExitCode; +import org.apache.camel.dsl.jbang.core.common.Printer; +import org.apache.camel.dsl.jbang.core.common.StringPrinter; +import org.apache.camel.util.json.JsonObject; +import picocli.CommandLine; + +/** + * Runs a Camel CLI command in this JVM, as camel ask and the TUI do, and gives what a script running camel gets: the + * exit code, and stdout and stderr apart. The commands share the printer of {@link CamelJBangMain} and the picocli + * command tree, so one command runs at a time: the camel shell takes the same {@link #LOCK}. + */ +public final class CliRunner { + + /** + * The lock of the commands that run in this JVM. + */ + public static final Object LOCK = CamelJBangMain.class; + + private CliRunner() { + } + + /** + * What a command did. + * + * @param exitCode the exit code, as camel exits with it ({@link CliExitCode#TIMEOUT} when it did not finish in + * time) + * @param stdout the output of the command + * @param stderr the errors, warnings and tips + * @param elapsedMs how long the command ran + * @param timedOut whether the command did not finish in time, and was interrupted + */ + public record CliResult(int exitCode, String stdout, String stderr, long elapsedMs, boolean timedOut) { + + public boolean isSuccess() { + return exitCode == CliExitCode.OK; + } + + /** + * The result as JSON, for an AI agent: exitCode, stdout, stderr, elapsedMs and timedOut, where stdout and + * stderr are cut at the given number of characters. + */ + public JsonObject toJson(int maxChars) { + JsonObject answer = new JsonObject(); + answer.put("exitCode", exitCode); + answer.put("stdout", cut(stdout, maxChars)); + answer.put("stderr", cut(stderr, maxChars)); + answer.put("elapsedMs", elapsedMs); + answer.put("timedOut", timedOut); + return answer; + } + + private static String cut(String text, int maxChars) { + return text.length() > maxChars ? text.substring(0, maxChars) + "\n... (truncated)" : text; + } + } + + /** + * Runs the command, and waits until it has finished. + * + * @param args the command line without camel, such as <tt>get route --json</tt> + */ + public static CliResult run(List<String> args) { + return run(args, 0); + } + + /** + * Runs the command, and interrupts it when it does not finish in time. + * + * @param args the command line without camel, such as <tt>get route --json</tt> + * @param timeoutMillis how long the command may run, or 0 to wait until it has finished + */ + public static CliResult run(List<String> args, long timeoutMillis) { + CommandLine commandLine = CamelJBangMain.getCommandLine(); + if (commandLine == null) { + throw new IllegalStateException("The Camel CLI is not initialized"); + } + Output output = new Output(); + long start = System.nanoTime(); + if (timeoutMillis <= 0) { + int code = execute(commandLine, args, output); + return output.result(code, elapsed(start), false); + } + + FutureTask<Integer> task = new FutureTask<>(() -> execute(commandLine, args, output)); + Thread thread = new Thread(task, "camel-cli-runner"); + thread.setDaemon(true); + thread.start(); + try { + int code = task.get(timeoutMillis, TimeUnit.MILLISECONDS); + return output.result(code, elapsed(start), false); + } catch (TimeoutException e) { + task.cancel(true); Review Comment: Thanks, it was worse than detection: `synchronized` does not answer an interrupt either, so a command that timed out while it waited for the lock still ran once the other command ended. Fixed in the next commit: - The caller marks the command as abandoned when it gives up; once the command gets the lock it returns without running (test: `config set` that timed out waiting for the lock sets nothing afterwards; it did before). - After the interrupt the runner waits up to 2 seconds for the command to end, and the result says what is left: `it waited for another command that is still running, and does not run`, or `it has not stopped yet, and the next command waits until it has` (test: a command that ignores the interrupt). The lock stays the `CamelJBangMain.class` monitor shared with the camel shell and the AI panel; a command that never ends still holds it, but it is no longer silent. _Claude Code on behalf of davsclaus_ ########## dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java: ########## @@ -1868,6 +1874,8 @@ String callRunExample(Map<String, Object> args) throws Exception { if (outcome.log() != null) { result.put("log", outcome.log()); } + // a tool error (MCP isError), which still has the log that tells why + throw new ToolExecutionException(Jsoner.serialize(result)); Review Comment: Fixed in the next commit: `handleToolsCall` catches `ToolExecutionException` before the generic `Exception` and answers with its message as is (`isError: true`), so the JSON of a failed `tui_run_example` stays JSON. Other exceptions keep the `Error: ` prefix. `TuiMcpServerToolErrorTest` now expects `expression is required` without the prefix. _Claude Code on behalf of davsclaus_ -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
