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]
