gnodet commented on code in PR #25317: URL: https://github.com/apache/camel/pull/25317#discussion_r3712552561
########## dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java: ########## @@ -0,0 +1,193 @@ +/* + * 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.commands.tui; + +import java.io.IOException; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.net.BindException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import dev.tamboui.backend.aesh.AeshBackend; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.group.ChannelGroup; +import io.netty.channel.group.DefaultChannelGroup; +import io.netty.channel.nio.NioIoHandler; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.stream.ChunkedWriteHandler; +import io.netty.util.concurrent.ImmediateEventExecutor; +import org.aesh.terminal.Connection; +import org.aesh.terminal.http.netty.HttpRequestHandler; +import org.aesh.terminal.http.netty.TtyWebSocketFrameHandler; +import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; + +/** + * Serves the Camel TUI dashboard to a web browser over WebSocket, using Aesh's HTTP/WebSocket terminal bridge. + * <p> + * Each incoming connection gets its own {@link CamelMonitor} instance (running the same live-monitoring logic as a + * local terminal session) driven by an {@link AeshBackend} wrapping that connection. + * <p> + * Binds to 127.0.0.1 only for security. + */ +class TuiWebServer { + + private static final Logger LOG = System.getLogger(TuiWebServer.class.getName()); + private final int port; + private final CamelJBangMain main; + private final ClassLoader classLoader; + private final String name; + private final long refreshInterval; + private final String theme; + private final ChannelGroup channels = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE); + private final EventLoopGroup bossGroup = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()); + private final EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); + private final ExecutorService sessionExecutor = Executors.newCachedThreadPool(r -> { Review Comment: Nit: `newCachedThreadPool()` creates an unbounded thread pool — each browser connection spawns a new thread running `CamelMonitor.call()`. While mitigated by loopback-only binding, a bounded pool (e.g., `newFixedThreadPool(Math.max(4, availableProcessors() * 2))`) would be more defensive and signals the expected concurrency to future readers. ########## dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java: ########## @@ -0,0 +1,193 @@ +/* + * 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.commands.tui; + +import java.io.IOException; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.net.BindException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import dev.tamboui.backend.aesh.AeshBackend; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.group.ChannelGroup; +import io.netty.channel.group.DefaultChannelGroup; +import io.netty.channel.nio.NioIoHandler; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.stream.ChunkedWriteHandler; +import io.netty.util.concurrent.ImmediateEventExecutor; +import org.aesh.terminal.Connection; +import org.aesh.terminal.http.netty.HttpRequestHandler; +import org.aesh.terminal.http.netty.TtyWebSocketFrameHandler; +import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; + +/** + * Serves the Camel TUI dashboard to a web browser over WebSocket, using Aesh's HTTP/WebSocket terminal bridge. + * <p> + * Each incoming connection gets its own {@link CamelMonitor} instance (running the same live-monitoring logic as a + * local terminal session) driven by an {@link AeshBackend} wrapping that connection. + * <p> + * Binds to 127.0.0.1 only for security. + */ +class TuiWebServer { + + private static final Logger LOG = System.getLogger(TuiWebServer.class.getName()); + private final int port; + private final CamelJBangMain main; + private final ClassLoader classLoader; + private final String name; + private final long refreshInterval; + private final String theme; + private final ChannelGroup channels = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE); + private final EventLoopGroup bossGroup = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()); + private final EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); + private final ExecutorService sessionExecutor = Executors.newCachedThreadPool(r -> { + Thread t = new Thread(r, "tui-web-session"); + t.setDaemon(true); + return t; + }); + private Channel serverChannel; + private boolean stopped; + + TuiWebServer(int port, CamelJBangMain main, ClassLoader classLoader, String name, long refreshInterval, + String theme) { + this.port = port; + this.main = main; + this.classLoader = classLoader; + this.name = name; + this.refreshInterval = refreshInterval; + this.theme = theme; + } + + void start() throws IOException { + try { + ServerBootstrap bootstrap = new ServerBootstrap(); + serverChannel = bootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new WebServerInitializer()) + .bind("127.0.0.1", port) + .sync() + .channel(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + stop(); + throw new IOException("Interrupted while starting web terminal server", e); + } catch (Exception e) { + stop(); + Throwable cause = e.getCause(); + if (cause instanceof BindException bindException) { + throw bindException; + } + if (e instanceof BindException bindException) { + throw bindException; + } + throw new IOException("Failed to start web terminal server", e); + } + } + + synchronized void stop() { + if (stopped) { + return; + } + stopped = true; + if (serverChannel != null) { + serverChannel.close().syncUninterruptibly(); + } + channels.close().syncUninterruptibly(); + bossGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS).syncUninterruptibly(); + workerGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS).syncUninterruptibly(); + sessionExecutor.shutdownNow(); + } + + boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { Review Comment: Minor: `awaitTermination()` checks only the Netty boss/worker event loops but does not wait on `sessionExecutor`. After `stop()` calls `sessionExecutor.shutdownNow()`, this method can return `true` while `CamelMonitor` session threads are still unwinding. Consider adding `sessionExecutor.awaitTermination(remaining, unit)` with the remaining deadline. ########## dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java: ########## @@ -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 (java.net.BindException e) { Review Comment: Style nit: uses FQCN `java.net.BindException` instead of importing it. The project convention is to always use imports with simple class names (no FQCNs). Note: this follows the same pattern as the pre-existing MCP server catch block above — both could be cleaned up. -- 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]
