Croway opened a new pull request, #28682:
URL: https://github.com/apache/camel/pull/28682

   _Claude Code on behalf of Croway_
   
   [CAMEL-26397](https://issues.apache.org/jira/browse/CAMEL-26397)
   
   ## Problem
   
   On Linux the JDK reads one page (4 KB) of `/proc/<pid>/cmdline` 
(`os_getCmdlineAndUserInfo` in
   `ProcessHandleImpl_linux.c`, the same in JDK 21.0.12 and 25.0.4). For a 
longer command line
   `ProcessHandle.Info.arguments()` is empty and `commandLine()` is cut at 4096 
characters. `camel run` started through
   jbang always has one: its `-classpath` alone is about 15 KB. macOS has no 
such limit.
   
   What read the command line through `ProcessHandle.Info`:
   
   - `camel_control restart` (`IntegrationLauncher.restart`): "Cannot restart 
pid N: its command line is not available".
   - `camel_write_file` / `camel_edit_file` 
(`IntegrationLauncher.devModeFrom`): no dev-mode integration found, so no
     `reload` result, although the integration reloads.
   - `camel_run` of a directory an integration runs from (`runningMessage`): 
the generic message; `watchesFile`: "watches
     every file".
   - `camel restart` (`RestartProcess`): falls back to parsing the cut 
`commandLine()`, so it would relaunch a broken
     classpath.
   - `ProcessHelper.doExtractName` (the NAME of `camel ps` and the other 
commands) and the "JBang" platform of
     `camel get` / `camel top`: they search the cut `commandLine()`, which does 
not reach `main.CamelJBang run`.
   
   On Windows the JDK gives no arguments and no command line of any process 
(`ProcessHandleImpl_win.c` sets only the
   command and the user), so `camel_control restart` never worked there either.
   
   ## Fix
   
   | Where | Change |
   |---|---|
   | `ProcessHelper.commandLine(ProcessHandle)` (new) | The command and all the 
arguments, or null. `Info.command()` + `Info.arguments()` when the JDK has 
them; when it knows the process but not its arguments (a cut command line on 
Linux, or a process with an empty argument), `/proc/<pid>/cmdline` is read 
(NUL-separated, a missing trailing NUL tolerated, decoded with 
`native.encoding` as the JDK does), with `Info.command()` as the command, as 
the JDK gives it. A missing or empty file gives null. |
   | `ProcessHelper.commandLineText(ProcessHandle)` (new) | Drop-in for 
`info().commandLine().orElse("")`: the same text, except where the JDK gave a 
command line without its arguments, then the whole one, or empty when it can 
only be the cut one. |
   | `ProcessHelper.isCut(String)` (new) | A command line as long as a page 
(4096, or 16/64 KB on systems with larger pages) was cut. |
   | `ProcessHelper.useProcRoot(Path)` (new) | Test seam, like 
`CommandLineHelper.useHomeDir`. |
   | `IntegrationLauncher` | Keeps the command each process it started 
(`camel_run`, and the process a restart starts) was started with, by pid, 
removed when the process ends. `restart`, `devModeFrom`, `watchesFile` and 
`runningMessage` read that first, then `ProcessHelper.commandLine`. So a 
restart also works where the OS gives no arguments at all, and a second restart 
works. |
   | `RestartProcess` (`camel restart`) | `ProcessHelper.commandLine`; the 
`commandLine()` parse fallback is kept, but never for a cut line ("Cannot 
restart: command line not available" instead of a relaunch with a broken 
classpath). |
   | `ProcessHelper.doExtractName`, `CamelContextStatus` / 
`CamelContextTop.extractPlatform` | `commandLineText`. |
   
   Behaviour change on Linux only, for a command line longer than 4 KB: the 
name of a jbang `camel run <files>` is now
   taken from the route files after `main.CamelJBang run`, as it always was on 
macOS, instead of falling back to the
   Camel context name; `camel get` / `camel top` show the platform `JBang` as 
on macOS.
   
   Left for a follow-up (camel-jbang-plugin-tui): 
`ProcessControl.restartCamelMainProcess` and the Maven restart read
   `Info.arguments()` / `commandLine()` too (same restart failure in the TUI on 
Linux), `StatusParser.parseRuntime` and
   `ProcessTab.getCommandLine` the cut `commandLine()`.
   
   ## Testing
   
   Unit tests (`dsl/camel-jbang/camel-jbang-core`):
   
   | Test | What | On origin/main |
   |---|---|---|
   | `ProcessHelperTest` (7 new) | the whole command line from a fake `/proc` 
when the JDK cut it (with the name from `main.CamelJBang run`), the JDK's own 
when it has all of it, NUL separation (empty argument, last one without NUL), 
no file / empty file, `isCut`, nothing read of a process the JDK knows nothing 
of; `@EnabledOnOs(LINUX)`: a real `sh -c 'sleep 60; exit 0' sh <5000 chars>` 
gives its full argv | new API (does not compile) |
   | `RestartProcessTest.testRestartRelaunchesTheWholeCommandLine` | cut 
`commandLine()`, no arguments, whole one in `/proc`: relaunched and started | 
fails: relaunches the cut line, "stopped right after the restart" |
   | `RestartProcessTest.testCommandLineCutShortIsNotRelaunched` | cut 
`commandLine()`, no `/proc`: "Cannot restart: command line not available", not 
stopped | fails: stops it and relaunches the cut line (`IOException`) |
   | `IntegrationLauncherCommandLineTest` (5, new) | integrations with an empty 
argument, of which the JDK gives no arguments on any OS: restart of one 
`camel_run` started (kept command), twice; restart of one started elsewhere 
(`/proc`); `devModeFrom`, `watchesFile` and the `camel_run` guard message for 
`--source-dir --dev`, own files `--dev`, and no dev mode | all 5 fail ("Cannot 
restart pid N: its command line is not available", `devModeFrom` null, generic 
guard message) |
   
   - `mvnd -P apache-snapshots -T 2 test -pl 
dsl/camel-jbang/camel-jbang-core,dsl/camel-jbang/camel-jbang-mcp,dsl/camel-jbang/camel-jbang-plugin-tui`
 (macOS): core 2527 + 244 tests, 0 failures (3 skipped); mcp 505, 0 failures; 
tui 2283, 0 failures (1 flake, `MonitorStartupTest` connection refused, passed 
on rerun).
   - The same core classes plus the `commands.process` and `commands.action` 
packages run on Linux (Temurin 21, container, JUnit console launcher): 743 
tests, 742 passed including the Linux-only one; 
`DirtyTest.testOnlyTheFilesThatWereRemovedAreCounted` fails there only because 
the container runs as root (a read-only directory does not stop root).
   
   End to end on Linux (container `eclipse-temurin:21-jdk`, a `camel` wrapper 
with a 16 KB `-classpath` around the 4.24.0-SNAPSHOT launcher, the MCP runner 
jar over stdio). Part 3 starts `camel run --source-dir=. --dev` from the shell, 
so the MCP server did not start it and reads `/proc/<pid>/cmdline`; parts 1 and 
2 use the command `camel_run` kept. `InfoCheck` prints what the JDK gives for 
the pid.
   
   Before (origin/main runner jar):
   
   ```
   == 1) camel_run without dev mode, then camel_control restart twice
   >>> camel_run {"directory": "/tmp/probe", "dev": false, "name": "probe"}  
(20.0s)
       {"directory":"/tmp/probe","command":"camel run --source-dir=. 
--name=probe 
--logging-color=false","status":"started","pid":70,"name":"probe","log":"/root/.camel/70.log","devMode":false,"sourceDir":true,"message":"Started
 probe (pid 70); restart it after changing a source file. camel_get_log reads 
its log, camel_get_errors its failed exchanges."}
   probe pid 70
       pid 70: /proc cmdline 16565 bytes; info.command()=true 
info.arguments()=Optional.empty commandLine length=4096
   >>> camel_control {"action": "restart", "name": "probe"}  (0.0s)
       isError=true: Cannot restart pid 70: its command line is not available; 
stop it and run it again
   >>> camel_control {"action": "restart", "name": "probe"}  (0.0s)
       isError=true: Cannot restart pid 70: its command line is not available; 
stop it and run it again
   >>> camel_control {"action": "stop", "name": "probe"}  (0.0s)
       {"result":"Graceful shutdown initiated for PID 70. The application will 
finish processing in-flight exchanges and shut down."}
   == 2) camel_run in dev mode: camel_edit_file waits for the reload, camel_run 
of the same directory
   >>> camel_run {"directory": "/tmp/devprobe", "dev": true, "name": 
"devprobe"}  (6.4s)
       {"directory":"/tmp/devprobe","command":"camel run --source-dir=. --dev 
--prop=camel.main.?routesReloadInterval=500 --name=devprobe 
--logging-color=false","status":"started","pid":159,"name":"devprobe","log":"/root/.camel/159.log","devMode":true,"sourceDir":true,"message":"Started
 devprobe (pid 159); dev mode watches the directory: a changed or added file is 
reloaded. camel_get_log reads its log, camel_get_errors ...
   devprobe pid 159
       pid 159: /proc cmdline 16618 bytes; info.command()=true 
info.arguments()=Optional.empty commandLine length=4096
   >>> camel_edit_file {"directory": "/tmp/devprobe", "file": "dev.camel.yaml", 
"find": "message: devtick", "replace": "message: devtock"}  (1.5s)
       
{"status":"edited","file":"dev.camel.yaml","directory":"/tmp/devprobe","lines":7,"bytes":129,"message":"An
 integration running the file in dev mode reloads it now; otherwise restart the 
integration for the change to take effect.","editedAtLine":7,"replacedLines":1}
   >>> camel_run {"directory": "/tmp/devprobe", "dev": true}  (0.0s)
       
{"directory":"/tmp/devprobe","status":"running","pid":159,"name":"devprobe","log":"/root/.camel/159.log","message":"devprobe
 (pid 159) already runs from this directory; a second copy was not started. 
camel_control stop stops it, then camel_run starts the directory again."}
   >>> camel_control {"action": "stop", "name": "devprobe"}  (0.0s)
       {"result":"Graceful shutdown initiated for PID 159. The application will 
finish processing in-flight exchanges and shut down."}
   == 3) an integration started from the shell in dev mode (not by the MCP 
server): read from /proc
   shelldev pid 224 (shell pid 224 )
       pid 224: /proc cmdline 16552 bytes; info.command()=true 
info.arguments()=Optional.empty commandLine length=4096
   >>> camel_edit_file {"directory": "/tmp/shelldev", "file": 
"shell.camel.yaml", "find": "message: shelltick", "replace": "message: 
shelltock"}  (0.1s)
       
{"status":"edited","file":"shell.camel.yaml","directory":"/tmp/shelldev","lines":7,"bytes":135,"message":"An
 integration running the file in dev mode reloads it now; otherwise restart the 
integration for the change to take effect.","editedAtLine":7,"replacedLines":1}
   >>> camel_run {"directory": "/tmp/shelldev", "dev": true}  (0.0s)
       
{"directory":"/tmp/shelldev","status":"running","pid":224,"name":"shelldev","log":"/root/.camel/224.log","message":"shelldev
 (pid 224) already runs from this directory; a second copy was not started. 
camel_control stop stops it, then camel_run starts the directory again."}
   >>> camel_control {"action": "restart", "name": "shelldev"}  (0.0s)
       isError=true: Cannot restart pid 224: its command line is not available; 
stop it and run it again
   >>> camel_control {"action": "stop", "name": "shelldev"}  (0.0s)
       {"result":"Graceful shutdown initiated for PID 224. The application will 
finish processing in-flight exchanges and shut down."}
   ```
   
   After:
   
   ```
   == 1) camel_run without dev mode, then camel_control restart twice
   >>> camel_run {"directory": "/tmp/probe", "dev": false, "name": "probe"}  
(17.4s)
       {"directory":"/tmp/probe","command":"camel run --source-dir=. 
--name=probe 
--logging-color=false","status":"started","pid":70,"name":"probe","log":"/root/.camel/70.log","devMode":false,"sourceDir":true,"message":"Started
 probe (pid 70); restart it after changing a source file. camel_get_log reads 
its log, camel_get_errors its failed exchanges."}
   probe pid 70
       pid 70: /proc cmdline 16565 bytes; info.command()=true 
info.arguments()=Optional.empty commandLine length=4096
   >>> camel_control {"action": "restart", "name": "probe"}  (6.0s)
       {"result":"Restarted probe as pid 163"}
       pid 163: /proc cmdline 16565 bytes; info.command()=true 
info.arguments()=Optional.empty commandLine length=4096
   >>> camel_control {"action": "restart", "name": "probe"}  (5.8s)
       {"result":"Restarted probe as pid 221"}
       pid 221: /proc cmdline 16565 bytes; info.command()=true 
info.arguments()=Optional.empty commandLine length=4096
   >>> camel_control {"action": "stop", "name": "probe"}  (0.0s)
       {"result":"Graceful shutdown initiated for PID 221. The application will 
finish processing in-flight exchanges and shut down."}
   == 2) camel_run in dev mode: camel_edit_file waits for the reload, camel_run 
of the same directory
   >>> camel_run {"directory": "/tmp/devprobe", "dev": true, "name": 
"devprobe"}  (4.3s)
       {"directory":"/tmp/devprobe","command":"camel run --source-dir=. --dev 
--prop=camel.main.?routesReloadInterval=500 --name=devprobe 
--logging-color=false","status":"started","pid":274,"name":"devprobe","log":"/root/.camel/274.log","devMode":true,"sourceDir":true,"message":"Started
 devprobe (pid 274); dev mode watches the directory: a changed or added file is 
reloaded. camel_get_log reads its log, camel_get_errors ...
   devprobe pid 274
       pid 274: /proc cmdline 16618 bytes; info.command()=true 
info.arguments()=Optional.empty commandLine length=4096
   >>> camel_edit_file {"directory": "/tmp/devprobe", "file": "dev.camel.yaml", 
"find": "message: devtick", "replace": "message: devtock"}  (3.0s)
       
{"status":"edited","file":"dev.camel.yaml","directory":"/tmp/devprobe","lines":7,"bytes":129,"reload":{"status":"reloaded","message":"Routes
 reloaded summary (total:1 started:1 
changed:1)","output":["devtock"]},"message":"The running integration reloaded 
the file. After the reload it logged: 
devtock","editedAtLine":7,"replacedLines":1}
   >>> camel_run {"directory": "/tmp/devprobe", "dev": true}  (0.0s)
       
{"directory":"/tmp/devprobe","status":"running","pid":274,"name":"devprobe","log":"/root/.camel/274.log","message":"devprobe
 (pid 274) already runs from this directory; a second copy was not started. In 
dev mode a changed or added file is reloaded; camel_control restart restarts 
it, for example after adding a Java class."}
   >>> camel_control {"action": "stop", "name": "devprobe"}  (0.0s)
       {"result":"Graceful shutdown initiated for PID 274. The application will 
finish processing in-flight exchanges and shut down."}
   == 3) an integration started from the shell in dev mode (not by the MCP 
server): read from /proc
   shelldev pid 342 (shell pid 342 )
       pid 342: /proc cmdline 16552 bytes; info.command()=true 
info.arguments()=Optional.empty commandLine length=4096
   >>> camel_edit_file {"directory": "/tmp/shelldev", "file": 
"shell.camel.yaml", "find": "message: shelltick", "replace": "message: 
shelltock"}  (2.8s)
       
{"status":"edited","file":"shell.camel.yaml","directory":"/tmp/shelldev","lines":7,"bytes":135,"reload":{"status":"reloaded","message":"Routes
 reloaded summary (total:1 started:1 
changed:1)","output":["shelltock"]},"message":"The running integration reloaded 
the file. After the reload it logged: 
shelltock","editedAtLine":7,"replacedLines":1}
   >>> camel_run {"directory": "/tmp/shelldev", "dev": true}  (0.0s)
       
{"directory":"/tmp/shelldev","status":"running","pid":342,"name":"shelldev","log":"/root/.camel/342.log","message":"shelldev
 (pid 342) already runs from this directory; a second copy was not started. In 
dev mode a changed or added file is reloaded; camel_control restart restarts 
it, for example after adding a Java class."}
   >>> camel_control {"action": "restart", "name": "shelldev"}  (5.7s)
       {"result":"Restarted shelldev as pid 409"}
       pid 409: /proc cmdline 16574 bytes; info.command()=true 
info.arguments()=Optional.empty commandLine length=4096
   >>> camel_control {"action": "stop", "name": "shelldev"}  (0.0s)
       {"result":"Graceful shutdown initiated for PID 409. The application will 
finish processing in-flight exchanges and shut down."}
   ```
   
   `camel restart` (the CLI, same wrapper), with the launcher of origin/main 
and with a launcher built from this branch:
   
   ```
   # origin/main
   cli pid 35
   pid 35: /proc cmdline 16541 bytes; info.command()=true 
info.arguments()=Optional.empty commandLine length=4096
                     allow classes to depend on preview features of this release
   To specify an argument for a long option, you can use --<name>=<value> or
   --<name> <value>.
   
   ERROR: Camel integration cli stopped right after the restart (exit code 1); 
the log is in /root/.camel/259-run.log
   
   # this branch
   cli pid 35
   pid 35: /proc cmdline 16541 bytes; info.command()=true 
info.arguments()=Optional.empty commandLine length=4096
   Stopping Camel integration: cli (PID: 35)
   Restarted Camel integration: cli (PID: 259)
   Log: /root/.camel/259-run.log
   pid 259: /proc cmdline 16563 bytes; info.command()=true 
info.arguments()=Optional.empty commandLine length=4096
   ```
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to