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
commit 92f52bb3d6c56c3cdf42f8d6ba907cb0ea353aa3 Author: Claus Ibsen <[email protected]> AuthorDate: Fri Jul 3 17:28:59 2026 +0200 CAMEL-23831: Add --jvm-args option to camel run and TUI run dialog - Add --jvm-args CLI option for passing JVM arguments (e.g. -Xms128m -Xmx256m) - Wire into all three runtimes: JBang (--java-options), Spring Boot (-Dspring-boot.run.jvmArguments), Quarkus (-Djvm.args) - Spawn separate JVM via JBang when --jvm-args is set for Camel Main - Add Init heap and Max heap fields to TUI run dialog with placeholder hints - Add keystroke-level input validation (digits + single k/m/g suffix) - Add launch-time validation (port range, max >= init heap) - Show validation errors inline in the run dialog - Color heap chart bars individually based on heap load at each point in time - Document --jvm-args with examples in run.adoc Co-Authored-By: Claude Opus 4.6 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../pages/jbang-commands/camel-jbang-debug.adoc | 1 + .../ROOT/pages/jbang-commands/camel-jbang-dev.adoc | 1 + .../ROOT/pages/jbang-commands/camel-jbang-run.adoc | 1 + .../ROOT/partials/jbang-commands/examples/run.adoc | 14 ++ .../META-INF/camel-jbang-commands-metadata.json | 6 +- .../apache/camel/dsl/jbang/core/commands/Run.java | 40 ++++- .../dsl/jbang/core/commands/tui/ActionsPopup.java | 5 +- .../dsl/jbang/core/commands/tui/MemoryTab.java | 12 +- .../jbang/core/commands/tui/RunOptionsForm.java | 163 +++++++++++++++++++-- 9 files changed, 212 insertions(+), 31 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc index 56291aa0ad2e..8302578f9113 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc @@ -40,6 +40,7 @@ camel debug [options] | `--java-version,--java` | Java version (21, 25) | 21 | String | `--jfr` | Enables Java Flight Recorder saving recording to disk on exit | false | boolean | `--jfr-profile` | Java Flight Recorder profile to use (such as default or profile) | | String +| `--jvm-args` | Additional JVM arguments (e.g. -Xmx256m -Xms128m) | | String | `--jvm-debug` | To enable JVM remote debugging on port 4004 by default. The supported values are true to enable the remote debugging, false to disable the remote debugging or a number to use a custom port | | int | `--kamelets-version` | Apache Camel Kamelets version (auto-detected from classpath if not set) | | String | `--lazy-bean` | Whether to use lazy bean initialization (can help with complex classloading issues) | false | boolean diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc index 271834e0c8c0..7e2fa362fbb3 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc @@ -38,6 +38,7 @@ camel dev [options] | `--java-version,--java` | Java version (21, 25) | 21 | String | `--jfr` | Enables Java Flight Recorder saving recording to disk on exit | false | boolean | `--jfr-profile` | Java Flight Recorder profile to use (such as default or profile) | | String +| `--jvm-args` | Additional JVM arguments (e.g. -Xmx256m -Xms128m) | | String | `--jvm-debug` | To enable JVM remote debugging on port 4004 by default. The supported values are true to enable the remote debugging, false to disable the remote debugging or a number to use a custom port | | int | `--kamelets-version` | Apache Camel Kamelets version (auto-detected from classpath if not set) | | String | `--lazy-bean` | Whether to use lazy bean initialization (can help with complex classloading issues) | false | boolean diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc index 79eba60ebcc2..554c5aab82b5 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc @@ -38,6 +38,7 @@ camel run [options] | `--java-version,--java` | Java version (21, 25) | 21 | String | `--jfr` | Enables Java Flight Recorder saving recording to disk on exit | false | boolean | `--jfr-profile` | Java Flight Recorder profile to use (such as default or profile) | | String +| `--jvm-args` | Additional JVM arguments (e.g. -Xmx256m -Xms128m) | | String | `--jvm-debug` | To enable JVM remote debugging on port 4004 by default. The supported values are true to enable the remote debugging, false to disable the remote debugging or a number to use a custom port | | int | `--kamelets-version` | Apache Camel Kamelets version (auto-detected from classpath if not set) | | String | `--lazy-bean` | Whether to use lazy bean initialization (can help with complex classloading issues) | false | boolean diff --git a/docs/user-manual/modules/ROOT/partials/jbang-commands/examples/run.adoc b/docs/user-manual/modules/ROOT/partials/jbang-commands/examples/run.adoc index 9610a6b09f77..e6eb95721105 100644 --- a/docs/user-manual/modules/ROOT/partials/jbang-commands/examples/run.adoc +++ b/docs/user-manual/modules/ROOT/partials/jbang-commands/examples/run.adoc @@ -27,3 +27,17 @@ Run with additional dependencies: ---- camel run hello.java --dep=camel-jackson ---- + +Run with custom JVM memory settings (init and max heap): + +[source,bash] +---- +camel run hello.java --jvm-args="-Xms128m -Xmx256m" +---- + +Run with max heap only: + +[source,bash] +---- +camel run hello.java --jvm-args="-Xmx512m" +---- 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 48228ac50fbe..d30315094616 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 @@ -6,9 +6,9 @@ { "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 [...] + { "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 [...] { "name": "dependency", "fullName": "dependency", "description": "Displays all Camel dependencies required to run", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.DependencyCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "copy", "fullName": "dependency copy", "description": "Copies all Camel dependencies required to run to a specific directory", "sourc [...] - { "name": "dev", "fullName": "dev", "description": "Run in dev mode with live reload", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Dev", "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": "dev", "fullName": "dev", "description": "Run in dev mode with live reload", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Dev", "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": "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": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, @@ -26,7 +26,7 @@ { "name": "plugin", "fullName": "plugin", "description": "Manage plugins that add sub-commands to this CLI", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.plugin.PluginCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "add", "fullName": "plugin add", "description": "Add new plugin", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.plugin.PluginA [...] { "name": "ps", "fullName": "ps", "description": "List running Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.ListProcess", "options": [ { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "--pid", "description": "List only pid in the output", "javaType": "boolean", "type": "boolean" }, { "names": "--remote", "description": "Break down counters into remote\/total pairs", "javaType": [...] { "name": "restart", "fullName": "restart", "description": "Restarts running Camel integrations (stop + re-launch)", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.RestartProcess", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, - { "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": "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": "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" } ] }, diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java index b3336b687eb9..230163bbb11e 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java @@ -261,6 +261,10 @@ public class Run extends CamelCommand { "enable the remote debugging, false to disable the remote debugging or a number to use a custom port") int jvmDebugPort; + @Option(names = { "--jvm-args" }, + description = "Additional JVM arguments (e.g. -Xmx256m -Xms128m)") + String jvmArgs; + @Option(names = { "--name" }, defaultValue = "CamelJBang", description = "The name of the Camel application") String name; @@ -558,7 +562,8 @@ public class Run extends CamelCommand { } private boolean isDebugMode() { - return jvmDebugPort > 0 || debugOptions.openTelemetryAgent; + return jvmDebugPort > 0 || debugOptions.openTelemetryAgent + || (jvmArgs != null && !jvmArgs.isBlank()); } private void writeSetting(KameletMain main, Properties existing, String key, String value) { @@ -1398,9 +1403,17 @@ public class Run extends CamelCommand { mvnw = "/mvnw.cmd"; } ProcessBuilder pb = new ProcessBuilder(); - pb.command(runDirPath + mvnw, "--quiet", "--file", - runDirPath.toRealPath().resolve("pom.xml").toString(), "package", - "quarkus:" + (dev ? "dev" : "run")); + List<String> mvnCmd = new ArrayList<>(); + mvnCmd.add(runDirPath + mvnw); + mvnCmd.add("--quiet"); + mvnCmd.add("--file"); + mvnCmd.add(runDirPath.toRealPath().resolve("pom.xml").toString()); + if (jvmArgs != null && !jvmArgs.isBlank()) { + mvnCmd.add("-Djvm.args=" + jvmArgs.trim()); + } + mvnCmd.add("package"); + mvnCmd.add("quarkus:" + (dev ? "dev" : "run")); + pb.command(mvnCmd); pb.inheritIO(); // run in foreground (with IO so logs are visible) Process p = pb.start(); @@ -1522,9 +1535,16 @@ public class Run extends CamelCommand { if (FileUtil.isWindows()) { mvnw = "/mvnw.cmd"; } - pb.command(runDirPath + mvnw, "--quiet", "--file", - runDirPath.toRealPath().resolve("pom.xml").toString(), - "spring-boot:run"); + List<String> mvnCmd = new ArrayList<>(); + mvnCmd.add(runDirPath + mvnw); + mvnCmd.add("--quiet"); + mvnCmd.add("--file"); + mvnCmd.add(runDirPath.toRealPath().resolve("pom.xml").toString()); + if (jvmArgs != null && !jvmArgs.isBlank()) { + mvnCmd.add("-Dspring-boot.run.jvmArguments=" + jvmArgs.trim()); + } + mvnCmd.add("spring-boot:run"); + pb.command(mvnCmd); pb.inheritIO(); // run in foreground (with IO so logs are visible) Process p = pb.start(); @@ -1728,6 +1748,12 @@ public class Run extends CamelCommand { jbangArgs.add("--debug=" + jvmDebugPort); // jbang --debug=port cmds.removeIf(arg -> arg.startsWith("--jvm-debug")); } + if (jvmArgs != null && !jvmArgs.isBlank()) { + for (String arg : jvmArgs.trim().split("\\s+")) { + jbangArgs.add("--java-options=" + arg); + } + cmds.removeIf(a -> a.startsWith("--jvm-args")); + } if (debugOptions.openTelemetryAgent) { boolean jaegerExport = "jaeger".equals(debugOptions.openTelemetryAgentExport); jbangArgs.add("--javaagent=io.opentelemetry.javaagent:opentelemetry-javaagent:RELEASE"); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java index 6a85b2f3d91d..5f5373b39c45 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java @@ -540,7 +540,10 @@ class ActionsPopup { showExampleBrowser = true; } } else if (ke.isConfirm()) { - if (selectedFolder != null) { + String error = runOptionsForm.validate(); + if (error != null) { + runOptionsForm.setError(error); + } else if (selectedFolder != null) { launchFolder(); } else { launchWithName(); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java index a3a9acaabfc3..529cfaf9adc3 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java @@ -266,9 +266,6 @@ class MemoryTab extends AbstractTab { ceiling = observedMax; } - long pct = ceiling > 0 ? info.heapMemUsed * 100 / ceiling : 0; - Color barColor = pct >= 80 ? Color.LIGHT_RED : pct >= 60 ? Color.YELLOW : Color.GREEN; - String title = String.format(" Heap Usage (%s / %s committed) ", formatBytes(info.heapMemUsed), formatBytes(ceiling)); // Render the block border first @@ -291,11 +288,14 @@ class MemoryTab extends AbstractTab { data[dataOffset + (i - startIdx)] = hist.get(i); } - // Render multi-row bar chart into the buffer + // Render multi-row bar chart with per-column color based on heap load at that point Buffer buf = frame.buffer(); - Style barStyle = Style.EMPTY.fg(barColor); for (int col = 0; col < chartW; col++) { + long colPct = ceiling > 0 ? data[col] * 100 / ceiling : 0; + Color colColor = colPct >= 80 ? Color.LIGHT_RED : colPct >= 60 ? Color.YELLOW : Color.GREEN; + Style colStyle = Style.EMPTY.fg(colColor); + double ratio = (double) data[col] / ceiling; // Total eighths this column fills (chartH rows * 8 eighths per row) double fillEighths = ratio * chartH * 8.0; @@ -307,7 +307,7 @@ class MemoryTab extends AbstractTab { int x = inner.x() + col; int rowEighths = Math.min(8, Math.max(0, totalEighths - row * 8)); if (rowEighths > 0) { - buf.setString(x, y, BAR_EIGHTHS[rowEighths], barStyle); + buf.setString(x, y, BAR_EIGHTHS[rowEighths], colStyle); } } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java index 967947065946..77e395313c09 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java @@ -47,18 +47,21 @@ class RunOptionsForm { private static final int ROW_RUNTIME = 1; private static final int ROW_PROFILE = 2; private static final int ROW_PORT = 3; - private static final int ROW_MAX = 4; - private static final int ROW_CONSOLE = 5; - private static final int ROW_DEV = 6; - private static final int ROW_OBSERVE = 7; - private static final int ROW_TRACE = 8; - private static final int ROW_STUB = 9; - private static final int ROW_OTEL_AGENT = 10; - private static final int ROW_COUNT = 11; + private static final int ROW_INIT_HEAP = 4; + private static final int ROW_MAX_HEAP = 5; + private static final int ROW_MAX = 6; + private static final int ROW_CONSOLE = 7; + private static final int ROW_DEV = 8; + private static final int ROW_OBSERVE = 9; + private static final int ROW_TRACE = 10; + private static final int ROW_STUB = 11; + private static final int ROW_OTEL_AGENT = 12; + private static final int ROW_COUNT = 13; private boolean visible; private int page; private int selectedRow; + private String errorMessage; private static final String[] MAX_MODES = { "Max seconds:", "Max messages:", "Max idle secs:" }; private static final String[] MAX_FLAGS = { "--max-seconds=", "--max-messages=", "--max-idle-seconds=" }; @@ -70,6 +73,8 @@ class RunOptionsForm { // Text fields private TextInputState nameInput; private TextInputState portInput; + private TextInputState initHeapInput; + private TextInputState maxHeapInput; private TextInputState maxInput; private int maxMode; private int runtimeMode; @@ -102,6 +107,8 @@ class RunOptionsForm { void open(String defaultName, String exampleName, boolean bundled, boolean dev) { nameInput = new TextInputState(defaultName != null ? defaultName : ""); portInput = new TextInputState(""); + initHeapInput = new TextInputState(""); + maxHeapInput = new TextInputState(""); maxInput = new TextInputState(""); maxMode = 0; runtimeMode = 0; @@ -133,6 +140,10 @@ class RunOptionsForm { return stubMode; } + void setError(String error) { + this.errorMessage = error; + } + boolean isJaegerExport() { return otelAgent && otelExportTarget == 1; } @@ -204,6 +215,21 @@ class RunOptionsForm { if (!port.isEmpty()) { args.add("--port=" + port); } + StringBuilder jvmArgs = new StringBuilder(); + String initHeap = initHeapInput.text().trim(); + if (!initHeap.isEmpty()) { + jvmArgs.append("-Xms").append(initHeap); + } + String maxHeap = maxHeapInput.text().trim(); + if (!maxHeap.isEmpty()) { + if (!jvmArgs.isEmpty()) { + jvmArgs.append(" "); + } + jvmArgs.append("-Xmx").append(maxHeap); + } + if (!jvmArgs.isEmpty()) { + args.add("--jvm-args=" + jvmArgs); + } String maxVal = maxInput.text().trim(); if (!maxVal.isEmpty() && !"0".equals(maxVal)) { args.add(MAX_FLAGS[maxMode] + maxVal); @@ -247,6 +273,7 @@ class RunOptionsForm { // ---- Options page (page 0) ---- private boolean handleOptionsPage(KeyEvent ke) { + errorMessage = null; if (ke.isUp()) { selectedRow = (selectedRow - 1 + ROW_COUNT) % ROW_COUNT; return true; @@ -336,7 +363,11 @@ class RunOptionsForm { if (selectedRow <= ROW_MAX && selectedRow != ROW_RUNTIME && selectedRow != ROW_PROFILE) { TextInputState active = activeInput(); if (active != null) { - handleTextInput(ke, active, selectedRow == ROW_PORT || selectedRow == ROW_MAX); + if (selectedRow == ROW_INIT_HEAP || selectedRow == ROW_MAX_HEAP) { + handleHeapInput(ke, active); + } else { + handleTextInput(ke, active, selectedRow == ROW_PORT || selectedRow == ROW_MAX); + } } return true; } @@ -444,7 +475,7 @@ class RunOptionsForm { private void renderOptionsPage(Frame frame, Rect area) { int popupW = Math.min(64, area.width() - 4); - int popupH = 15; + int popupH = errorMessage != null ? 18 : 17; int x = area.left() + Math.max(0, (area.width() - popupW) / 2); int y = area.top() + Math.max(0, (area.height() - popupH) / 4); Rect popup = new Rect(x, y, Math.min(popupW, area.width()), Math.min(popupH, area.height())); @@ -466,7 +497,7 @@ class RunOptionsForm { int innerX = popup.left() + 2; int innerW = popup.width() - 4; - int labelW = 16; + int labelW = 18; int fieldW = innerW - labelW; int rowY = popup.top() + 1; @@ -486,6 +517,16 @@ class RunOptionsForm { renderTextInput(frame, innerX + labelW, rowY, fieldW, portInput, selectedRow == ROW_PORT); rowY++; + renderLabel(frame, innerX, rowY, labelW, "Init heap (-Xms):", selectedRow == ROW_INIT_HEAP); + renderTextInputWithHint(frame, innerX + labelW, rowY, fieldW, initHeapInput, selectedRow == ROW_INIT_HEAP, + "e.g. 128m, 1g"); + rowY++; + + renderLabel(frame, innerX, rowY, labelW, "Max heap (-Xmx):", selectedRow == ROW_MAX_HEAP); + renderTextInputWithHint(frame, innerX + labelW, rowY, fieldW, maxHeapInput, selectedRow == ROW_MAX_HEAP, + "e.g. 256m, 2g"); + rowY++; + renderLabel(frame, innerX, rowY, labelW, MAX_MODES[maxMode], selectedRow == ROW_MAX); renderTextInput(frame, innerX + labelW, rowY, fieldW, maxInput, selectedRow == ROW_MAX); rowY++; @@ -519,6 +560,12 @@ class RunOptionsForm { Span.styled(" ", Style.EMPTY), Span.styled(jaegerLabel, jaegerStyle))), exportArea); } + if (errorMessage != null) { + rowY++; + Rect errorArea = new Rect(innerX, rowY, innerW, 1); + frame.renderWidget(Paragraph.from(Line.from( + Span.styled("⚠ " + errorMessage, Style.EMPTY.bold()))), errorArea); + } } private void renderPropertiesPage(Frame frame, Rect area) { @@ -638,11 +685,89 @@ class RunOptionsForm { return switch (selectedRow) { case ROW_NAME -> nameInput; case ROW_PORT -> portInput; + case ROW_INIT_HEAP -> initHeapInput; + case ROW_MAX_HEAP -> maxHeapInput; case ROW_MAX -> maxInput; default -> null; }; } + private void handleHeapInput(KeyEvent ke, TextInputState active) { + if (ke.isDeleteBackward()) { + active.deleteBackward(); + } else if (ke.isDeleteForward()) { + active.deleteForward(); + } else if (ke.isLeft()) { + active.moveCursorLeft(); + } else if (ke.isRight()) { + active.moveCursorRight(); + } else if (ke.isHome()) { + active.moveCursorToStart(); + } else if (ke.isEnd()) { + active.moveCursorToEnd(); + } else if (ke.code() == KeyCode.CHAR) { + char c = ke.string().charAt(0); + String text = active.text(); + if (Character.isDigit(c)) { + // only allow digits before any suffix + if (text.isEmpty() || Character.isDigit(text.charAt(text.length() - 1))) { + active.insert(c); + } + } else if ((c == 'k' || c == 'm' || c == 'g') && !text.isEmpty() + && Character.isDigit(text.charAt(text.length() - 1))) { + active.insert(c); + } + } + } + + String validate() { + String port = portInput.text().trim(); + if (!port.isEmpty()) { + try { + int p = Integer.parseInt(port); + if (p < 0 || p > 65535) { + return "Port must be 0-65535"; + } + } catch (NumberFormatException e) { + return "Invalid port: " + port; + } + } + String initHeap = initHeapInput.text().trim(); + String maxHeap = maxHeapInput.text().trim(); + if (!initHeap.isEmpty() && !isValidHeap(initHeap)) { + return "Invalid init heap: " + initHeap; + } + if (!maxHeap.isEmpty() && !isValidHeap(maxHeap)) { + return "Invalid max heap: " + maxHeap; + } + if (!initHeap.isEmpty() && !maxHeap.isEmpty()) { + long initBytes = parseHeapBytes(initHeap); + long maxBytes = parseHeapBytes(maxHeap); + if (initBytes > maxBytes) { + return "Init heap cannot exceed max heap"; + } + } + return null; + } + + private static boolean isValidHeap(String value) { + return value.matches("\\d+[kmg]?"); + } + + private static long parseHeapBytes(String value) { + char last = value.charAt(value.length() - 1); + if (Character.isDigit(last)) { + return Long.parseLong(value); + } + long num = Long.parseLong(value.substring(0, value.length() - 1)); + return switch (last) { + case 'k' -> num * 1024; + case 'm' -> num * 1024 * 1024; + case 'g' -> num * 1024 * 1024 * 1024; + default -> num; + }; + } + private void handleTextInput(KeyEvent ke, TextInputState active, boolean digitsOnly) { if (ke.isDeleteBackward()) { active.deleteBackward(); @@ -674,6 +799,11 @@ class RunOptionsForm { } private void renderTextInput(Frame frame, int x, int y, int w, TextInputState state, boolean active) { + renderTextInputWithHint(frame, x, y, w, state, active, null); + } + + private void renderTextInputWithHint( + Frame frame, int x, int y, int w, TextInputState state, boolean active, String hint) { Rect inputArea = new Rect(x, y, w, 1); if (active) { TextInput textInput = TextInput.builder() @@ -682,9 +812,14 @@ class RunOptionsForm { frame.renderStatefulWidget(textInput, inputArea, state); } else { String text = state.text(); - Style style = text.isEmpty() ? Style.EMPTY.dim() : Style.EMPTY; - frame.renderWidget(Paragraph.from(Line.from( - Span.styled(text.isEmpty() ? "—" : text, style))), inputArea); + if (text.isEmpty() && hint != null) { + frame.renderWidget(Paragraph.from(Line.from( + Span.styled(hint, Style.EMPTY.dim()))), inputArea); + } else { + Style style = text.isEmpty() ? Style.EMPTY.dim() : Style.EMPTY; + frame.renderWidget(Paragraph.from(Line.from( + Span.styled(text.isEmpty() ? "—" : text, style))), inputArea); + } } }
