This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch cd in repository https://gitbox.apache.org/repos/asf/camel.git
commit cbb75e30bf1ee48ee4c8c8fdb2c2dbb6cce25082 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Dec 4 16:58:17 2025 +0100 CAMEL-22748: camel-jbang - camel debug to do remote attach to existing running Camel --- bom/camel-bom/pom.xml | 5 + .../org/apache/camel/catalog/others.properties | 1 + .../org/apache/camel/catalog/others/cli-debug.json | 15 +++ .../java/org/apache/camel/spi/CliConnector.java | 7 ++ .../org/apache/camel/spi/ShutdownPrepared.java | 6 +- .../impl/debugger/DefaultBacklogDebugger.java | 12 +- .../camel/impl/engine/AbstractCamelContext.java | 23 ++-- docs/components/modules/others/nav.adoc | 1 + .../components/modules/others/pages/cli-debug.adoc | 1 + .../camel/cli/connector/LocalCliConnector.java | 50 +++++++- dsl/{ => camel-cli-debug}/pom.xml | 61 ++++------ .../services/org/apache/camel/debugger-factory | 2 + .../services/org/apache/camel/other.properties | 7 ++ .../src/generated/resources/cli-debug.json | 15 +++ dsl/camel-cli-debug/src/main/docs/cli-debug.adoc | 55 +++++++++ .../cli/debug/CamelCliDebuggerFactory.java | 127 +++++++++++++++++++++ .../camel/dsl/jbang/core/commands/Debug.java | 119 ++++++++++++++++++- dsl/pom.xml | 1 + parent/pom.xml | 5 + 19 files changed, 451 insertions(+), 62 deletions(-) diff --git a/bom/camel-bom/pom.xml b/bom/camel-bom/pom.xml index 10a4c83ec8eb..710d1a661cba 100644 --- a/bom/camel-bom/pom.xml +++ b/bom/camel-bom/pom.xml @@ -441,6 +441,11 @@ <artifactId>camel-cli-connector</artifactId> <version>4.17.0-SNAPSHOT</version> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-cli-debug</artifactId> + <version>4.17.0-SNAPSHOT</version> + </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-clickup</artifactId> diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others.properties b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others.properties index ed5acbb0859e..964ecb3fc0d7 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others.properties +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others.properties @@ -2,6 +2,7 @@ attachments aws-xray azure-schema-registry cli-connector +cli-debug cloud cloudevents cluster diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/cli-debug.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/cli-debug.json new file mode 100644 index 000000000000..4acfc083d275 --- /dev/null +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/cli-debug.json @@ -0,0 +1,15 @@ +{ + "other": { + "kind": "other", + "name": "cli-debug", + "title": "CLI Debug", + "description": "Remote CLI debugger", + "deprecated": false, + "firstVersion": "4.17.0", + "label": "tooling", + "supportLevel": "Preview", + "groupId": "org.apache.camel", + "artifactId": "camel-cli-debug", + "version": "4.17.0-SNAPSHOT" + } +} diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/CliConnector.java b/core/camel-api/src/main/java/org/apache/camel/spi/CliConnector.java index ded99a33e065..36997762846b 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/CliConnector.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/CliConnector.java @@ -31,4 +31,11 @@ public interface CliConnector extends StaticService, NonManagedService { */ void sigterm(); + /** + * Allows to adjust the frequency delay which the CliConnect reacts. Uses 1000 millis by default. + * + * @param delay delay in millis + */ + void updateDelay(int delay); + } diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/ShutdownPrepared.java b/core/camel-api/src/main/java/org/apache/camel/spi/ShutdownPrepared.java index 60f2c8c246e0..666dc38abdcf 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/ShutdownPrepared.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/ShutdownPrepared.java @@ -37,7 +37,7 @@ public interface ShutdownPrepared { * {@link ShutdownStrategy} performs a more aggressive shutdown, calling this method a second time with * <tt>true</tt> for the given forced parameter. For example by graceful stopping any threads or the likes. * <p/> - * In addition a service can also be suspended (not stopped), and when this happens the parameter + * In addition, a service can also be suspended (not stopped), and when this happens the parameter * <tt>suspendOnly</tt> has the value <tt>true</tt>. This can be used to prepare the service for suspension, such as * marking a worker thread to skip action. * <p/> @@ -46,8 +46,8 @@ public interface ShutdownPrepared { * * @param suspendOnly <tt>true</tt> if the intention is to only suspend the service, and not stop/shutdown the * service. - * @param forced <tt>true</tt> is forcing a more aggressive shutdown, <tt>false</tt> is for preparing to - * shutdown. + * @param forced <tt>true</tt> is forcing a more aggressive shutdown, <tt>false</tt> is for preparing to shut + * down. */ void prepareShutdown(boolean suspendOnly, boolean forced); diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultBacklogDebugger.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultBacklogDebugger.java index 2d41f81a70a4..92daba25a46d 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultBacklogDebugger.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultBacklogDebugger.java @@ -50,6 +50,7 @@ import org.apache.camel.spi.CamelEvent.ExchangeEvent; import org.apache.camel.spi.CamelLogger; import org.apache.camel.spi.Condition; import org.apache.camel.spi.Debugger; +import org.apache.camel.spi.ShutdownPrepared; import org.apache.camel.support.BreakpointSupport; import org.apache.camel.support.CamelContextHelper; import org.apache.camel.support.LoggerHelper; @@ -63,7 +64,7 @@ import org.apache.camel.util.json.JsonObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public final class DefaultBacklogDebugger extends ServiceSupport implements BacklogDebugger { +public final class DefaultBacklogDebugger extends ServiceSupport implements BacklogDebugger, ShutdownPrepared { private static final Logger LOG = LoggerFactory.getLogger(DefaultBacklogDebugger.class); @@ -267,6 +268,15 @@ public final class DefaultBacklogDebugger extends ServiceSupport implements Back } } + @Override + public void prepareShutdown(boolean suspendOnly, boolean forced) { + logger.log("Preparing debugger for shutdown"); + // camel is being shutdown so if we are suspended during debugging + // then turn this off so messages can finish processing + suspendMode = false; + resumeMessageProcessing(); + } + /** * Resolves the value of the flag indicating whether the {@code BacklogDebugger} should suspend processing the * messages and wait for a debugger to attach or not. diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java index a321429a24d7..73c7e1165023 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java @@ -2506,6 +2506,18 @@ public abstract class AbstractCamelContext extends BaseService forceLazyInitialization(); + // setup cli-connector if not already done (before debugger) + if (hasService(CliConnector.class) == null) { + CliConnectorFactory ccf = getCamelContextExtension().getContextPlugin(CliConnectorFactory.class); + if (ccf != null && ccf.isEnabled()) { + CliConnector connector = ccf.createConnector(); + addService(connector, true); + // force start cli connector early as otherwise it will be deferred until context is started + // but, we want status available during startup phase + ServiceHelper.startService(connector); + } + } + // auto-detect camel-debug on classpath (if debugger has not been explicit added) boolean debuggerDetected = false; if (getDebugger() == null && hasService(BacklogDebugger.class) == null) { @@ -2530,17 +2542,6 @@ public abstract class AbstractCamelContext extends BaseService addService(backlog, true, true); } } - // setup cli-connector if not already done (after debugger) - if (hasService(CliConnector.class) == null) { - CliConnectorFactory ccf = getCamelContextExtension().getContextPlugin(CliConnectorFactory.class); - if (ccf != null && ccf.isEnabled()) { - CliConnector connector = ccf.createConnector(); - addService(connector, true); - // force start cli connector early as otherwise it will be deferred until context is started - // but, we want status available during startup phase - ServiceHelper.startService(connector); - } - } addService(getManagementStrategy(), false); diff --git a/docs/components/modules/others/nav.adoc b/docs/components/modules/others/nav.adoc index 6ecb7d4919ce..dd4d8071bf88 100644 --- a/docs/components/modules/others/nav.adoc +++ b/docs/components/modules/others/nav.adoc @@ -6,6 +6,7 @@ ** xref:aws-xray.adoc[AWS XRay] ** xref:azure-schema-registry.adoc[Azure Schema Registry] ** xref:cli-connector.adoc[CLI Connector] +** xref:cli-debug.adoc[CLI Debug] ** xref:cloudevents.adoc[Cloudevents] ** xref:csimple-joor.adoc[CSimple jOOR] ** xref:cxf-transport.adoc[CXF Transport] diff --git a/docs/components/modules/others/pages/cli-debug.adoc b/docs/components/modules/others/pages/cli-debug.adoc new file mode 120000 index 000000000000..882fa9b85009 --- /dev/null +++ b/docs/components/modules/others/pages/cli-debug.adoc @@ -0,0 +1 @@ +../../../../../dsl/camel-cli-debug/src/main/docs/cli-debug.adoc \ No newline at end of file diff --git a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java index 0e43279af66d..6019e7d817f0 100644 --- a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java +++ b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java @@ -40,6 +40,7 @@ import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -63,6 +64,7 @@ import org.apache.camel.model.ProcessorDefinition; import org.apache.camel.model.ProcessorDefinitionHelper; import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.language.ExpressionDefinition; +import org.apache.camel.spi.BacklogDebugger; import org.apache.camel.spi.CliConnector; import org.apache.camel.spi.CliConnectorFactory; import org.apache.camel.spi.ContextReloadStrategy; @@ -72,6 +74,7 @@ import org.apache.camel.spi.Resource; import org.apache.camel.spi.ResourceLoader; import org.apache.camel.spi.ResourceReloadStrategy; import org.apache.camel.spi.RoutesLoader; +import org.apache.camel.spi.ShutdownPrepared; import org.apache.camel.support.LoadOnDemandReloadStrategy; import org.apache.camel.support.MessageHelper; import org.apache.camel.support.PatternHelper; @@ -101,6 +104,7 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C private final CliConnectorFactory cliConnectorFactory; private CamelContext camelContext; + private ScheduledFuture scheduledFuture; private int delay = 1000; private long counter; private String platform; @@ -192,13 +196,32 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C messageHistoryFile = createLockFile(lockFile.getName() + "-history.json"); debugFile = createLockFile(lockFile.getName() + "-debug.json"); receiveFile = createLockFile(lockFile.getName() + "-receive.json"); - executor.scheduleWithFixedDelay(this::task, 0, delay, TimeUnit.MILLISECONDS); + scheduledFuture = executor.scheduleWithFixedDelay(this::task, 0, delay, TimeUnit.MILLISECONDS); LOG.info("Camel JBang CLI enabled"); } else { LOG.warn("Cannot create PID file: {}. This integration cannot be managed by Camel JBang CLI.", getPid()); } } + @Override + public void updateDelay(int delay) { + if (this.delay == delay) { + return; + } + if (scheduledFuture != null) { + try { + scheduledFuture.cancel(true); + } catch (Exception e) { + // ignore + } + } + boolean done = scheduledFuture == null || scheduledFuture.isDone(); + if (done) { + this.delay = delay; + scheduledFuture = executor.scheduleWithFixedDelay(this::task, 0, delay, TimeUnit.MILLISECONDS); + } + } + @Override public void sigterm() { // we are terminating @@ -214,6 +237,12 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C public void run() { LOG.info("Camel JBang terminating JVM"); try { + // if we are debugging then detach before stopping camel + BacklogDebugger debugger = camelContext.hasService(BacklogDebugger.class); + if (debugger instanceof ShutdownPrepared sp) { + sp.prepareShutdown(false, true); + } + ServiceHelper.stopAndShutdownServices(debugger); camelContext.stop(); } finally { ServiceHelper.stopAndShutdownService(this); @@ -300,6 +329,8 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C doActionBrowseTask(root); } else if ("receive".equals(action)) { doActionReceiveTask(root); + } else if ("cli-debug".equals(action)) { + doActionCliDebug(root); } } catch (Exception e) { // ignore @@ -312,6 +343,23 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C } } + private void doActionCliDebug(JsonObject root) { + String command = root.getString("command"); + String breakpoint = root.getString("breakpoint"); + BacklogDebugger debugger = camelContext.hasService(BacklogDebugger.class); + if (debugger != null) { + if ("attach".equals(command)) { + if (breakpoint != null) { + debugger.setInitialBreakpoints(breakpoint); + } + debugger.enableDebugger(); + debugger.attach(); + } else if ("detach".equals(command)) { + debugger.detach(); + } + } + } + private void doActionTransformTask(JsonObject root) throws Exception { StopWatch watch = new StopWatch(); long timestamp = System.currentTimeMillis(); diff --git a/dsl/pom.xml b/dsl/camel-cli-debug/pom.xml similarity index 64% copy from dsl/pom.xml copy to dsl/camel-cli-debug/pom.xml index a3188d172db7..f4ae9f60b988 100644 --- a/dsl/pom.xml +++ b/dsl/camel-cli-debug/pom.xml @@ -17,51 +17,41 @@ limitations under the License. --> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.apache.camel</groupId> - <artifactId>camel-parent</artifactId> + <artifactId>dsl</artifactId> <version>4.17.0-SNAPSHOT</version> - <relativePath>../parent/pom.xml</relativePath> </parent> - <artifactId>dsl</artifactId> - <packaging>pom</packaging> - - <name>Camel :: DSL :: Parent</name> - <description>Camel DSL Parent</description> + <artifactId>camel-cli-debug</artifactId> + <packaging>jar</packaging> + <name>Camel :: DSL :: CLI Debug</name> + <description>Remote CLI debugger</description> <properties> - <camel.surefire.parallel>true</camel.surefire.parallel> - <camel-prepare-component>true</camel-prepare-component> + <firstVersion>4.17.0</firstVersion> + <title>CLI Debug</title> + <label>tooling</label> </properties> - <modules> - <module>camel-endpointdsl</module> - <module>camel-componentdsl</module> - <module>camel-cli-connector</module> - <module>camel-dsl-support</module> - <module>camel-dsl-modeline</module> - <module>camel-endpointdsl-support</module> - <module>camel-java-joor-dsl</module> - <module>camel-xml-io-dsl</module> - <module>camel-xml-jaxb-dsl</module> - <module>camel-xml-jaxb-dsl-test</module> - <module>camel-yaml-dsl</module> - <module>camel-kamelet-main</module> - <module>camel-jbang</module> - </modules> + <dependencies> + + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-cli-connector</artifactId> + </dependency> + + </dependencies> <build> <plugins> <plugin> <groupId>org.apache.camel</groupId> <artifactId>camel-package-maven-plugin</artifactId> - <configuration> - </configuration> - <executions> <execution> <id>generate</id> @@ -79,24 +69,12 @@ </execution> </executions> </plugin> - <plugin> - <artifactId>maven-compiler-plugin</artifactId> - <executions> - <execution> - <id>recompile</id> - <goals> - <goal>compile</goal> - </goals> - <phase>process-classes</phase> - </execution> - </executions> - </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <executions> <execution> - <phase>generate-sources</phase> + <phase>initialize</phase> <goals> <goal>add-source</goal> <goal>add-resource</goal> @@ -116,4 +94,5 @@ </plugin> </plugins> </build> + </project> diff --git a/dsl/camel-cli-debug/src/generated/resources/META-INF/services/org/apache/camel/debugger-factory b/dsl/camel-cli-debug/src/generated/resources/META-INF/services/org/apache/camel/debugger-factory new file mode 100644 index 000000000000..1247dbb94f9a --- /dev/null +++ b/dsl/camel-cli-debug/src/generated/resources/META-INF/services/org/apache/camel/debugger-factory @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.component.cli.debug.CamelCliDebuggerFactory diff --git a/dsl/camel-cli-debug/src/generated/resources/META-INF/services/org/apache/camel/other.properties b/dsl/camel-cli-debug/src/generated/resources/META-INF/services/org/apache/camel/other.properties new file mode 100644 index 000000000000..d6d2632c5751 --- /dev/null +++ b/dsl/camel-cli-debug/src/generated/resources/META-INF/services/org/apache/camel/other.properties @@ -0,0 +1,7 @@ +# Generated by camel build tools - do NOT edit this file! +name=cli-debug +groupId=org.apache.camel +artifactId=camel-cli-debug +version=4.17.0-SNAPSHOT +projectName=Camel :: DSL :: CLI Debug +projectDescription=Remote CLI debugger diff --git a/dsl/camel-cli-debug/src/generated/resources/cli-debug.json b/dsl/camel-cli-debug/src/generated/resources/cli-debug.json new file mode 100644 index 000000000000..4acfc083d275 --- /dev/null +++ b/dsl/camel-cli-debug/src/generated/resources/cli-debug.json @@ -0,0 +1,15 @@ +{ + "other": { + "kind": "other", + "name": "cli-debug", + "title": "CLI Debug", + "description": "Remote CLI debugger", + "deprecated": false, + "firstVersion": "4.17.0", + "label": "tooling", + "supportLevel": "Preview", + "groupId": "org.apache.camel", + "artifactId": "camel-cli-debug", + "version": "4.17.0-SNAPSHOT" + } +} diff --git a/dsl/camel-cli-debug/src/main/docs/cli-debug.adoc b/dsl/camel-cli-debug/src/main/docs/cli-debug.adoc new file mode 100644 index 000000000000..265d41de125d --- /dev/null +++ b/dsl/camel-cli-debug/src/main/docs/cli-debug.adoc @@ -0,0 +1,55 @@ += CLI Debug Component +:doctitle: CLI Debug +:shortname: cli-debug +:artifactid: camel-cli-debug +:description: Remote CLI debugger +:since: 4.17 +:supportlevel: Preview +:tabs-sync-option: +//Manually maintained attributes +:camel-spring-boot-name: debug + +*Since Camel {since}* + +The camel-cli-debug enables Camel debugger for Camel CLI (camel-jbang). + +[IMPORTANT] +==== +The camel-cli-debug is only for development purposes, it should **not** be used for production. + +Do not use both `camel-debug` and `camel-cli-debug` JARs in the Camel application classpath. Use only `camel-debug` JAR. +==== + +== Auto-detection from classpath + +To use this implementation all you need to do is to add the `camel-cli-debug` dependency to the classpath, +and Camel should auto-detect this on startup and log as follows: + +[source,text] +---- +Detected: camel-cli-debug JAR (Enabling Camel Debugging) +---- + +== Debugging + +Add `camel-cli-debug` JAR to the classpath of your application, and then run this application. For example with Spring Boot using `mvn spring-boot:run`. + +Then the application starts and Camel detects the CLI debugger that then logs a message, waiting for the CLI to remotely attach. + +From a CLI terminal then you can execute: + +[source,bash] +---- +$ camel debug --remote-attach --name=<pid> +---- + +Where `<pid>` is the process id of the running Camel integration. You can see this from the log such as: + +[source,text] +---- +2025-12-04T15:48:12.542+01:00 INFO 6667 --- [ main] o.a.c.impl.engine.AbstractCamelContext : Detected: camel-cli-debug JAR (Enabling Camel Debugging) +2025-12-04T15:48:12.543+01:00 INFO 6667 --- [ main] o.a.c.c.c.debug.CamelCliDebuggerFactory : ================================================================================ +2025-12-04T15:48:12.543+01:00 INFO 6667 --- [ main] o.a.c.c.c.debug.CamelCliDebuggerFactory : Waiting for CLI to remote attach (camel debug --remote-attach --name=6667) +---- + +To connect the CLI to the Camel application that will then continue to start and perform debugging from the CLI terminal. \ No newline at end of file diff --git a/dsl/camel-cli-debug/src/main/java/org/apache/camel/component/cli/debug/CamelCliDebuggerFactory.java b/dsl/camel-cli-debug/src/main/java/org/apache/camel/component/cli/debug/CamelCliDebuggerFactory.java new file mode 100644 index 000000000000..4018beaf71cc --- /dev/null +++ b/dsl/camel-cli-debug/src/main/java/org/apache/camel/component/cli/debug/CamelCliDebuggerFactory.java @@ -0,0 +1,127 @@ +/* + * 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.component.cli.debug; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.camel.CamelContext; +import org.apache.camel.impl.debugger.DefaultBacklogDebugger; +import org.apache.camel.spi.BacklogDebugger; +import org.apache.camel.spi.CliConnector; +import org.apache.camel.spi.Debugger; +import org.apache.camel.spi.DebuggerFactory; +import org.apache.camel.spi.annotations.JdkService; +import org.apache.camel.support.LifecycleStrategySupport; +import org.apache.camel.util.StopWatch; +import org.apache.camel.util.concurrent.ThreadHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@JdkService(Debugger.FACTORY) +public class CamelCliDebuggerFactory implements DebuggerFactory { + + private static final Logger LOG = LoggerFactory.getLogger(CamelCliDebuggerFactory.class); + + private final AtomicBoolean hangupIntercepted = new AtomicBoolean(); + + @Override + // Debugger is created and added as a service. This method always returns a null object. + public Debugger createDebugger(CamelContext camelContext) throws Exception { + // only create a debugger if none already exists + if (camelContext.hasService(BacklogDebugger.class) == null) { + + // NOTE: the AutoCloseable object is added as a Service, hence it is closed by Camel context + // according to the object lifecycle. + BacklogDebugger backlog = DefaultBacklogDebugger.createDebugger(camelContext); // NOSONAR + backlog.setStandby(true); + backlog.setLoggingLevel("DEBUG"); + backlog.setSingleStepIncludeStartEnd(true); + backlog.setInitialBreakpoints(BacklogDebugger.BREAKPOINT_ALL_ROUTES); + backlog.setSuspendMode(true); // wait for attach via CLI + + // must enable source location and history + // so debugger tooling knows to map breakpoints to source code + camelContext.setSourceLocationEnabled(true); + camelContext.setMessageHistory(true); + + // enable debugger on camel + camelContext.setDebugging(true); + + // we need to enable debugger after context is started + camelContext.addLifecycleStrategy(new LifecycleStrategySupport() { + @Override + public void onContextStarted(CamelContext context) { + // noop + } + + @Override + public void onContextStopping(CamelContext context) { + backlog.detach(); + backlog.disableDebugger(); + } + }); + camelContext.addService(backlog, true, true); + + // need to make debugger faster + CliConnector cli = camelContext.hasService(CliConnector.class); + if (cli != null) { + cli.updateDelay(100); + } + + installHangupInterceptor(); + + long pid = ProcessHandle.current().pid(); + + LOG.info("=".repeat(80)); + LOG.info("Waiting for CLI to remote attach (camel debug --remote-attach --name={})", pid); + StopWatch watch = new StopWatch(); + while (!hangupIntercepted.get() && !backlog.isEnabled() && !camelContext.isStopping()) { + try { + Thread.sleep(1000); + if (watch.taken() > 5000) { + LOG.info("Waiting for CLI to remote attach (camel debug --remote-attach --name={})", pid); + watch.restart(); + } + } catch (InterruptedException e) { + return null; + } + } + if (backlog.isEnabled()) { + LOG.info("CLI debugger attached"); + } + } + + // return null as we fool camel-core into using this backlog debugger as we added it as a service + return null; + } + + @Override + public String toString() { + return "camel-cli-debug"; + } + + private void handleHangup() { + hangupIntercepted.set(true); + } + + private void installHangupInterceptor() { + Thread task = new Thread(this::handleHangup); + task.setName(ThreadHelper.resolveThreadName(null, "CamelCliDebuggerHangupInterceptor")); + Runtime.getRuntime().addShutdownHook(task); + } + +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java index c2bf8cbac795..8aac1e82db4c 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java @@ -42,8 +42,10 @@ import org.apache.camel.dsl.jbang.core.commands.action.MessageTableHelper; import org.apache.camel.dsl.jbang.core.common.CamelCommandHelper; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; import org.apache.camel.dsl.jbang.core.common.PathUtils; +import org.apache.camel.dsl.jbang.core.common.ProcessHelper; import org.apache.camel.dsl.jbang.core.common.VersionHelper; import org.apache.camel.main.KameletMain; +import org.apache.camel.support.PatternHelper; import org.apache.camel.util.FileUtil; import org.apache.camel.util.IOHelper; import org.apache.camel.util.StringHelper; @@ -74,6 +76,10 @@ import static org.apache.camel.util.IOHelper.buffered; @Command(name = "debug", description = "Debug local Camel integration", sortOptions = false, showDefaultValues = true) public class Debug extends Run { + @CommandLine.Option(names = { "--remote-attach" }, + description = "Attaches debugger remotely to an existing running Camel integration. (Add camel-cli-debug JAR to the existing Camel application and run before attaching this debugger)") + boolean remoteAttach; + @CommandLine.Option(names = { "--breakpoint" }, description = "To set breakpoint at the given node id (Multiple ids can be separated by comma). If no breakpoint is set, then the first route is automatic selected.") String breakpoint; @@ -151,7 +157,12 @@ public class Debug extends Run { printConfigurationValues("Debugging integration with the following configuration:"); } - Integer exit = runDebug(); + Integer exit; + if (remoteAttach) { + exit = runRemoteAttach(); + } else { + exit = runDebug(); + } if (exit != null && exit != 0) { return exit; } @@ -170,10 +181,12 @@ public class Debug extends Run { // read log input final AtomicBoolean quit = new AtomicBoolean(); final Console c = System.console(); - Thread t = new Thread(() -> { - doReadLog(quit); - }, "ReadLog"); - t.start(); + if (logLines > 0) { + Thread t = new Thread(() -> { + doReadLog(quit); + }, "ReadLog"); + t.start(); + } // read CLI input from user Thread t2 = new Thread(() -> doRead(c, quit), "ReadCommand"); @@ -205,6 +218,41 @@ public class Debug extends Run { return 0; } + private Integer runRemoteAttach() { + // find PID of running apps (if there are multiple then filter by name) + List<Long> pids = findPids(name); + if (pids.isEmpty()) { + return -1; + } else if (pids.size() > 1) { + printer().println("Name or pid " + name + " matches " + pids.size() + + " running Camel integrations. Specify a name or PID that matches exactly one."); + return -1; + } + long pid = pids.get(0); + + Path outputFile = getOutputFile(Long.toString(pid)); + PathUtils.deleteFile(outputFile); + + try { + JsonObject root = new JsonObject(); + root.put("action", "cli-debug"); + root.put("command", "attach"); + if (breakpoint != null) { + root.put("breakpoint", breakpoint); + } + Path f = getActionFile(Long.toString(pid)); + Files.writeString(f, root.toJson()); + } catch (Exception e) { + return -1; + } + + // attach to this pid + spawnPid = pid; + logLines = 0; // no logging possible + + return 0; + } + private void doReadLog(AtomicBoolean quit) { do { if (spawnOutput != null) { @@ -1209,4 +1257,65 @@ public class Debug extends Run { } + List<Long> findPids(String name) { + List<Long> pids = new ArrayList<>(); + + // we need to know the pids of the running camel integrations + if (name.matches("\\d+")) { + return List.of(Long.parseLong(name)); + } else { + if (name.endsWith("!")) { + // exclusive this name only + name = name.substring(0, name.length() - 1); + } else if (!name.endsWith("*")) { + // lets be open and match all that starts with this pattern + name = name + "*"; + } + } + + final long cur = ProcessHandle.current().pid(); + final String pattern = name; + ProcessHandle.allProcesses() + .filter(ph -> ph.pid() != cur) + .forEach(ph -> { + JsonObject root = loadStatus(ph.pid()); + // there must be a status file for the running Camel integration + if (root != null) { + String pName = ProcessHelper.extractName(root, ph); + // ignore file extension, so it is easier to match by name + pName = FileUtil.onlyName(pName); + if (pName != null && !pName.isEmpty() && PatternHelper.matchPattern(pName, pattern)) { + pids.add(ph.pid()); + } else { + // try camel context name + JsonObject context = (JsonObject) root.get("context"); + if (context != null) { + pName = context.getString("name"); + if ("CamelJBang".equals(pName)) { + pName = null; + } + if (pName != null && !pName.isEmpty() && PatternHelper.matchPattern(pName, pattern)) { + pids.add(ph.pid()); + } + } + } + } + }); + + return pids; + } + + JsonObject loadStatus(long pid) { + try { + Path f = getStatusFile(Long.toString(pid)); + if (f != null && Files.exists(f)) { + String text = Files.readString(f); + return (JsonObject) Jsoner.deserialize(text); + } + } catch (Exception e) { + // ignore + } + return null; + } + } diff --git a/dsl/pom.xml b/dsl/pom.xml index a3188d172db7..2089278bba19 100644 --- a/dsl/pom.xml +++ b/dsl/pom.xml @@ -42,6 +42,7 @@ <module>camel-endpointdsl</module> <module>camel-componentdsl</module> <module>camel-cli-connector</module> + <module>camel-cli-debug</module> <module>camel-dsl-support</module> <module>camel-dsl-modeline</module> <module>camel-endpointdsl-support</module> diff --git a/parent/pom.xml b/parent/pom.xml index e672c43b33e9..896e389ce8ba 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -2912,6 +2912,11 @@ <artifactId>camel-cli-connector</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-cli-debug</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-componentdsl</artifactId>
