-
Notifications
You must be signed in to change notification settings - Fork 5.1k
CAMEL-24202: camel-tui accessible from a web browser via --web #25317
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
75cbf03
cf51090
c3810ff
16f3d63
7fc6356
276166d
51bce14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -641,6 +641,17 @@ be closed as `not a vulnerability`. | |
| "MBean operation X executes code or sends to endpoint Y when invoked | ||
| from a JMX or Jolokia connection" describes the documented contract, | ||
| not a framework vulnerability. | ||
| * *The Camel TUI's `--mcp` and `--web` servers.* Camel TUI | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Info — looks good] Clear framing of |
||
| (`dsl/camel-jbang/camel-jbang-plugin-tui`) can optionally expose an MCP | ||
| server (`--mcp`, for AI-agent access) and a browser-accessible terminal | ||
| (`--web`, over WebSocket). Both are management surfaces under the same | ||
| framing as above: opt-in (off unless the flag is passed), bound to | ||
| `127.0.0.1` only, and - like JMX and Jolokia without an auth layer | ||
| configured - carry no authentication of their own beyond that loopback | ||
| bind. Full interactive control of the TUI (including anything it can | ||
| shell out to, such as `docker`/`podman`/`camel run`) reachable from | ||
| `127.0.0.1` is the documented contract for both flags, not a framework | ||
| vulnerability. | ||
| * *Vulnerabilities in third-party transitive dependencies.* Camel fixes | ||
| CVEs in Camel code, not in third-party JARs; the report belongs | ||
| upstream. Camel may upgrade the dependency to pick up an upstream fix, | ||
|
|
@@ -1084,8 +1095,10 @@ YAML:: | |
| upstream broker is not inside the trust boundary. If the option is | ||
| unavoidable, install an `ObjectInputFilter`. | ||
| * *Do not expose management surfaces.* `camel-management`, the developer | ||
| console, `camel-jolokia` and JMX should listen on a loopback interface, a | ||
| sidecar, or a separate network only. | ||
| console, `camel-jolokia`, JMX, and Camel TUI's `--mcp` / `--web` servers | ||
| should listen on a loopback interface, a sidecar, or a separate network | ||
| only. The TUI servers already default to `127.0.0.1`; do not front them | ||
| with a reverse proxy that makes them reachable from a public network. | ||
| * *Keep components patched.* Pin Camel to a supported version, subscribe to | ||
| the announce list, and respond to advisories at | ||
| link:/security/[]. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
| import java.io.IOException; | ||
| import java.lang.System.Logger; | ||
| import java.lang.System.Logger.Level; | ||
| import java.net.BindException; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.util.ArrayList; | ||
|
|
@@ -40,6 +41,7 @@ | |
| import dev.tamboui.layout.Rect; | ||
| import dev.tamboui.style.Color; | ||
| import dev.tamboui.style.Style; | ||
| import dev.tamboui.terminal.Backend; | ||
| import dev.tamboui.terminal.Frame; | ||
| import dev.tamboui.text.CharWidth; | ||
| import dev.tamboui.text.Line; | ||
|
|
@@ -108,6 +110,15 @@ public class CamelMonitor extends CamelCommand { | |
| defaultValue = "8123") | ||
| int mcpPort = 8123; | ||
|
|
||
| @CommandLine.Option(names = { "--web" }, | ||
| description = "Enable browser-accessible terminal (WebSocket) server") | ||
| boolean web; | ||
|
|
||
| @CommandLine.Option(names = { "--web-port" }, | ||
| description = "Web terminal server port (default: ${DEFAULT-VALUE})", | ||
| defaultValue = "8090") | ||
| int webPort = 8090; | ||
|
|
||
| @CommandLine.Option(names = { "--theme" }, | ||
| description = "Color theme (overrides persisted preference for this session)", | ||
| completionCandidates = ThemeModeCompletionCandidates.class) | ||
|
|
@@ -127,6 +138,7 @@ public class CamelMonitor extends CamelCommand { | |
| private String lastWaveNotification; | ||
| private boolean mcpInjectedKey; | ||
| private TuiMcpServer mcpServer; | ||
| private TuiWebServer webServer; | ||
| private McpFacade mcpFacade; | ||
| private final Queue<McpFacade.PendingKey> pendingKeys = new ConcurrentLinkedQueue<>(); | ||
| private final CaptionOverlay captionOverlay = new CaptionOverlay(); | ||
|
|
@@ -141,6 +153,9 @@ public class CamelMonitor extends CamelCommand { | |
|
|
||
| private ActionsPopup actionsPopup; | ||
| private TuiRunner runner; | ||
| // Set by TuiWebServer for browser sessions; local terminal sessions leave this null | ||
| // and let TuiBackendHelper auto-detect the active terminal instead. | ||
| Backend webBackend; | ||
|
|
||
| private MonitorContext ctx; | ||
|
|
||
|
|
@@ -514,7 +529,7 @@ public void resetIntegrationTabState() { | |
| actionsPopup.setMcpEnabled(true, mcpPort, mcpServer::getConnectedClient, | ||
| mcpServer::getActivityLog, mcpServer::getToolCallCount); | ||
| mcpJsonFile = writeMcpJson(mcpPort); | ||
| } catch (java.net.BindException e) { | ||
| } catch (BindException e) { | ||
| System.err.println("MCP server failed to start: port " + mcpPort + " is already in use."); | ||
| System.err.println("Use --mcp-port to specify a different port, e.g.: camel tui --mcp --mcp-port 8124"); | ||
| mcpServer = null; | ||
|
|
@@ -523,7 +538,21 @@ public void resetIntegrationTabState() { | |
| } | ||
| aiPanel.setMcpInfo(mcp, mcpPort); | ||
|
|
||
| try (var tui = TuiBackendHelper.createTuiRunner()) { | ||
| if (web) { | ||
| webServer = new TuiWebServer(webPort, getMain(), classLoader, name, refreshInterval, theme); | ||
| try { | ||
| webServer.start(); | ||
| } catch (BindException e) { | ||
| System.err.println("Web server failed to start: port " + webPort + " is already in use."); | ||
| System.err.println("Use --web-port to specify a different port, e.g.: camel tui --web --web-port 8091"); | ||
| webServer = null; | ||
| web = false; | ||
| } | ||
| } | ||
|
|
||
| try (var tui = webBackend != null | ||
| ? TuiBackendHelper.createTuiRunner(webBackend) | ||
| : TuiBackendHelper.createTuiRunner()) { | ||
| this.runner = tui; | ||
| aiPanel.setExitCallbackForTestingOrRuntime(tui::quit); | ||
| ctx.runner = tui; | ||
|
|
@@ -539,9 +568,14 @@ public void resetIntegrationTabState() { | |
| applyLogPin(); | ||
| applyRatePer(); | ||
| applyConfirmActions(); | ||
| // Intercept Ctrl+C: quit the TUI cleanly instead of letting | ||
| // the JVM tear down the classloader while we're still running | ||
| Signal.handle(new Signal("INT"), sig -> tui.quit()); | ||
| if (webBackend == null) { | ||
| // Intercept Ctrl+C: quit the TUI cleanly instead of letting | ||
| // the JVM tear down the classloader while we're still running. | ||
| // Signal.handle is process-wide and would clobber concurrent sessions | ||
| // (e.g. browser connections via --web), so only the local terminal | ||
| // session registers it. | ||
| Signal.handle(new Signal("INT"), sig -> tui.quit()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Info — looks good] Correctly scopes |
||
| } | ||
| tui.run( | ||
| this::handleEvent, | ||
| this::render); | ||
|
|
@@ -552,6 +586,9 @@ public void resetIntegrationTabState() { | |
| if (mcpServer != null) { | ||
| mcpServer.stop(); | ||
| } | ||
| if (webServer != null) { | ||
| webServer.stop(); | ||
| } | ||
| deleteMcpJson(mcpJsonFile); | ||
| this.runner = null; | ||
| } | ||
|
|
@@ -754,6 +791,8 @@ private boolean handleGlobalKeys(KeyEvent ke, TuiRunner runner) { | |
| boolean textEditing = probeEditing || sourceSearchActive || logSearchActive || spanFilterActive | ||
| || beanFilterActive || classpathFilterActive || mavenDepsFilterActive || sqlInputActive | ||
| || catalogFilterActive || filesBrowserTextActive; | ||
| // Each session (the local terminal, or a browser tab connected via --web) owns an | ||
| // independent CamelMonitor/TuiRunner, so quitting here only ends this session. | ||
| if (!textEditing && (ke.isCharIgnoreCase('q') || ke.isCtrlC())) { | ||
| if (!ke.isCtrlC() && ctx.confirmActions) { | ||
| popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", () -> runner.quit()); | ||
|
|
@@ -1394,6 +1433,11 @@ private void renderHeader(Frame frame, Rect area) { | |
| titleSpans.add(Span.raw(" ")); | ||
| titleSpans.add(Span.styled(activeInfra + " infra(s)", Theme.notice())); | ||
| } | ||
| if (web && webBackend == null) { | ||
| titleSpans.add(Span.raw(" ")); | ||
| titleSpans.add(Span.styled("web :" + webPort, | ||
| Theme.notice().underlined().hyperlink("http://127.0.0.1:" + webPort + "/"))); | ||
| } | ||
| if (ctx.selectedPid != null) { | ||
| titleSpans.add(Span.raw(" ")); | ||
| InfraInfo selInfra = findSelectedInfra(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
| package org.apache.camel.dsl.jbang.core.commands.tui; | ||
|
|
||
| import dev.tamboui.backend.jline3.JLineBackend; | ||
| import dev.tamboui.terminal.Backend; | ||
| import dev.tamboui.tui.TuiConfig; | ||
| import dev.tamboui.tui.TuiRunner; | ||
| import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; | ||
|
|
@@ -29,10 +30,15 @@ private TuiBackendHelper() { | |
|
|
||
| static TuiRunner createTuiRunner() throws Exception { | ||
| Terminal activeTerminal = EnvironmentHelper.getActiveTerminal(); | ||
| if (activeTerminal != null) { | ||
| JLineBackend backend = new JLineBackend(activeTerminal); | ||
| return TuiRunner.create(TuiConfig.builder().backend(backend).mouseCapture(true).build()); | ||
| } | ||
| return TuiRunner.create(TuiConfig.builder().mouseCapture(true).build()); | ||
| // Build the JLine backend explicitly rather than leaving backend selection to | ||
| // TamboUI's ServiceLoader-based auto-discovery: with tamboui-aesh-backend also on the | ||
| // classpath (for --web), auto-discovery can pick AeshBackend for the local session too, | ||
| // which drives a native PosixSysTerminal that doesn't shut down cleanly here. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Info — looks good] Explicit JLine backend avoids ServiceLoader picking |
||
| JLineBackend backend = activeTerminal != null ? new JLineBackend(activeTerminal) : new JLineBackend(); | ||
| return TuiRunner.create(TuiConfig.builder().backend(backend).mouseCapture(true).build()); | ||
| } | ||
|
|
||
| static TuiRunner createTuiRunner(Backend backend) throws Exception { | ||
| return TuiRunner.create(TuiConfig.builder().backend(backend).mouseCapture(true).build()); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Low — docs] States browser sessions have "the same … keyboard shortcuts" as local terminal, but web sessions hide the
q/quit footer hint (OverviewTab) and swallowq/Ctrl+C (CamelMonitorL795–798). Consider noting that quit behaves differently in browser sessions.