This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 5186b4b03f5b CAMEL-25391: camel-jbang - ActionClient, ProcessFinder
and shared --timeout (#27914)
5186b4b03f5b is described below
commit 5186b4b03f5b7c8024d64f351b33ce41b9be7390
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 16 01:29:34 2026 +0200
CAMEL-25391: camel-jbang - ActionClient, ProcessFinder and shared --timeout
(#27914)
One ProcessFinder finds the running integrations by name or pid (a prefix
unless the name ends with !, wildcards or a regular expression), and
requireOne/requireAny exit with 3 when nothing matches. A pid must be a
running Camel integration, and an integration whose Camel has stopped is
skipped by every command.
One ActionClient talks to them: a file per request from Camel 4.21, the
shared action file before, with the results OK, ERROR (and not-found),
TIMEOUT and UNCONFIRMED as the exit codes 0, 1, 3 and 124. --timeout takes
millis or a duration such as 30s.
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 11 +
.../modules/ROOT/pages/camel-jbang-managing.adoc | 4 +-
.../jbang-commands/camel-jbang-cmd-receive.adoc | 2 +-
.../pages/jbang-commands/camel-jbang-cmd-send.adoc | 2 +-
.../camel-jbang-eval-expression.adoc | 2 +-
.../jbang-commands/camel-jbang-semantic-audit.adoc | 2 +-
.../jbang-commands/camel-jbang-semantic-eval.adoc | 2 +-
.../jbang-commands/camel-jbang-semantic-get.adoc | 2 +-
.../camel-jbang-semantic-monitoring.adoc | 2 +-
.../pages/jbang-commands/camel-jbang-semantic.adoc | 2 +-
.../camel-jbang-transform-message.adoc | 2 +-
.../META-INF/camel-jbang-commands-metadata.json | 8 +-
.../camel/dsl/jbang/core/commands/Debug.java | 52 +---
.../core/commands/action/ActionBaseCommand.java | 58 +---
.../core/commands/action/CamelReceiveAction.java | 5 +-
.../core/commands/action/CamelSendAction.java | 6 +-
.../commands/action/EvalExpressionCommand.java | 5 +-
.../commands/action/SemanticActionCommand.java | 5 +-
.../commands/action/TransformMessageAction.java | 5 +-
.../core/commands/process/ProcessBaseCommand.java | 52 +---
.../camel/dsl/jbang/core/common/ActionClient.java | 327 +++++++++++++++++++++
.../camel/dsl/jbang/core/common/ProcessFinder.java | 226 ++++++++++++++
.../camel/dsl/jbang/core/common/RuntimeHelper.java | 123 +-------
.../dsl/jbang/core/common/TimeoutConverter.java | 37 +++
.../dsl/jbang/core/common/ActionClientTest.java | 238 +++++++++++++++
.../dsl/jbang/core/common/ProcessFinderTest.java | 207 +++++++++++++
.../jbang/core/common/TimeoutConverterTest.java | 47 +++
27 files changed, 1151 insertions(+), 283 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 7d4edff7dd70..f58c0870708a 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -196,6 +196,17 @@ model better at tool calling, such as `qwen3.6`, is
installed.
`bean`). Scripts that read the `REMOTE` column of the table must read `SCOPE`
instead. The `--json` output keeps
`remote` and adds `internal`.
+=== camel-jbang - finding integrations by name or PID
+
+All commands find running integrations the same way:
+
+* A PID given to a command must be a running Camel integration (a live process
with a status file in `~/.camel`).
+ Before, any PID was used as is, so `camel stop 1234 --kill` killed whatever
process had that PID.
+* The commands no longer find an integration whose Camel has stopped (phase 9
or later), such as the launcher process
+ of an integration that runs in a child process. `camel ps` does not list it
anymore.
+* The `--timeout` of `camel cmd send`, `camel cmd receive`, `camel eval
expression`, `camel transform message` and the
+ `camel semantic` commands also takes a duration such as `30s` (a number is
still millis).
+
=== camel-jbang - exit codes, and errors on stderr
The Camel CLI has a documented set of exit codes (see
xref:camel-jbang-exit-codes.adoc[Exit Codes and Output]):
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
index bd7ccaabf7a5..2f19964de767 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
@@ -31,7 +31,9 @@ Stopping running Camel integration (pid: 62506)
----
NOTE: The name is matched as a prefix — `camel stop d` stops all integrations
starting with _d_.
-To stop exactly one when names overlap (e.g., `dude` and `dude2`), use the PID
or append `!`: `camel stop dude!`
+To stop exactly one when names overlap (e.g., `dude` and `dude2`), use the PID
or append `!`: `camel stop dude!`.
+A PID must be a running Camel integration, and a name or PID that matches
nothing exits with code `3`
+(see xref:camel-jbang-exit-codes.adoc[Exit Codes and Output]).
Restart an integration (stop + re-launch):
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-receive.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-receive.adoc
index 10ead1002ddc..fff76b9c622f 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-receive.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-receive.adoc
@@ -44,7 +44,7 @@ camel cmd receive [options]
| `--since` | Return messages newer than a relative duration like 5s, 2m, or
1h. The value is in seconds if no unit specified. | | String
| `--sort` | Sort by pid, name or age for showing status of messages | pid |
String
| `--tail` | The number of messages from the end to show. Use -1 to read from
the beginning. Use 0 to read only new lines. Defaults to showing all messages
from beginning. | -1 | int
-| `--timeout` | Timeout in millis waiting for message to be received | 20000 |
long
+| `--timeout` | Timeout waiting for message to be received, in millis or as a
duration such as 30s | 20000 | long
| `--wide-uri` | List endpoint URI in full details | | boolean
| `-h,--help` | Display the help and sub-commands | | boolean
|===
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-send.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-send.adoc
index b509f055f75a..517c948ef3fa 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-send.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-send.adoc
@@ -38,7 +38,7 @@ camel cmd send [options]
| `--show-exchange-properties` | Show exchange properties from response
message (InOut) | false | boolean
| `--show-exchange-variables` | Show exchange variables from response message
(InOut) | false | boolean
| `--show-headers` | Show message headers from response message (InOut) | true
| boolean
-| `--timeout` | Timeout in millis waiting for message to be sent (and reply
message if InOut messaging) | 20000 | long
+| `--timeout` | Timeout waiting for message to be sent (and reply message if
InOut messaging), in millis or as a duration such as 30s | 20000 | long
| `-h,--help` | Display the help and sub-commands | | boolean
|===
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval-expression.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval-expression.adoc
index 258ebdad7dc5..a902876befe1 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval-expression.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval-expression.adoc
@@ -29,7 +29,7 @@ camel eval expression [options]
| `--predicate` | Whether to force evaluating as predicate | false | boolean
| `--repo,--repos` | Additional maven repositories for download on-demand (Use
commas to separate multiple repositories) | | String
| `--template` | The template to use for evaluating (prefix with file: to
refer to loading template from file) | | String
-| `--timeout` | Timeout in millis waiting for evaluation to be done | 10000 |
long
+| `--timeout` | Timeout waiting for evaluation to be done, in millis or as a
duration such as 30s | 10000 | long
| `--variable` | Exchange variable (key=value) | | List
| `--watch` | Execute periodically and showing output fullscreen | | boolean
| `-h,--help` | Display the help and sub-commands | | boolean
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-audit.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-audit.adoc
index 58188160ae3c..0397c8f7062d 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-audit.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-audit.adoc
@@ -42,7 +42,7 @@ camel semantic audit [options]
| `--score-max` | Inclusive numeric upper bound; requires --result-type=SCORE
| | String
| `--score-min` | Inclusive numeric lower bound; requires --result-type=SCORE
| | String
| `--since` | Earliest timestamp, inclusive, in ISO-8601 format with a UTC
offset | | String
-| `--timeout` | Timeout in milliseconds waiting for the integration | 60000 |
long
+| `--timeout` | Timeout waiting for the integration, in millis or as a
duration such as 30s | 60000 | long
| `--until` | Latest timestamp, exclusive, in ISO-8601 format with a UTC
offset | | String
| `-h,--help` | Display the help and sub-commands | | boolean
|===
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-eval.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-eval.adoc
index 4415aef5c4bf..9799ec5662d0 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-eval.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-eval.adoc
@@ -28,7 +28,7 @@ camel semantic eval [options]
| `--json` | Output a single JSON document | | boolean
| `--operation` | Expert operation to call directly | | String
| `--parameter` | Expert parameter (key=value; use json: for a typed value) |
| List
-| `--timeout` | Timeout in milliseconds waiting for the integration | 60000 |
long
+| `--timeout` | Timeout waiting for the integration, in millis or as a
duration such as 30s | 60000 | long
| `--variable` | Sample exchange-local variable (key=value; use json: for a
typed value) | | List
| `-h,--help` | Display the help and sub-commands | | boolean
|===
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-get.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-get.adoc
index 78378a9f5756..d7c647008f87 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-get.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-get.adoc
@@ -21,7 +21,7 @@ camel semantic get [options]
| Option | Description | Default | Type
| `--expert` | Show the operations and parameter contract of this expert | |
String
| `--json` | Output a single JSON document | | boolean
-| `--timeout` | Timeout in milliseconds waiting for the integration | 60000 |
long
+| `--timeout` | Timeout waiting for the integration, in millis or as a
duration such as 30s | 60000 | long
| `-h,--help` | Display the help and sub-commands | | boolean
|===
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-monitoring.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-monitoring.adoc
index 7f54c47638f7..e0601985ef3f 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-monitoring.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-monitoring.adoc
@@ -21,7 +21,7 @@ camel semantic monitoring [options]
| Option | Description | Default | Type
| `--json` | Output a single JSON document | | boolean
| `--source` | Producer or configured metric source name | | String
-| `--timeout` | Timeout in milliseconds waiting for the integration | 60000 |
long
+| `--timeout` | Timeout waiting for the integration, in millis or as a
duration such as 30s | 60000 | long
| `-h,--help` | Display the help and sub-commands | | boolean
|===
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic.adoc
index 0ec56653637b..eadfc4c6c020 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic.adoc
@@ -33,7 +33,7 @@ camel semantic [options]
| Option | Description | Default | Type
| `--expert` | Show the operations and parameter contract of this expert | |
String
| `--json` | Output a single JSON document | | boolean
-| `--timeout` | Timeout in milliseconds waiting for the integration | 60000 |
long
+| `--timeout` | Timeout waiting for the integration, in millis or as a
duration such as 30s | 60000 | long
| `-h,--help` | Display the help and sub-commands | | boolean
|===
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
index 634ac9b8a5a1..3c53c970aae7 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
@@ -38,7 +38,7 @@ camel transform message [options]
| `--show-headers` | Show message headers from the output message | true |
boolean
| `--source` | Instead of using external template file then refer to an
existing Camel route source with inlined Camel language expression in a route.
(use :line-number or :id to refer to the exact location of the EIP to use) | |
String
| `--template` | The template to use for message transformation (prefix with
file: to refer to loading template from file) | | String
-| `--timeout` | Timeout in millis waiting for message to be transformed |
20000 | long
+| `--timeout` | Timeout waiting for message to be transformed, in millis or as
a duration such as 30s | 20000 | long
| `--watch` | Execute periodically and showing output fullscreen | | boolean
| `-h,--help` | Display the help and sub-commands | | boolean
|===
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index c9b2b774adfd..6363d0efb127 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -3,7 +3,7 @@
{ "name": "ask", "fullName": "ask", "description": "Ask a question about a
running Camel application using AI", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names":
"--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY,
OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars", "javaType":
"java.lang.String", "type": "string" }, { "names": "--api-type", "description":
"API type: 'ollama', 'openai', 'anthropic', or 'watsonx'", "javaType": [...]
{ "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind
source and sink Kamelets as a new Camel integration", "deprecated": true,
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options":
[ { "names": "--error-handler", "description": "Add error handler
(none|log|sink:<endpoint>). Sink endpoints are expected in the format
[[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet
name.", "javaType": "java.lang.String", "type": "stri [...]
{ "name": "catalog", "fullName": "catalog", "description": "List artifacts
from Camel Catalog", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [
{ "names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"component", "fullName": "catalog component", "description": "List components
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
- { "name": "cmd", "fullName": "cmd", "description": "Performs commands in
the running Camel integrations, such as start\/stop route, or change logging
levels.", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"browse", "fullName": "cmd browse", "description": "Browse pending messages on
endpoints [...]
+ { "name": "cmd", "fullName": "cmd", "description": "Performs commands in
the running Camel integrations, such as start\/stop route, or change logging
levels.", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"browse", "fullName": "cmd browse", "description": "Browse pending messages on
endpoints [...]
{ "name": "completion", "fullName": "completion", "description": "Generate
completion script for bash\/zsh", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ] },
{ "name": "config", "fullName": "config", "description": "Get and set user
configuration values", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get",
"fullName": "config get", "description": "Display user configuration value",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
{ "name": "debug", "fullName": "debug", "description": "Debug local Camel
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug",
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names":
"--background", "description": "Run in the background", "defaultValue":
"false", "javaType": "boolean", "type": "boolean" }, { "names":
"--background-wait", "description": "To [...]
@@ -12,7 +12,7 @@
{ "name": "dirty", "fullName": "dirty", "description": "Check if there are
dirty files from previous Camel runs that did not terminate gracefully",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.Dirty",
"options": [ { "names": "--clean", "description": "Clean dirty files which are
no longer in use", "defaultValue": "false", "javaType": "boolean", "type":
"boolean" }, { "names": "-h,--help", "description": "Display the help and
sub-commands", "javaType": "boolean", " [...]
{ "name": "doc", "fullName": "doc", "description": "Shows documentation
for kamelet, component, and other Camel resources", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogDoc", "options": [ {
"names": "--camel-version", "description": "To use a different Camel version
than the default version", "javaType": "java.lang.String", "type": "string" },
{ "names": "--download", "description": "Whether to allow automatic downloading
JAR dependencies (over the internet [...]
{ "name": "doctor", "fullName": "doctor", "description": "Checks the
environment and reports potential issues", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Doctor", "options": [ { "names":
"--json", "description": "Output in JSON Format: the status (OK, WARN or FAIL)
of each check", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ] },
- { "name": "eval", "fullName": "eval", "description": "Evaluate Camel
expressions and scripts", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.EvalCommand", "options": [ { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "expression",
"fullName": "eval expression", "description": "Evaluates Camel expression",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.EvalEx [...]
+ { "name": "eval", "fullName": "eval", "description": "Evaluate Camel
expressions and scripts", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.EvalCommand", "options": [ { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "expression",
"fullName": "eval expression", "description": "Evaluates Camel expression",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.EvalEx [...]
{ "name": "explain", "fullName": "explain", "description": "Explain what a
Camel route does using AI\/LLM", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Explain", "options": [ { "names":
"--api-key", "description": "API key for authentication. Also reads
ANTHROPIC_API_KEY, OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars",
"javaType": "java.lang.String", "type": "string" }, { "names": "--api-type",
"description": "API type: 'ollama', 'openai' (OpenAI-compatible), ' [...]
{ "name": "export", "fullName": "export", "description": "Export to other
runtimes (Camel Main, Spring Boot, or Quarkus)", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Export", "options": [ { "names":
"--build-property", "description": "Maven build properties, ex.
--build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, {
"names": "--camel-spring-boot-version", "description": "Camel version to use
with Spring Boot", "javaType": "java.lang.String", "ty [...]
{ "name": "get", "fullName": "get", "description": "Get status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "activity",
"fullName": " [...]
@@ -30,12 +30,12 @@
{ "name": "run", "fullName": "run", "description": "Run as local Camel
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Run",
"options": [ { "names": "--background", "description": "Run in the background",
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names":
"--background-wait", "description": "To wait for run in background to startup
successfully, before returning", "defaultValue": "true", "javaType": "boolean",
"type": "boolean" }, { [...]
{ "name": "sbom", "fullName": "sbom", "description": "Generate a CycloneDX
or SPDX SBOM for a specific project", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.SBOMGenerator", "options": [ {
"names": "--build-property", "description": "Maven build properties, ex.
--build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, {
"names": "--camel-spring-boot-version", "description": "Camel version to use
with Spring Boot", "javaType": "java.lang.String", "type" [...]
{ "name": "script", "fullName": "script", "description": "Run Camel
integration as shell script for terminal scripting", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Script", "options": [ { "names":
"--logging", "description": "Can be used to turn on logging (logs to file in
<user home>\/.camel directory)", "defaultValue": "false", "javaType":
"boolean", "type": "boolean" }, { "names": "--logging-level", "description":
"Logging level (ERROR, WARN, INFO, DEBUG, TRACE)", "d [...]
- { "name": "semantic", "fullName": "semantic", "description": "List
semantic definitions and expert contracts", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelSemantic", "options": [ {
"names": "--expert", "description": "Show the operations and parameter contract
of this expert", "javaType": "java.lang.String", "type": "string" }, { "names":
"--json", "description": "Output a single JSON document", "javaType":
"boolean", "type": "boolean" }, { "names": "--timeou [...]
+ { "name": "semantic", "fullName": "semantic", "description": "List
semantic definitions and expert contracts", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelSemantic", "options": [ {
"names": "--expert", "description": "Show the operations and parameter contract
of this expert", "javaType": "java.lang.String", "type": "string" }, { "names":
"--json", "description": "Output a single JSON document", "javaType":
"boolean", "type": "boolean" }, { "names": "--timeou [...]
{ "name": "shell", "fullName": "shell", "description": "Interactive Camel
CLI shell.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Shell",
"options": [ { "names": "-h,--help", "description": "Display the help and
sub-commands", "javaType": "boolean", "type": "boolean" } ] },
{ "name": "stop", "fullName": "stop", "description": "Shuts down running
Camel integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.StopProcess", "options": [ {
"names": "--kill", "description": "To force killing the process (SIGKILL)",
"javaType": "boolean", "type": "boolean" }, { "names": "-h,--help",
"description": "Display the help and sub-commands", "javaType": "boolean",
"type": "boolean" } ] },
{ "name": "top", "fullName": "top", "description": "Top status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelTop", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "context",
"fullName": "top [...]
{ "name": "trace", "fullName": "trace", "description": "Tail message
traces from running Camel integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelTraceAction", "options":
[ { "names": "--action", "description": "Action to start, stop, clear, list
status, or dump traces", "defaultValue": "status", "javaType":
"java.lang.String", "type": "string" }, { "names": "--ago", "description": "Use
ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "b [...]
- { "name": "transform", "fullName": "transform", "description": "Transform
message or Camel routes", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.TransformCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"dataweave", "fullName": "transform dataweave", "description": "Convert
DataWeave scripts to DataSonnet format", "sourceClass": "org.apache.camel.dsl.
[...]
+ { "name": "transform", "fullName": "transform", "description": "Transform
message or Camel routes", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.TransformCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"dataweave", "fullName": "transform dataweave", "description": "Convert
DataWeave scripts to DataSonnet format", "sourceClass": "org.apache.camel.dsl.
[...]
{ "name": "update", "fullName": "update", "description": "Update Camel
project", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.update.UpdateCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"list", "fullName": "update list", "description": "List available update
versions for Camel and its runtime variants", "sourceClass":
"org.apache.camel.dsl.jbang.cor [...]
{ "name": "version", "fullName": "version", "description": "Manage Camel
versions", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.version.VersionCommand", "options": [
{ "names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get",
"fullName": "version get", "description": "Displays current Camel version",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.version.VersionGet",
[...]
{ "name": "wrapper", "fullName": "wrapper", "description": "Install Camel
wrapper scripts for version pinning", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.WrapperCommand", "options": [ {
"names": "--camel-version", "description": "Camel version to pin (defaults to
current version)", "javaType": "java.lang.String", "type": "string" }, {
"names": "--command-name", "description": "The name of the command to use
(camel or camelw)", "defaultValue": "camel", "javaType": "java [...]
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 6a19c526c15f..e24240454e8b 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
@@ -36,6 +36,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
import org.apache.camel.dsl.jbang.core.commands.action.MessageTableHelper;
import org.apache.camel.dsl.jbang.core.common.CamelCommandHelper;
@@ -43,11 +44,10 @@ import org.apache.camel.dsl.jbang.core.common.CliExitCode;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
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.ProcessFinder;
import org.apache.camel.dsl.jbang.core.common.VersionHelper;
import org.apache.camel.main.KameletMain;
import org.apache.camel.support.LoggerHelper;
-import org.apache.camel.support.PatternHelper;
import org.apache.camel.tooling.maven.MavenGav;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.IOHelper;
@@ -1301,51 +1301,9 @@ 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;
+ // the running integrations that match the name or pid (see
ProcessFinder)
+ return
ProcessFinder.find(name).stream().map(ProcessFinder.CamelProcess::pid)
+ .collect(Collectors.toCollection(ArrayList::new));
}
JsonObject loadStatus(long pid) {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
index 8adf0526fccd..a6384a160d57 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
@@ -24,6 +24,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
@@ -33,10 +34,8 @@ import
org.apache.camel.dsl.jbang.core.common.CamelJBangConstants;
import org.apache.camel.dsl.jbang.core.common.CliException;
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.ProcessFinder;
import org.apache.camel.main.KameletMain;
-import org.apache.camel.support.PatternHelper;
-import org.apache.camel.util.FileUtil;
import org.apache.camel.util.StopWatch;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
@@ -78,56 +77,9 @@ abstract class ActionBaseCommand extends CamelCommand {
}
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) {
- // skip terminated processes (e.g. launcher process
for spring-boot/quarkus runtimes)
- JsonObject context = (JsonObject) root.get("context");
- if (context != null) {
- int phase = context.getIntegerOrDefault("phase",
0);
- if (phase >= 9) {
- return;
- }
- }
- 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 if (context != null) {
- // try camel context name
- 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;
+ // the running integrations that match the name or pid (see
ProcessFinder)
+ return
ProcessFinder.find(name).stream().map(ProcessFinder.CamelProcess::pid)
+ .collect(Collectors.toCollection(ArrayList::new));
}
static long extractSince(ProcessHandle ph) {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveAction.java
index 1207c5f8c422..8fbca6d57d2e 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveAction.java
@@ -50,6 +50,7 @@ import org.apache.camel.dsl.jbang.core.common.PathUtils;
import org.apache.camel.dsl.jbang.core.common.PidNameAgeCompletionCandidates;
import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
+import org.apache.camel.dsl.jbang.core.common.TimeoutConverter;
import org.apache.camel.main.KameletMain;
import org.apache.camel.util.StopWatch;
import org.apache.camel.util.StringHelper;
@@ -162,8 +163,8 @@ public class CamelReceiveAction extends ActionBaseCommand {
description = "Filter messages to only output matching
text (ignore case).", arity = "0..*")
String[] grep;
- @CommandLine.Option(names = { "--timeout" }, defaultValue = "20000",
- description = "Timeout in millis waiting for message
to be received")
+ @CommandLine.Option(names = { "--timeout" }, defaultValue = "20000",
converter = TimeoutConverter.class,
+ description = "Timeout waiting for message to be
received, in millis or as a duration such as 30s")
long timeout = 20000;
@CommandLine.Option(names = { "--show-exchange-properties" }, defaultValue
= "false",
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
index 304799dade23..7a6a29ab6d2e 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
@@ -40,6 +40,7 @@ import
org.apache.camel.dsl.jbang.core.commands.MavenResolverMixin;
import org.apache.camel.dsl.jbang.core.commands.Run;
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.TimeoutConverter;
import org.apache.camel.main.KameletMain;
import org.apache.camel.util.StringHelper;
import org.apache.camel.util.TimeUtils;
@@ -97,8 +98,9 @@ public class CamelSendAction extends ActionBaseCommand {
description = "Message header (key=value)")
List<String> headers;
- @CommandLine.Option(names = { "--timeout" }, defaultValue = "20000",
- description = "Timeout in millis waiting for message
to be sent (and reply message if InOut messaging)")
+ @CommandLine.Option(names = { "--timeout" }, defaultValue = "20000",
converter = TimeoutConverter.class,
+ description = "Timeout waiting for message to be sent
(and reply message if InOut messaging),"
+ + " in millis or as a duration such as
30s")
long timeout = 20000;
@CommandLine.Option(names = { "--show-exchange-properties" }, defaultValue
= "false",
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
index aef385b7b12e..725ac0835a25 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
@@ -27,6 +27,7 @@ import org.apache.camel.dsl.jbang.core.commands.Run;
import org.apache.camel.dsl.jbang.core.common.CliExitCode;
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.TimeoutConverter;
import org.apache.camel.dsl.jbang.core.common.VersionHelper;
import org.apache.camel.util.StopWatch;
import org.apache.camel.util.StringHelper;
@@ -81,8 +82,8 @@ public class EvalExpressionCommand extends ActionWatchCommand
{
description = "Exchange variable (key=value)")
List<String> variables;
- @CommandLine.Option(names = { "--timeout" }, defaultValue = "10000",
- description = "Timeout in millis waiting for
evaluation to be done")
+ @CommandLine.Option(names = { "--timeout" }, defaultValue = "10000",
converter = TimeoutConverter.class,
+ description = "Timeout waiting for evaluation to be
done, in millis or as a duration such as 30s")
long timeout = 10000;
@CommandLine.Mixin
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java
index 3bf620773181..2226234abb56 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java
@@ -26,6 +26,7 @@ import
org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import
org.apache.camel.dsl.jbang.core.commands.exceptionhandler.UsageErrorHandler;
import org.apache.camel.dsl.jbang.core.common.CliExitCode;
import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
+import org.apache.camel.dsl.jbang.core.common.TimeoutConverter;
import org.apache.camel.dsl.jbang.core.common.VersionHelper;
import org.apache.camel.util.json.JsonObject;
import picocli.CommandLine;
@@ -38,8 +39,8 @@ public abstract class SemanticActionCommand extends
ActionBaseCommand implements
@CommandLine.Option(names = "--json", description = "Output a single JSON
document")
boolean json;
- @CommandLine.Option(names = "--timeout", defaultValue = "60000",
- description = "Timeout in milliseconds waiting for the
integration")
+ @CommandLine.Option(names = "--timeout", defaultValue = "60000", converter
= TimeoutConverter.class,
+ description = "Timeout waiting for the integration, in
millis or as a duration such as 30s")
long timeout = 60000;
@CommandLine.Spec
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
index 8b03e1c1dd37..1c683a0a6628 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
@@ -28,6 +28,7 @@ import org.apache.camel.dsl.jbang.core.commands.Run;
import org.apache.camel.dsl.jbang.core.common.CliExitCode;
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.TimeoutConverter;
import org.apache.camel.dsl.jbang.core.common.VersionHelper;
import org.apache.camel.util.StopWatch;
import org.apache.camel.util.StringHelper;
@@ -109,8 +110,8 @@ public class TransformMessageAction extends
ActionWatchCommand {
description = "Show exception and stacktrace for
failed transformation")
boolean showException = true;
- @CommandLine.Option(names = { "--timeout" }, defaultValue = "20000",
- description = "Timeout in millis waiting for message
to be transformed")
+ @CommandLine.Option(names = { "--timeout" }, defaultValue = "20000",
converter = TimeoutConverter.class,
+ description = "Timeout waiting for message to be
transformed, in millis or as a duration such as 30s")
long timeout = 20000;
@CommandLine.Option(names = { "--logging-color" }, defaultValue = "true",
description = "Use colored logging")
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java
index b2176c9bf49e..e40587e8cfaf 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java
@@ -20,11 +20,11 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
+import java.util.stream.Collectors;
import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
-import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
-import org.apache.camel.support.PatternHelper;
+import org.apache.camel.dsl.jbang.core.common.ProcessFinder;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.StringHelper;
import org.apache.camel.util.json.JsonObject;
@@ -37,51 +37,9 @@ abstract class ProcessBaseCommand extends CamelCommand {
}
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;
+ // the running integrations that match the name or pid (see
ProcessFinder)
+ return
ProcessFinder.find(name).stream().map(ProcessFinder.CamelProcess::pid)
+ .collect(Collectors.toCollection(ArrayList::new));
}
public static long extractSince(ProcessHandle ph) {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ActionClient.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ActionClient.java
new file mode 100644
index 000000000000..d223a23e2485
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ActionClient.java
@@ -0,0 +1,327 @@
+/*
+ * 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.common;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.UUID;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess;
+import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * Sends actions to a running Camel integration and waits for its answer (see
camel-cli-connector).
+ * <p/>
+ * An integration on Camel 4.21 or later takes a file per request
(<tt>~/.camel/{pid}-action-{id}.json</tt>, answered in
+ * <tt>{pid}-output-{id}.json</tt>), so clients never take each other's
answers; an older integration only reads the
+ * shared <tt>{pid}-action.json</tt>. An integration on Camel 4.24 or later
answers every action, also one without a
+ * result, so no answer is a timeout; an older integration does not answer an
action without a result, which is then
+ * sent but not confirmed.
+ */
+public final class ActionClient {
+
+ /**
+ * How long to wait for an answer by default, in millis.
+ */
+ public static final long DEFAULT_TIMEOUT = 10_000;
+
+ private static final long POLL_INTERVAL = 100;
+ private static final Pattern VERSION = Pattern.compile("^(\\d+)\\.(\\d+)");
+
+ /**
+ * How an action went.
+ */
+ public enum Status {
+ /**
+ * The integration answered with a result, or confirmed the action.
+ */
+ OK,
+ /**
+ * The integration answered that the action failed.
+ */
+ ERROR,
+ /**
+ * The integration did not answer in time.
+ */
+ TIMEOUT,
+ /**
+ * The integration took the action, but it is too old (before Camel
4.24) to confirm an action without a result.
+ */
+ UNCONFIRMED
+ }
+
+ /**
+ * The answer of an integration to an action.
+ *
+ * @param status how the action went
+ * @param reply the answer of the integration, or <tt>null</tt> when
there is none
+ * @param error why the action failed, or did not get an answer;
<tt>null</tt> when it went well
+ * @param reason the kind of error, such as <tt>not-found</tt>, or
<tt>null</tt>
+ */
+ public record ActionResult(Status status, JsonObject reply, String error,
String reason) {
+
+ /**
+ * Whether the action went well (also when an older integration took
it without confirming it).
+ */
+ public boolean isOk() {
+ return status == Status.OK || status == Status.UNCONFIRMED;
+ }
+
+ /**
+ * Whether the action failed because what it was asked to work on does
not exist.
+ */
+ public boolean isNotFound() {
+ return status == Status.ERROR && "not-found".equals(reason);
+ }
+
+ /**
+ * The exit code of a command that ends with this result, see {@link
CliExitCode}.
+ */
+ public int exitCode() {
+ return switch (status) {
+ case OK, UNCONFIRMED -> CliExitCode.OK;
+ case ERROR -> isNotFound() ? CliExitCode.NOT_FOUND :
CliExitCode.FAILURE;
+ case TIMEOUT -> CliExitCode.TIMEOUT;
+ };
+ }
+
+ /**
+ * The reply when the action went well.
+ *
+ * @throws CliException with the exit code of the result when the
action failed or timed out
+ */
+ public JsonObject orThrow() {
+ if (isOk()) {
+ return reply;
+ }
+ throw new CliException(exitCode(), error);
+ }
+ }
+
+ private ActionClient() {
+ }
+
+ /**
+ * Runs an action that has a result, such as a route dump, and waits for
the result.
+ *
+ * @param process the integration
+ * @param action the action name
+ * @param options sets the options of the action, or <tt>null</tt>
+ * @param timeout how long to wait for the result, in millis
+ */
+ public static ActionResult execute(CamelProcess process, String action,
Consumer<JsonObject> options, long timeout) {
+ return execute(process.pid(), process.camelVersion(), request(action,
options), timeout, true);
+ }
+
+ /**
+ * Sends an action without a result, such as a route stop, and waits until
the integration confirms it.
+ *
+ * @param process the integration
+ * @param action the action name
+ * @param options sets the options of the action, or <tt>null</tt>
+ * @param timeout how long to wait for the confirmation, in millis
+ */
+ public static ActionResult send(CamelProcess process, String action,
Consumer<JsonObject> options, long timeout) {
+ return execute(process.pid(), process.camelVersion(), request(action,
options), timeout, false);
+ }
+
+ /**
+ * Runs an action that has a result, for an integration known by its pid
only.
+ *
+ * @throws IOException when the action cannot be written
+ */
+ public static ActionResult execute(long pid, JsonObject request, long
timeout) throws IOException {
+ return run(pid, versionOf(pid), request, timeout, true);
+ }
+
+ private static ActionResult execute(
+ long pid, String camelVersion, JsonObject request, long timeout,
boolean hasResult) {
+ try {
+ return run(pid, camelVersion, request, timeout, hasResult);
+ } catch (IOException e) {
+ return new ActionResult(
+ Status.ERROR, null, "Cannot send the action to the
integration with pid " + pid + ": " + e.getMessage(),
+ null);
+ }
+ }
+
+ private static ActionResult run(long pid, String camelVersion, JsonObject
request, long timeout, boolean hasResult)
+ throws IOException {
+ // an unknown version is a recent one (every connector writes its
version in the status file)
+ boolean confirms = camelVersion == null || isAtLeast(camelVersion, 4,
24);
+ Exchange<JsonObject> exchange = exchange(pid, camelVersion, request,
timeout, hasResult || confirms,
+ text -> Jsoner.deserialize(text, (JsonObject) null));
+ if (exchange.reply() != null) {
+ return result(exchange.reply());
+ }
+ if (exchange.taken()) {
+ // the integration took the action (it deletes the file), but does
not confirm it
+ return new ActionResult(Status.UNCONFIRMED, null, null, null);
+ }
+ String action = request.getString("action");
+ return new ActionResult(
+ Status.TIMEOUT, null, "No answer from the integration with pid
" + pid + " to the action " + action
+ + " within " + timeout + " millis",
+ null);
+ }
+
+ /**
+ * Runs an action and returns the answer as the integration wrote it, for
tools that pass it on as text.
+ *
+ * @return the answer, or <tt>null</tt> when there is none in
time
+ * @throws IOException when the action cannot be written
+ */
+ public static String executeText(long pid, JsonObject request, long
timeout) throws IOException {
+ return exchange(pid, versionOf(pid), request, timeout, true, text ->
text).reply();
+ }
+
+ // the answer to an action, as read, or null; and whether the integration
took the action
+ private record Exchange<T>(T reply, boolean taken) {
+ }
+
+ private static <T> Exchange<T> exchange(
+ long pid, String camelVersion, JsonObject request, long timeout,
boolean waitForAnswer,
+ Function<String, T> reader)
+ throws IOException {
+ boolean requestFiles = camelVersion == null || isAtLeast(camelVersion,
4, 21);
+ Path dir = CommandLineHelper.getCamelDir();
+ Path actionFile;
+ Path outputFile;
+ if (requestFiles) {
+ String requestId = UUID.randomUUID().toString();
+ actionFile = dir.resolve(pid + "-action-" + requestId + ".json");
+ outputFile = dir.resolve(pid + "-output-" + requestId + ".json");
+ } else {
+ actionFile = dir.resolve(pid + "-action.json");
+ outputFile = dir.resolve(pid + "-output.json");
+ // an answer left behind by an earlier command
+ Files.deleteIfExists(outputFile);
+ }
+ Path temporary = dir.resolve(pid + "-" + UUID.randomUUID() + ".tmp");
+ try {
+ Files.writeString(temporary, request.toJson());
+ // the integration must never read half an action
+ Files.move(temporary, actionFile, StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
+
+ StopWatch watch = new StopWatch();
+ while (watch.taken() < timeout) {
+ try {
+ Thread.sleep(Math.min(POLL_INTERVAL, Math.max(1, timeout -
watch.taken())));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ if (waitForAnswer) {
+ T reply = readReply(outputFile, reader);
+ if (reply != null) {
+ return new Exchange<>(reply, true);
+ }
+ } else if (!Files.exists(actionFile)) {
+ return new Exchange<>(null, true);
+ }
+ }
+ return new Exchange<>(null, false);
+ } finally {
+ // removing an action the integration has not taken yet cancels it
+ Files.deleteIfExists(temporary);
+ Files.deleteIfExists(actionFile);
+ Files.deleteIfExists(outputFile);
+ }
+ }
+
+ private static <T> T readReply(Path outputFile, Function<String, T>
reader) {
+ try {
+ if (Files.exists(outputFile)) {
+ String text = Files.readString(outputFile);
+ if (!text.isBlank()) {
+ // null when the answer is still being written
+ return reader.apply(text);
+ }
+ }
+ } catch (IOException e) {
+ // the integration writes the answer in place: try again
+ }
+ return null;
+ }
+
+ /**
+ * The result of an answer: an error when its status is <tt>failed</tt> or
<tt>error</tt>, the same as the connector
+ * does for its WebSocket transport.
+ */
+ static ActionResult result(JsonObject reply) {
+ String status = reply.getString("status");
+ if ("failed".equals(status) || "error".equals(status)) {
+ return new ActionResult(Status.ERROR, reply, messageOf(reply),
reply.getString("reason"));
+ }
+ return new ActionResult(Status.OK, reply, null, null);
+ }
+
+ private static String messageOf(JsonObject reply) {
+ if (reply.get("exception") instanceof JsonObject exception &&
exception.getString("message") != null) {
+ return exception.getString("message");
+ }
+ if (reply.get("error") instanceof String error && !error.isBlank()) {
+ return error;
+ }
+ String message = reply.getString("message");
+ return message != null && !message.isBlank() ? message : "Action
failed";
+ }
+
+ private static JsonObject request(String action, Consumer<JsonObject>
options) {
+ JsonObject request = new JsonObject();
+ request.put("action", action);
+ if (options != null) {
+ options.accept(request);
+ }
+ return request;
+ }
+
+ private static String versionOf(long pid) {
+ try {
+ Path f = CommandLineHelper.getCamelDir().resolve(pid +
"-status.json");
+ JsonObject status = Files.exists(f) ?
Jsoner.deserialize(Files.readString(f), (JsonObject) null) : null;
+ if (status != null && status.get("context") instanceof JsonObject
context) {
+ return context.getString("version");
+ }
+ } catch (Exception e) {
+ // unknown
+ }
+ return null;
+ }
+
+ /**
+ * Whether a Camel version such as <tt>4.24.0-SNAPSHOT</tt> is the given
major.minor or later.
+ */
+ static boolean isAtLeast(String version, int major, int minor) {
+ Matcher m = VERSION.matcher(version);
+ if (!m.find()) {
+ // not a version we know how to read: assume a recent one
+ return true;
+ }
+ int actualMajor = Integer.parseInt(m.group(1));
+ int actualMinor = Integer.parseInt(m.group(2));
+ return actualMajor > major || actualMajor == major && actualMinor >=
minor;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ProcessFinder.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ProcessFinder.java
new file mode 100644
index 000000000000..37849967d869
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ProcessFinder.java
@@ -0,0 +1,226 @@
+/*
+ * 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.common;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * Finds the running Camel integrations on this machine, by name or pid.
+ * <p/>
+ * An integration is running when its process is alive and has a status file
in <tt>~/.camel</tt>, which the Camel CLI
+ * connector writes. An integration whose Camel has stopped (phase 9 or later,
such as the launcher process of an
+ * integration that runs in a child process) is not running anymore.
+ * <p/>
+ * A number is a pid. A name matches the name of the integration (as <tt>camel
ps</tt> shows it, without file extension)
+ * or its CamelContext name: it is a prefix (<tt>foo</tt> matches
<tt>foo-bar</tt>) unless it ends with <tt>!</tt>
+ * (<tt>foo!</tt> matches only <tt>foo</tt>), and it may use wildcards and
regular expressions.
+ */
+public final class ProcessFinder {
+
+ // the phase of a CamelContext that has stopped (see AbstractCamelContext)
+ private static final int STOPPED_PHASE = 9;
+
+ /**
+ * A running Camel integration, and the status it last wrote.
+ *
+ * @param pid the pid
+ * @param name the name of the integration: its main class or source file
+ * @param status the status file
+ */
+ public record CamelProcess(long pid, String name, JsonObject status) {
+
+ /**
+ * The name of the CamelContext, or <tt>null</tt>.
+ */
+ public String contextName() {
+ JsonObject context = context();
+ return context != null ? context.getString("name") : null;
+ }
+
+ /**
+ * The Camel version of the integration, or <tt>null</tt> when unknown.
+ */
+ public String camelVersion() {
+ JsonObject context = context();
+ return context != null ? context.getString("version") : null;
+ }
+
+ private JsonObject context() {
+ return status != null && status.get("context") instanceof
JsonObject context ? context : null;
+ }
+ }
+
+ private ProcessFinder() {
+ }
+
+ /**
+ * Every running Camel integration.
+ */
+ public static List<CamelProcess> findAll() {
+ return scan(null, false);
+ }
+
+ /**
+ * The running Camel integrations that match the name or pid. A name never
matches this process, as the Camel CLI
+ * can run Camel itself.
+ *
+ * @param nameOrPid a name, a pattern or a pid; <tt>null</tt> or
<tt>*</tt> for every integration
+ */
+ public static List<CamelProcess> find(String nameOrPid) {
+ if (nameOrPid == null || nameOrPid.isBlank()) {
+ nameOrPid = "*";
+ }
+ return scan(nameOrPid, true);
+ }
+
+ /**
+ * The one running Camel integration that matches the name or pid.
+ *
+ * @throws CliException (exit code 3) when no integration matches, or more
than one
+ */
+ public static CamelProcess requireOne(String nameOrPid) {
+ List<CamelProcess> found = find(nameOrPid);
+ if (found.isEmpty()) {
+ throw notFound(nameOrPid);
+ }
+ if (found.size() > 1) {
+ throw CliException.notFound("Name or pid " + nameOrPid + " matches
" + found.size()
+ + " running Camel integrations: " +
describe(found)
+ + ". Specify a name or pid that
matches exactly one.");
+ }
+ return found.get(0);
+ }
+
+ /**
+ * The running Camel integrations that match the name or pid, at least one.
+ *
+ * @throws CliException (exit code 3) when no integration matches
+ */
+ public static List<CamelProcess> requireAny(String nameOrPid) {
+ List<CamelProcess> found = find(nameOrPid);
+ if (found.isEmpty()) {
+ throw notFound(nameOrPid);
+ }
+ return found;
+ }
+
+ private static CliException notFound(String nameOrPid) {
+ return CliException.notFound("No running Camel integration matches: "
+ nameOrPid);
+ }
+
+ private static String describe(List<CamelProcess> processes) {
+ StringJoiner sj = new StringJoiner(", ");
+ for (CamelProcess p : processes) {
+ sj.add(p.name() != null && !p.name().isEmpty() ? p.pid() + " (" +
p.name() + ")" : String.valueOf(p.pid()));
+ }
+ return sj.toString();
+ }
+
+ private static List<CamelProcess> scan(String nameOrPid, boolean
skipThisProcess) {
+ long current = ProcessHandle.current().pid();
+ Map<Long, Path> statusFiles = statusFiles();
+ boolean byPid = nameOrPid != null && nameOrPid.matches("\\d+");
+ long pid = byPid ? Long.parseLong(nameOrPid) : -1;
+ String pattern = nameOrPid == null || byPid ? null :
pattern(nameOrPid);
+
+ List<CamelProcess> answer = new ArrayList<>();
+ // the live processes among those with a status file
+ ProcessHandle.allProcesses()
+ .filter(ph -> statusFiles.containsKey(ph.pid()))
+ .forEach(ph -> {
+ if (byPid ? ph.pid() != pid : skipThisProcess && ph.pid()
== current) {
+ return;
+ }
+ JsonObject status = readStatus(statusFiles.get(ph.pid()));
+ if (status == null || stopped(status)) {
+ return;
+ }
+ String name = ProcessHelper.extractName(status, ph);
+ if (pattern == null || matches(name, status, pattern)) {
+ answer.add(new CamelProcess(ph.pid(), name, status));
+ }
+ });
+ answer.sort(Comparator.comparingLong(CamelProcess::pid));
+ return answer;
+ }
+
+ private static String pattern(String name) {
+ if (name.endsWith("!")) {
+ // exactly this name
+ return name.substring(0, name.length() - 1);
+ }
+ // a prefix
+ return name.endsWith("*") ? name : name + "*";
+ }
+
+ private static boolean matches(String name, JsonObject status, String
pattern) {
+ // without file extension, so it is easier to match by name
+ String onlyName = FileUtil.onlyName(name);
+ if (onlyName != null && !onlyName.isEmpty() &&
PatternHelper.matchPattern(onlyName, pattern)) {
+ return true;
+ }
+ if (status.get("context") instanceof JsonObject context) {
+ String contextName = context.getString("name");
+ // the default name of the Camel CLI, which says nothing about the
integration
+ if (contextName != null && !contextName.isEmpty() &&
!"CamelJBang".equals(contextName)) {
+ return PatternHelper.matchPattern(contextName, pattern);
+ }
+ }
+ return false;
+ }
+
+ private static boolean stopped(JsonObject status) {
+ return status.get("context") instanceof JsonObject context
+ && context.getIntegerOrDefault("phase", 0) >= STOPPED_PHASE;
+ }
+
+ // the status files in ~/.camel, by pid: listing them is much faster than
looking at every process
+ private static Map<Long, Path> statusFiles() {
+ Map<Long, Path> answer = new HashMap<>();
+ File[] files = CommandLineHelper.getCamelDir().toFile().listFiles((d,
n) -> n.matches("\\d+-status\\.json"));
+ if (files != null) {
+ for (File f : files) {
+ String n = f.getName();
+ answer.put(Long.parseLong(n.substring(0, n.indexOf('-'))),
f.toPath());
+ }
+ }
+ return answer;
+ }
+
+ private static JsonObject readStatus(Path file) {
+ try {
+ String text = Files.readString(file);
+ return text.isBlank() ? null : Jsoner.deserialize(text,
(JsonObject) null);
+ } catch (Exception e) {
+ // being written, or gone
+ return null;
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
index 4ed9da5462d7..11c819f8ef67 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
@@ -16,20 +16,13 @@
*/
package org.apache.camel.dsl.jbang.core.common;
-import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
-import java.util.UUID;
import java.util.function.Consumer;
-import java.util.function.Function;
-import org.apache.camel.support.PatternHelper;
-import org.apache.camel.util.FileUtil;
-import org.apache.camel.util.StopWatch;
import org.apache.camel.util.json.JsonObject;
import org.apache.camel.util.json.Jsoner;
@@ -70,37 +63,8 @@ public final class RuntimeHelper {
*/
public static List<ProcessInfo> discoverProcesses() {
List<ProcessInfo> result = new ArrayList<>();
- Path camelDir = CommandLineHelper.getCamelDir();
- File dir = camelDir.toFile();
- if (!dir.isDirectory()) {
- return result;
- }
-
- File[] statusFiles = dir.listFiles((d, name) ->
name.matches("\\d+-status\\.json"));
- if (statusFiles == null) {
- return result;
- }
-
- for (File sf : statusFiles) {
- String fileName = sf.getName();
- long pid = Long.parseLong(fileName.substring(0,
fileName.indexOf('-')));
- if
(!ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false)) {
- continue;
- }
- try {
- JsonObject root = readStatusFromFile(sf.toPath());
- if (root != null) {
- String name = ProcessHelper.extractName(root,
ProcessHandle.of(pid).orElse(null));
- String contextName = null;
- JsonObject context = (JsonObject) root.get("context");
- if (context != null) {
- contextName = context.getString("name");
- }
- result.add(new ProcessInfo(pid, name, contextName));
- }
- } catch (Exception e) {
- // skip
- }
+ for (ProcessFinder.CamelProcess p : ProcessFinder.findAll()) {
+ result.add(new ProcessInfo(p.pid(), p.name(), p.contextName()));
}
return result;
}
@@ -113,32 +77,11 @@ public final class RuntimeHelper {
* @param nameOrPid a process name (with optional wildcard), a numeric PID
string, or {@code null} for auto-detect
*/
public static ProcessInfo findProcess(String nameOrPid) {
- List<ProcessInfo> processes = discoverProcesses();
- if (processes.isEmpty()) {
- return null;
- }
-
- if (nameOrPid != null && !nameOrPid.isBlank()) {
- if (nameOrPid.matches("\\d+")) {
- long pid = Long.parseLong(nameOrPid);
- return processes.stream()
- .filter(p -> p.pid == pid)
- .findFirst()
- .orElse(null);
- }
- String pattern = nameOrPid.endsWith("*") ? nameOrPid : nameOrPid +
"*";
- List<ProcessInfo> matched = processes.stream()
- .filter(p -> (p.name != null &&
PatternHelper.matchPattern(FileUtil.onlyName(p.name), pattern))
- || (p.contextName != null &&
PatternHelper.matchPattern(p.contextName, pattern)))
- .toList();
- if (matched.size() == 1) {
- return matched.get(0);
- }
- return null;
- }
-
- if (processes.size() == 1) {
- return processes.get(0);
+ List<ProcessFinder.CamelProcess> found = nameOrPid != null &&
!nameOrPid.isBlank()
+ ? ProcessFinder.find(nameOrPid) : ProcessFinder.findAll();
+ if (found.size() == 1) {
+ ProcessFinder.CamelProcess p = found.get(0);
+ return new ProcessInfo(p.pid(), p.name(), p.contextName());
}
return null;
}
@@ -196,7 +139,7 @@ public final class RuntimeHelper {
configure.accept(request);
}
try {
- String response = executeAction(pid, request, timeoutMs,
Function.identity());
+ String response = ActionClient.executeText(pid, request,
timeoutMs);
if (response != null) {
return response;
}
@@ -207,58 +150,14 @@ public final class RuntimeHelper {
}
/**
- * Executes a request and waits for a complete JSON object. Each request
uses independent files, published
- * atomically, and removing an unfinished request asks the connector to
cancel it.
+ * Executes a request and waits for a complete JSON object (see {@link
ActionClient}). Each request uses independent
+ * files, published atomically, and removing an unfinished request asks
the connector to cancel it.
*
* @return the response, or {@code null} on timeout or
interruption
* @throws IOException if publishing or cleaning up the request fails
*/
public static JsonObject executeAction(long pid, JsonObject request, long
timeoutMs) throws IOException {
- return executeAction(pid, request, timeoutMs, text ->
Jsoner.deserialize(text, (JsonObject) null));
- }
-
- private static <T> T executeAction(long pid, JsonObject request, long
timeoutMs, Function<String, T> reader)
- throws IOException {
- String requestId = UUID.randomUUID().toString();
- Path directory = CommandLineHelper.getCamelDir();
- Path actionFile = directory.resolve(pid + "-action-" + requestId +
".json");
- Path outputFile = directory.resolve(pid + "-output-" + requestId +
".json");
- Path temporary = directory.resolve(pid + "-" + requestId + ".tmp");
- try {
- Files.writeString(temporary, request.toJson());
- Files.move(temporary, actionFile, StandardCopyOption.ATOMIC_MOVE);
- StopWatch watch = new StopWatch();
- while (watch.taken() < timeoutMs) {
- try {
- Thread.sleep(Math.min(POLL_INTERVAL_MS, Math.max(1,
timeoutMs - watch.taken())));
- if (Files.exists(outputFile)) {
- String text = Files.readString(outputFile);
- if (!text.isEmpty()) {
- T response = reader.apply(text);
- if (response != null) {
- return response;
- }
- }
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- break;
- } catch (IOException e) {
- // The runtime writes output in place; retry until the
response is complete.
- }
- }
- return null;
- } finally {
- try {
- Files.deleteIfExists(actionFile);
- } finally {
- try {
- Files.deleteIfExists(outputFile);
- } finally {
- Files.deleteIfExists(temporary);
- }
- }
- }
+ return ActionClient.execute(pid, request, timeoutMs).reply();
}
/**
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TimeoutConverter.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TimeoutConverter.java
new file mode 100644
index 000000000000..4598582e5567
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TimeoutConverter.java
@@ -0,0 +1,37 @@
+/*
+ * 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.common;
+
+import org.apache.camel.util.TimeUtils;
+import picocli.CommandLine;
+
+/**
+ * Converts the <tt>--timeout</tt> option of the commands that talk to a
running integration, to millis: a number is
+ * millis (<tt>20000</tt>), and a duration has units (<tt>30s</tt>,
<tt>1m30s</tt>).
+ */
+public class TimeoutConverter implements CommandLine.ITypeConverter<Long> {
+
+ @Override
+ public Long convert(String value) {
+ try {
+ return TimeUtils.toMilliSeconds(value.trim());
+ } catch (RuntimeException e) {
+ throw new CommandLine.TypeConversionException(
+ "'" + value + "' is not a timeout: use millis, or a
duration such as 30s");
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ActionClientTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ActionClientTest.java
new file mode 100644
index 000000000000..dec47a966cb2
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ActionClientTest.java
@@ -0,0 +1,238 @@
+/*
+ * 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.common;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+import org.apache.camel.dsl.jbang.core.common.ActionClient.ActionResult;
+import org.apache.camel.dsl.jbang.core.common.ActionClient.Status;
+import org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.awaitility.Awaitility.await;
+
+/**
+ * Talks to a fake integration that answers like the camel-cli-connector of a
given Camel version.
+ */
+class ActionClientTest {
+
+ private static final long PID = 123;
+
+ @TempDir
+ Path home;
+
+ private Path previousHome;
+ private ExecutorService executor;
+
+ @BeforeEach
+ void setup() throws Exception {
+ previousHome = CommandLineHelper.getHomeDir();
+ CommandLineHelper.useHomeDir(home.toString());
+ Files.createDirectories(CommandLineHelper.getCamelDir());
+ executor = Executors.newSingleThreadExecutor();
+ }
+
+ @AfterEach
+ void restore() {
+ executor.shutdownNow();
+ CommandLineHelper.useHomeDir(previousHome.toString());
+ }
+
+ @Test
+ void resultOfAnAction() throws Exception {
+ Future<JsonObject> request = answerRequest(r ->
json("{\"status\":\"success\",\"routes\":[]}"));
+
+ ActionResult result = ActionClient.execute(integration("4.24.0"),
"route-dump", o -> o.put("filter", "*"), 5000);
+
+ assertThat(result.status()).isEqualTo(Status.OK);
+ assertThat(result.reply()).containsKey("routes");
+ assertThat(result.exitCode()).isEqualTo(CliExitCode.OK);
+ assertThat(request.get(5, TimeUnit.SECONDS)).containsEntry("action",
"route-dump").containsEntry("filter", "*");
+ assertNoFilesLeft();
+ }
+
+ @Test
+ void failedActionIsAnError() throws Exception {
+ answerRequest(r -> json("{\"status\":\"failed\",\"error\":\"Boom\"}"));
+
+ ActionResult result = ActionClient.execute(integration("4.24.0"),
"eval", null, 5000);
+
+ assertThat(result.status()).isEqualTo(Status.ERROR);
+ assertThat(result.error()).isEqualTo("Boom");
+ assertThat(result.exitCode()).isEqualTo(CliExitCode.FAILURE);
+
assertThatThrownBy(result::orThrow).isInstanceOf(CliException.class).hasMessage("Boom");
+ }
+
+ @Test
+ void notFoundIsExitCodeThree() throws Exception {
+ answerRequest(r -> json("{\"status\":\"failed\",\"error\":\"No route
matching: x\",\"reason\":\"not-found\"}"));
+
+ ActionResult result = ActionClient.send(integration("4.24.0"),
"route", o -> o.put("id", "x"), 5000);
+
+ assertThat(result.isNotFound()).isTrue();
+ assertThat(result.exitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+ assertThatThrownBy(result::orThrow).isInstanceOf(CliException.class)
+ .extracting(e -> ((CliException)
e).getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+ }
+
+ @Test
+ void actionWithoutResultIsConfirmed() throws Exception {
+ answerRequest(r -> json("{\"status\":\"success\"}"));
+
+ ActionResult result = ActionClient.send(integration("4.24.0"), "gc",
null, 5000);
+
+ assertThat(result.status()).isEqualTo(Status.OK);
+ assertNoFilesLeft();
+ }
+
+ @Test
+ void noAnswerIsATimeout() throws Exception {
+ ActionResult result = ActionClient.send(integration("4.24.0"), "gc",
null, 300);
+
+ assertThat(result.status()).isEqualTo(Status.TIMEOUT);
+ assertThat(result.exitCode()).isEqualTo(CliExitCode.TIMEOUT);
+ assertThat(result.error()).contains("No answer from the integration
with pid 123 to the action gc");
+ // the action is withdrawn, so the integration does not run it later
+ assertNoFilesLeft();
+ }
+
+ @Test
+ void olderIntegrationTakesAnActionWithoutConfirmingIt() throws Exception {
+ // before 4.24 the connector deletes the request without answering an
action without a result
+ answerRequest(r -> null);
+
+ ActionResult result = ActionClient.send(integration("4.22.1"), "gc",
null, 5000);
+
+ assertThat(result.status()).isEqualTo(Status.UNCONFIRMED);
+ assertThat(result.isOk()).isTrue();
+ assertThat(result.exitCode()).isEqualTo(CliExitCode.OK);
+ }
+
+ @Test
+ void integrationBefore421UsesTheSharedActionFile() throws Exception {
+ Path output = CommandLineHelper.getCamelDir().resolve(PID +
"-output.json");
+ // a stale answer left by an earlier command must not be taken as the
answer
+ Files.writeString(output, "{\"status\":\"success\",\"stale\":true}");
+ Path action = CommandLineHelper.getCamelDir().resolve(PID +
"-action.json");
+ executor.submit(() -> {
+ await().atMost(5, TimeUnit.SECONDS).until(() ->
Files.exists(action));
+ Files.writeString(output,
"{\"status\":\"success\",\"fresh\":true}");
+ Files.delete(action);
+ return null;
+ });
+
+ ActionResult result = ActionClient.execute(integration("4.18.3"),
"route-dump", null, 5000);
+
+ assertThat(result.reply()).containsEntry("fresh",
true).doesNotContainKey("stale");
+ assertThat(output).doesNotExist();
+ }
+
+ @Test
+ void executeByPidReadsTheVersionFromTheStatusFile() throws Exception {
+ Files.writeString(CommandLineHelper.getCamelDir().resolve(PID +
"-status.json"),
+ "{\"context\":{\"version\":\"4.24.0\"}}");
+ answerRequest(r -> json("{\"status\":\"success\",\"value\":42}"));
+
+ JsonObject request = new JsonObject();
+ request.put("action", "jvm");
+ ActionResult result = ActionClient.execute(PID, request, 5000);
+
+ assertThat(result.status()).isEqualTo(Status.OK);
+ assertThat(result.reply().getInteger("value")).isEqualTo(42);
+ }
+
+ @Test
+ void errorMessageComesFromTheAnswer() {
+
assertThat(ActionClient.result(json("{\"status\":\"error\",\"exception\":{\"message\":\"Forced\"}}")).error())
+ .isEqualTo("Forced");
+
assertThat(ActionClient.result(json("{\"status\":\"error\",\"message\":\"No
such datasource\"}")).error())
+ .isEqualTo("No such datasource");
+
assertThat(ActionClient.result(json("{\"status\":\"failed\"}")).error()).isEqualTo("Action
failed");
+
assertThat(ActionClient.result(json("{\"value\":1}")).status()).isEqualTo(Status.OK);
+ }
+
+ @Test
+ void versions() {
+ assertThat(ActionClient.isAtLeast("4.24.0-SNAPSHOT", 4, 24)).isTrue();
+ assertThat(ActionClient.isAtLeast("4.24.1", 4, 21)).isTrue();
+ assertThat(ActionClient.isAtLeast("4.22.0", 4, 24)).isFalse();
+ assertThat(ActionClient.isAtLeast("3.22.0", 4, 21)).isFalse();
+ assertThat(ActionClient.isAtLeast("5.0.0", 4, 24)).isTrue();
+ }
+
+ private static CamelProcess integration(String version) {
+ JsonObject context = new JsonObject();
+ context.put("name", "app");
+ context.put("version", version);
+ JsonObject status = new JsonObject();
+ status.put("context", context);
+ return new CamelProcess(PID, "app", status);
+ }
+
+ /**
+ * Answers the next request with a file of its own like the connector
does: writes the answer (unless null), then
+ * deletes the request.
+ */
+ private Future<JsonObject> answerRequest(Function<JsonObject, JsonObject>
answer) {
+ return executor.submit(() -> {
+ Path dir = CommandLineHelper.getCamelDir();
+ Path request = await().atMost(5, TimeUnit.SECONDS).until(() ->
requestFiles(dir), files -> !files.isEmpty())
+ .get(0);
+ JsonObject action = json(Files.readString(request));
+ JsonObject reply = answer.apply(action);
+ String id = request.getFileName().toString().substring((PID +
"-action-").length()).replace(".json", "");
+ if (reply != null) {
+ Files.writeString(dir.resolve(PID + "-output-" + id +
".json"), reply.toJson());
+ }
+ Files.delete(request);
+ return action;
+ });
+ }
+
+ private static List<Path> requestFiles(Path dir) throws Exception {
+ try (var files = Files.list(dir)) {
+ return files.filter(f -> f.getFileName().toString().startsWith(PID
+ "-action-")).toList();
+ }
+ }
+
+ private static void assertNoFilesLeft() throws Exception {
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+ try (var files = Files.list(CommandLineHelper.getCamelDir())) {
+ assertThat(files.map(f -> f.getFileName().toString()).filter(n
-> !n.endsWith("-status.json")))
+ .isEmpty();
+ }
+ });
+ }
+
+ private static JsonObject json(String text) {
+ return (JsonObject) Jsoner.deserialize(text, (JsonObject) null);
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessFinderTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessFinderTest.java
new file mode 100644
index 000000000000..6aed63614207
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessFinderTest.java
@@ -0,0 +1,207 @@
+/*
+ * 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.common;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.MockedStatic;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+/**
+ * Which integrations a name or pid finds: a running integration has a live
process and a status file, and its Camel has
+ * not stopped.
+ */
+class ProcessFinderTest {
+
+ private static final long CURRENT = 99999L;
+
+ @TempDir
+ Path home;
+
+ private Path previousHome;
+ private final List<ProcessHandle> processes = new ArrayList<>();
+ private MockedStatic<ProcessHandle> mocked;
+
+ @BeforeEach
+ void setup() throws Exception {
+ previousHome = CommandLineHelper.getHomeDir();
+ CommandLineHelper.useHomeDir(home.toString());
+ Files.createDirectories(CommandLineHelper.getCamelDir());
+
+ mocked = mockStatic(ProcessHandle.class);
+ ProcessHandle current = handle(CURRENT);
+ mocked.when(ProcessHandle::current).thenReturn(current);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv ->
processes.stream());
+ }
+
+ @AfterEach
+ void restore() {
+ mocked.close();
+ CommandLineHelper.useHomeDir(previousHome.toString());
+ }
+
+ @Test
+ void findsRunningIntegrations() throws Exception {
+ running(100, "orders", 5);
+ running(200, "billing", 5);
+ // Camel has stopped: the launcher of an integration that runs in a
child process
+ running(300, "launcher", 9);
+ // a status file left behind by a process that is gone
+ status(400, "gone", 5);
+ // a process without status file is no Camel integration
+ processes.add(handle(500));
+
+
assertThat(ProcessFinder.findAll()).extracting(CamelProcess::pid).containsExactly(100L,
200L);
+
assertThat(ProcessFinder.find("*")).extracting(CamelProcess::pid).containsExactly(100L,
200L);
+ }
+
+ @Test
+ void nameIsAPrefixUnlessItEndsWithExclamationMark() throws Exception {
+ running(100, "orders", 5);
+ running(200, "orders-eu", 5);
+
+
assertThat(ProcessFinder.find("ord")).extracting(CamelProcess::pid).containsExactly(100L,
200L);
+
assertThat(ProcessFinder.find("orders!")).extracting(CamelProcess::pid).containsExactly(100L);
+ assertThat(ProcessFinder.find("ord!")).isEmpty();
+ // a regular expression
+
assertThat(ProcessFinder.find("orders-(eu|us)")).extracting(CamelProcess::pid).containsExactly(200L);
+ }
+
+ @Test
+ void theDefaultContextNameOfTheCliMatchesNothing() throws Exception {
+ // the name comes from the main class, and the context has the default
name of the Camel CLI
+ JsonObject root = statusOf("CamelJBang", 5);
+ JsonObject runtime = new JsonObject();
+ runtime.put("mainClass", "Orders");
+ root.put("runtime", runtime);
+
Files.writeString(CommandLineHelper.getCamelDir().resolve("100-status.json"),
root.toJson());
+ processes.add(handle(100));
+
+ assertThat(ProcessFinder.find("Camel")).isEmpty();
+
assertThat(ProcessFinder.find("Ord")).extracting(CamelProcess::name).containsExactly("Orders");
+ }
+
+ @Test
+ void pidMustBeARunningIntegration() throws Exception {
+ running(100, "orders", 5);
+ processes.add(handle(500));
+
+
assertThat(ProcessFinder.find("100")).extracting(CamelProcess::name).containsExactly("orders");
+ // a process that is no Camel integration, and a pid that does not
exist
+ assertThat(ProcessFinder.find("500")).isEmpty();
+ assertThat(ProcessFinder.find("600")).isEmpty();
+ }
+
+ @Test
+ void aNameNeverMatchesThisProcessButItsPidDoes() throws Exception {
+ status(CURRENT, "cli", 5);
+ processes.add(handle(CURRENT));
+
+ assertThat(ProcessFinder.find("cli")).isEmpty();
+ assertThat(ProcessFinder.find(String.valueOf(CURRENT))).hasSize(1);
+ }
+
+ @Test
+ void processKnowsItsVersionAndContextName() throws Exception {
+ JsonObject root = statusOf("orders", 5);
+ ((JsonObject) root.get("context")).put("version", "4.24.0-SNAPSHOT");
+
Files.writeString(CommandLineHelper.getCamelDir().resolve("100-status.json"),
root.toJson());
+ processes.add(handle(100));
+
+ CamelProcess p = ProcessFinder.requireOne("orders");
+ assertThat(p.camelVersion()).isEqualTo("4.24.0-SNAPSHOT");
+ assertThat(p.contextName()).isEqualTo("orders");
+ }
+
+ @Test
+ void requireOneNeedsExactlyOneMatch() throws Exception {
+ running(100, "orders", 5);
+ running(200, "orders-eu", 5);
+
+ assertThat(ProcessFinder.requireOne("orders!").pid()).isEqualTo(100L);
+ assertThatThrownBy(() -> ProcessFinder.requireOne("billing"))
+ .isInstanceOf(CliException.class)
+ .hasMessage("No running Camel integration matches: billing")
+ .extracting(e -> ((CliException)
e).getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+ assertThatThrownBy(() -> ProcessFinder.requireOne("orders"))
+ .isInstanceOf(CliException.class)
+ .hasMessage("Name or pid orders matches 2 running Camel
integrations: 100 (orders), 200 (orders-eu)."
+ + " Specify a name or pid that matches exactly
one.")
+ .extracting(e -> ((CliException)
e).getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+ }
+
+ @Test
+ void requireAnyNeedsAMatch() throws Exception {
+ running(100, "orders", 5);
+
+ assertThat(ProcessFinder.requireAny("ord")).hasSize(1);
+ assertThatThrownBy(() -> ProcessFinder.requireAny("billing"))
+ .isInstanceOf(CliException.class)
+ .extracting(e -> ((CliException)
e).getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+ }
+
+ @Test
+ void noCamelDirectoryFindsNothing() {
+ CommandLineHelper.useHomeDir(home.resolve("missing").toString());
+
+ assertThat(ProcessFinder.findAll()).isEmpty();
+ assertThat(ProcessFinder.find("orders")).isEmpty();
+ }
+
+ private void running(long pid, String contextName, int phase) throws
Exception {
+ status(pid, contextName, phase);
+ processes.add(handle(pid));
+ }
+
+ private static void status(long pid, String contextName, int phase) throws
Exception {
+ Files.writeString(CommandLineHelper.getCamelDir().resolve(pid +
"-status.json"),
+ statusOf(contextName, phase).toJson());
+ }
+
+ private static JsonObject statusOf(String contextName, int phase) {
+ JsonObject context = new JsonObject();
+ context.put("name", contextName);
+ context.put("phase", phase);
+ JsonObject root = new JsonObject();
+ root.put("context", context);
+ return root;
+ }
+
+ private static ProcessHandle handle(long pid) {
+ ProcessHandle ph = mock(ProcessHandle.class);
+ ProcessHandle.Info info = mock(ProcessHandle.Info.class);
+ when(ph.pid()).thenReturn(pid);
+ when(ph.info()).thenReturn(info);
+ when(info.commandLine()).thenReturn(Optional.empty());
+ return ph;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TimeoutConverterTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TimeoutConverterTest.java
new file mode 100644
index 000000000000..393393c722d3
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TimeoutConverterTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.common;
+
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class TimeoutConverterTest {
+
+ private final TimeoutConverter converter = new TimeoutConverter();
+
+ @Test
+ void numberIsMillis() {
+ assertThat(converter.convert("20000")).isEqualTo(20000L);
+ }
+
+ @Test
+ void durationHasUnits() {
+ assertThat(converter.convert("30s")).isEqualTo(30_000L);
+ assertThat(converter.convert("1m30s")).isEqualTo(90_000L);
+ assertThat(converter.convert(" 2m ")).isEqualTo(120_000L);
+ }
+
+ @Test
+ void anythingElseIsAnInvalidOption() {
+ assertThatThrownBy(() -> converter.convert("soon"))
+ .isInstanceOf(CommandLine.TypeConversionException.class)
+ .hasMessage("'soon' is not a timeout: use millis, or a
duration such as 30s");
+ }
+}