From 8cf60713390b834ad6be9714f30bc803c2fcb0b8 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Wed, 20 May 2026 16:31:28 +0200 Subject: [PATCH 1/5] CAMEL-23569: camel-tui: Add support for camel-test-infra services Add infrastructure service management to the TUI monitor including: - Discover running infra services by scanning ~/.camel/infra-*.json files - Show infra services in a separate table below integrations in the overview - Display connection details (host, port, credentials) in the info panel - Support infra service logs in the Log tab - Add stop/kill (x/X keys) for both infra services and integrations Co-Authored-By: Claude Opus 4.6 --- .../jbang/core/commands/tui/CamelMonitor.java | 440 ++++++++++++++++-- 1 file changed, 401 insertions(+), 39 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java index 805ee13277379..c5f084fad0f59 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java @@ -36,6 +36,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -163,8 +164,12 @@ public class CamelMonitor extends CamelCommand { // State private final AtomicReference> data = new AtomicReference<>(Collections.emptyList()); + private final AtomicReference> infraData = new AtomicReference<>(Collections.emptyList()); private final Map vanishing = new ConcurrentHashMap<>(); + private final Map vanishingInfra = new ConcurrentHashMap<>(); private final TableState overviewTableState = new TableState(); + private final TableState infraTableState = new TableState(); + private boolean infraTableFocused; private final TableState routeTableState = new TableState(); private final TableState consumerTableState = new TableState(); private final TableState healthTableState = new TableState(); @@ -439,6 +444,11 @@ private boolean handleEvent(Event event, TuiRunner runner) { tabsState.select(TAB_OVERVIEW); return true; } + if (infraTableFocused) { + infraTableFocused = false; + syncSelectedPidFromOverview(); + return true; + } if (selectedPid != null) { selectedPid = null; return true; @@ -625,8 +635,8 @@ private boolean handleEvent(Event event, TuiRunner runner) { chartMode = (chartMode + 1) % 3; return true; } - // Overview tab: start/stop all routes for selected integration - if (tab == TAB_OVERVIEW && ke.isChar('p') && selectedPid != null) { + // Overview tab: start/stop all routes for selected integration (not infra) + if (tab == TAB_OVERVIEW && ke.isChar('p') && selectedPid != null && !infraTableFocused) { IntegrationInfo selInfo = findSelectedIntegration(); if (selInfo != null) { String cmd = selInfo.routeStarted > 0 ? "stop" : "start"; @@ -634,6 +644,16 @@ private boolean handleEvent(Event event, TuiRunner runner) { } return true; } + // Overview tab: stop process (SIGTERM) for selected integration or infra + if (tab == TAB_OVERVIEW && ke.isChar('x') && selectedPid != null) { + stopSelectedProcess(false); + return true; + } + // Overview tab: kill process (SIGKILL) for selected integration or infra + if (tab == TAB_OVERVIEW && ke.isChar('X') && selectedPid != null) { + stopSelectedProcess(true); + return true; + } // Consumers tab: sort if (tab == TAB_CONSUMERS && ke.isChar('s')) { @@ -1038,12 +1058,20 @@ private void selectCurrentIntegration() { if (selectedPid != null) { return; } - List infos = sortedOverviewInfos(); - Integer sel = overviewTableState.selected(); - if (sel != null && sel >= 0 && sel < infos.size()) { - selectedPid = infos.get(sel).pid; - } else if (infos.size() == 1) { - selectedPid = infos.get(0).pid; + if (infraTableFocused) { + List infras = infraData.get(); + Integer sel = infraTableState.selected(); + if (sel != null && sel >= 0 && sel < infras.size()) { + selectedPid = infras.get(sel).pid; + } + } else { + List infos = sortedOverviewInfos(); + Integer sel = overviewTableState.selected(); + if (sel != null && sel >= 0 && sel < infos.size()) { + selectedPid = infos.get(sel).pid; + } else if (infos.size() == 1) { + selectedPid = infos.get(0).pid; + } } } @@ -1062,6 +1090,19 @@ private void syncSelectedPidFromOverview() { } } + private void syncSelectedPidFromInfra() { + List infras = infraData.get(); + Integer sel = infraTableState.selected(); + String newPid = null; + if (sel != null && sel >= 0 && sel < infras.size()) { + newPid = infras.get(sel).pid; + } + if (newPid != null && !newPid.equals(selectedPid)) { + selectedPid = newPid; + resetIntegrationTabState(); + } + } + // NOTE: When adding a new tab, reset its view state here too so switching integrations // on the Overview always shows a clean slate for the newly selected integration. private void resetIntegrationTabState() { @@ -1105,8 +1146,23 @@ private void resetIntegrationTabState() { private void navigateUp() { switch (tabsState.selected()) { case TAB_OVERVIEW -> { - overviewTableState.selectPrevious(); - syncSelectedPidFromOverview(); + if (infraTableFocused) { + Integer sel = infraTableState.selected(); + if (sel != null && sel <= 0) { + infraTableFocused = false; + List intInfos = sortedOverviewInfos(); + if (!intInfos.isEmpty()) { + overviewTableState.select(intInfos.size() - 1); + } + syncSelectedPidFromOverview(); + } else { + infraTableState.selectPrevious(); + syncSelectedPidFromInfra(); + } + } else { + overviewTableState.selectPrevious(); + syncSelectedPidFromOverview(); + } } case TAB_ROUTES -> routeTableState.selectPrevious(); case TAB_CIRCUIT_BREAKER -> cbTableState.selectPrevious(); @@ -1132,11 +1188,24 @@ private void navigateUp() { } private void navigateDown() { - List infos = data.get().stream().filter(i -> !i.vanishing).toList(); switch (tabsState.selected()) { case TAB_OVERVIEW -> { - overviewTableState.selectNext(sortedOverviewInfos().size()); - syncSelectedPidFromOverview(); + if (infraTableFocused) { + List infraInfos = infraData.get(); + infraTableState.selectNext(infraInfos.size()); + syncSelectedPidFromInfra(); + } else { + List overviewInfos = sortedOverviewInfos(); + Integer sel = overviewTableState.selected(); + if (sel != null && sel >= overviewInfos.size() - 1 && !infraData.get().isEmpty()) { + infraTableFocused = true; + infraTableState.select(0); + syncSelectedPidFromInfra(); + } else { + overviewTableState.selectNext(overviewInfos.size()); + syncSelectedPidFromOverview(); + } + } } case TAB_ROUTES -> { IntegrationInfo info = findSelectedIntegration(); @@ -1204,6 +1273,11 @@ private void renderHeader(Frame frame, Rect area) { titleSpans.add(Span.styled(camelVersion != null ? "v" + camelVersion : "", Style.EMPTY.fg(Color.GREEN))); titleSpans.add(Span.raw(" ")); titleSpans.add(Span.styled(activeCount + " integration(s)", Style.EMPTY.fg(Color.CYAN))); + long activeInfra = infraData.get().stream().filter(i -> !i.vanishing).count(); + if (activeInfra > 0) { + titleSpans.add(Span.raw(" ")); + titleSpans.add(Span.styled(activeInfra + " infra", Style.EMPTY.fg(Color.MAGENTA))); + } if (selectedPid != null) { titleSpans.add(Span.raw(" ")); titleSpans.add(Span.styled("selected: " + selectedName(), Style.EMPTY.fg(Color.YELLOW))); @@ -1349,9 +1423,10 @@ private void renderContent(Frame frame, Rect area) { private void renderOverview(Frame frame, Rect area) { List infos = sortedOverviewInfos(); + List infraInfos = infraData.get(); // Keep the table selection index tracking the same PID across sort changes and data refreshes - if (selectedPid != null) { + if (selectedPid != null && !infraTableFocused) { for (int i = 0; i < infos.size(); i++) { if (selectedPid.equals(infos.get(i).pid)) { overviewTableState.select(i); @@ -1359,17 +1434,31 @@ private void renderOverview(Frame frame, Rect area) { } } } + if (selectedPid != null && infraTableFocused) { + for (int i = 0; i < infraInfos.size(); i++) { + if (selectedPid.equals(infraInfos.get(i).pid)) { + infraTableState.select(i); + break; + } + } + } - // Split: table (fill) + chart (14 rows: 13 chart + 1 x-axis) if we have data and chart is on + // Split: integrations (fill) + infra table (if present) + chart (14 rows) boolean hasSparkline = chartMode != CHART_OFF && !throughputHistory.isEmpty(); - List chunks; + boolean hasInfra = !infraInfos.isEmpty(); + // infra table height: header(1) + borders(2) + rows (capped at 6) + int infraHeight = hasInfra ? Math.min(infraInfos.size(), 6) + 3 : 0; + List constraints = new ArrayList<>(); + constraints.add(Constraint.fill()); + if (hasInfra) { + constraints.add(Constraint.length(infraHeight)); + } if (hasSparkline) { - chunks = Layout.vertical() - .constraints(Constraint.fill(), Constraint.length(14)) - .split(area); - } else { - chunks = List.of(area); + constraints.add(Constraint.length(14)); } + List chunks = Layout.vertical() + .constraints(constraints) + .split(area); // Integration table List rows = new ArrayList<>(); @@ -1439,6 +1528,9 @@ private void renderOverview(Frame frame, Rect area) { rightCell("INFLIGHT", 8, Style.EMPTY.bold()), Cell.from(Span.styled("SINCE-LAST", Style.EMPTY.bold()))); + Style integrationHighlight = infraTableFocused + ? Style.EMPTY.fg(Color.WHITE).dim() + : Style.EMPTY.fg(Color.WHITE).bold().onBlue(); Table table = Table.builder() .rows(rows) .header(header) @@ -1455,16 +1547,21 @@ private void renderOverview(Frame frame, Rect area) { Constraint.length(6), Constraint.length(8), Constraint.length(12)) - .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue()) + .highlightStyle(integrationHighlight) .highlightSpacing(Table.HighlightSpacing.ALWAYS) .block(Block.builder().borderType(BorderType.ROUNDED).title(" Integrations ").build()) .build(); frame.renderStatefulWidget(table, chunks.get(0), overviewTableState); + // Infrastructure services table + if (hasInfra) { + renderInfraTable(frame, chunks.get(1), infraInfos); + } + // Split green/red throughput bar chart with Y and X axes if (hasSparkline && chunks.size() > 1) { - Rect chartTotalArea = chunks.get(1); + Rect chartTotalArea = chunks.get(chunks.size() - 1); // Split chart area horizontally: bar chart (fill) + info panel (30 cols) List chartHSplit = Layout.horizontal() @@ -1611,6 +1708,13 @@ private void renderOverview(Frame frame, Rect area) { } private void renderOverviewInfoPanel(Frame frame, Rect area) { + // Check if an infra service is selected — show connection details instead + InfraInfo infraSel = infraTableFocused ? findSelectedInfra() : null; + if (infraSel != null) { + renderInfraInfoPanel(frame, area, infraSel); + return; + } + IntegrationInfo sel = findSelectedIntegration(); // Fall back to the single active integration when nothing is explicitly selected if (sel == null) { @@ -1713,6 +1817,91 @@ private void renderOverviewInfoPanel(Frame frame, Rect area) { frame.renderWidget(Paragraph.builder().text(Text.from(lines)).build(), inner); } + // ---- Infra table (overview sub-section) ---- + + private void renderInfraTable(Frame frame, Rect area, List infraInfos) { + List infraRows = new ArrayList<>(); + for (InfraInfo info : infraInfos) { + if (info.vanishing) { + long elapsed = System.currentTimeMillis() - info.vanishStart; + float fade = 1.0f - Math.min(1.0f, (float) elapsed / VANISH_DURATION_MS); + int gray = (int) (100 * fade); + Style dimStyle = Style.EMPTY.fg(Color.indexed(232 + Math.min(gray / 4, 23))); + infraRows.add(Row.from( + Cell.from(Span.styled(info.pid, dimStyle)), + Cell.from(Span.styled(info.alias, dimStyle)), + Cell.from(Span.styled("✖ Stopped", Style.EMPTY.fg(Color.LIGHT_RED).dim())), + Cell.from(Span.styled("", dimStyle)), + Cell.from(Span.styled("", dimStyle)))); + } else { + Style statusStyle = info.alive ? Style.EMPTY.fg(Color.GREEN) : Style.EMPTY.fg(Color.LIGHT_RED); + String statusText = info.alive ? "Running" : "Stopped"; + String port = objToString(info.properties.get("getPort")); + String host = objToString(info.properties.get("getHost")); + if (host.isEmpty()) { + host = objToString(info.properties.get("getHostname")); + } + infraRows.add(Row.from( + Cell.from(info.pid), + Cell.from(Span.styled(info.alias, Style.EMPTY.fg(Color.MAGENTA))), + Cell.from(Span.styled(statusText, statusStyle)), + Cell.from(port), + Cell.from(host))); + } + } + + Row infraHeader = Row.from( + Cell.from(Span.styled("PID", Style.EMPTY.bold())), + Cell.from(Span.styled("SERVICE", Style.EMPTY.bold())), + Cell.from(Span.styled("STATUS", Style.EMPTY.bold())), + Cell.from(Span.styled("PORT", Style.EMPTY.bold())), + Cell.from(Span.styled("HOST", Style.EMPTY.bold()))); + + Style infraHighlight = infraTableFocused + ? Style.EMPTY.fg(Color.WHITE).bold().onBlue() + : Style.EMPTY.fg(Color.WHITE).dim(); + Table infraTable = Table.builder() + .rows(infraRows) + .header(infraHeader) + .widths( + Constraint.length(8), + Constraint.fill(), + Constraint.length(10), + Constraint.length(8), + Constraint.length(20)) + .highlightStyle(infraHighlight) + .highlightSpacing(Table.HighlightSpacing.ALWAYS) + .block(Block.builder().borderType(BorderType.ROUNDED).title(" Infrastructure ").build()) + .build(); + + frame.renderStatefulWidget(infraTable, area, infraTableState); + } + + private void renderInfraInfoPanel(Frame frame, Rect area, InfraInfo infra) { + Block infoBlock = Block.builder().borderType(BorderType.ROUNDED).build(); + frame.renderWidget(infoBlock, area); + Rect inner = infoBlock.inner(area); + List lines = new ArrayList<>(); + Style dim = Style.EMPTY.dim(); + lines.add(Line.from( + Span.styled("Service: ", dim), + Span.styled(infra.alias, Style.EMPTY.fg(Color.MAGENTA)))); + lines.add(Line.from(Span.raw(""))); + // Show connection properties with cleaned-up key names + for (Map.Entry e : infra.properties.entrySet()) { + String key = e.getKey(); + // Strip "get" prefix and capitalize + if (key.startsWith("get") && key.length() > 3) { + key = key.substring(3); + } + String value = String.valueOf(e.getValue()); + lines.add(Line.from( + Span.styled(key + ": ", dim), + Span.raw(TuiHelper.truncate(value, inner.width() - key.length() - 2)))); + } + frame.renderWidget(Paragraph.builder().text(Text.from(lines)).build(), inner); + } + // ---- Tab 2: Routes ---- private void renderRoutes(Frame frame, Rect area) { @@ -2855,6 +3044,39 @@ private void toggleRouteSuspendResume() { sendRouteCommand(selectedPid, route.routeId, command); } + private void stopSelectedProcess(boolean forceKill) { + if (selectedPid == null) { + return; + } + long pid; + try { + pid = Long.parseLong(selectedPid); + } catch (NumberFormatException e) { + return; + } + if (infraTableFocused) { + // For infra services: delete the JSON file to trigger graceful shutdown + InfraInfo infra = findSelectedInfra(); + if (infra != null) { + Path jsonFile = CommandLineHelper.getCamelDir() + .resolve("infra-" + infra.alias + "-" + infra.pid + ".json"); + PathUtils.deleteFile(jsonFile); + } + if (forceKill) { + ProcessHandle.of(pid).ifPresent(ProcessHandle::destroyForcibly); + } + } else { + // For integrations: signal the process directly + ProcessHandle.of(pid).ifPresent(ph -> { + if (forceKill) { + ph.destroyForcibly(); + } else { + ph.destroy(); + } + }); + } + } + private void sendRouteCommand(String pid, String routeId, String command) { JsonObject root = new JsonObject(); root.put("action", "route"); @@ -3961,7 +4183,11 @@ private void sendLoggerLevelAction(String pid, String level) { } private void readNewLogLines(String pid, List newLines) { - Path logFile = CommandLineHelper.getCamelDir().resolve(pid + ".log"); + readNewLogLinesFromFile(pid, pid + ".log", newLines); + } + + private void readNewLogLinesFromFile(String pid, String fileName, List newLines) { + Path logFile = CommandLineHelper.getCamelDir().resolve(fileName); if (!Files.exists(logFile)) { logFilePid = pid; logFilePos = -1; @@ -5099,22 +5325,28 @@ private void renderFooter(Frame frame, Rect area) { if (tab == TAB_OVERVIEW) { hint(spans, "q", "quit"); if (selectedPid != null) { - hint(spans, "Esc", "unselect"); + hint(spans, "Esc", infraTableFocused ? "integrations" : "unselect"); } hint(spans, "\u2191\u2193", "navigate"); - hint(spans, "s", "sort"); - hint(spans, "a", "chart " + switch (chartMode) { - case CHART_ALL -> "[all]"; - case CHART_SINGLE -> "[single]"; - default -> "[off]"; - }); + if (!infraTableFocused) { + hint(spans, "s", "sort"); + hint(spans, "a", "chart " + switch (chartMode) { + case CHART_ALL -> "[all]"; + case CHART_SINGLE -> "[single]"; + default -> "[off]"; + }); + } hint(spans, "Enter", "details"); - if (selectedPid != null) { + if (selectedPid != null && !infraTableFocused) { IntegrationInfo selInfo = findSelectedIntegration(); if (selInfo != null) { - hint(spans, "p", selInfo.routeStarted > 0 ? "stop" : "start"); + hint(spans, "p", selInfo.routeStarted > 0 ? "stop routes" : "start routes"); } } + if (selectedPid != null) { + hint(spans, "x", "stop"); + hint(spans, "X", "kill"); + } hint(spans, "1-9", "tabs"); } else if (tab == TAB_ROUTES && showSource) { hint(spans, "c/Esc", "close"); @@ -5394,12 +5626,27 @@ private void refreshDataSync() { data.set(infos); + // Discover running infra services + refreshInfraData(); + // Refresh log data only when the Log tab is visible if (tabsState.selected() == TAB_LOG) { - IntegrationInfo selected = findSelectedIntegration(); - if (selected != null) { - if (!selected.pid.equals(logFilePid)) { - // Integration changed: reset all incremental log state + // Determine which log file to tail: infra or integration + String logPid = null; + String logFileName = null; + InfraInfo selInfra = findSelectedInfra(); + if (selInfra != null) { + logPid = selInfra.pid; + logFileName = "infra-" + selInfra.alias + "-" + selInfra.pid + ".log"; + } else { + IntegrationInfo selected = findSelectedIntegration(); + if (selected != null) { + logPid = selected.pid; + logFileName = selected.pid + ".log"; + } + } + if (logPid != null) { + if (!logPid.equals(logFilePid)) { mutableFilteredEntries.clear(); logFilePos = -1; logTotalLinesRead = 0; @@ -5407,7 +5654,7 @@ private void refreshDataSync() { logLineBuffer.setLength(0); } List newRawLines = new ArrayList<>(); - readNewLogLines(selected.pid, newRawLines); + readNewLogLinesFromFile(logPid, logFileName, newRawLines); if (!newRawLines.isEmpty()) { logTotalLinesRead += newRawLines.size(); for (String line : newRawLines) { @@ -5430,6 +5677,88 @@ private void refreshDataSync() { } } + @SuppressWarnings("unchecked") + private void refreshInfraData() { + List infraInfos = new ArrayList<>(); + try { + Path camelDir = CommandLineHelper.getCamelDir(); + if (Files.isDirectory(camelDir)) { + try (var files = Files.list(camelDir)) { + List jsonFiles = files + .filter(p -> { + String n = p.getFileName().toString(); + return n.startsWith("infra-") && n.endsWith(".json"); + }) + .toList(); + for (Path jsonFile : jsonFiles) { + String fn = jsonFile.getFileName().toString(); + // Format: infra-{alias}-{pid}.json + String withoutExt = fn.substring(0, fn.lastIndexOf('.')); + int lastDash = withoutExt.lastIndexOf('-'); + if (lastDash <= 6) { + continue; + } + String alias = withoutExt.substring(6, lastDash); + String pidStr = withoutExt.substring(lastDash + 1); + long pid; + try { + pid = Long.parseLong(pidStr); + } catch (NumberFormatException e) { + continue; + } + boolean alive = ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false); + + InfraInfo info = new InfraInfo(); + info.pid = pidStr; + info.alias = alias; + info.alive = alive; + try { + String json = Files.readString(jsonFile); + Object parsed = Jsoner.deserialize(json); + if (parsed instanceof Map map) { + for (Map.Entry e : map.entrySet()) { + info.properties.put(String.valueOf(e.getKey()), e.getValue()); + } + } + } catch (Exception e) { + // ignore parse errors + } + infraInfos.add(info); + } + } + } + } catch (IOException e) { + // ignore + } + + // Handle vanishing infra services + Set liveInfraPids = infraInfos.stream().map(i -> i.pid).collect(Collectors.toSet()); + List previousInfra = infraData.get(); + for (InfraInfo prev : previousInfra) { + if (!prev.vanishing && !liveInfraPids.contains(prev.pid) && !vanishingInfra.containsKey(prev.pid)) { + vanishingInfra.put(prev.pid, new VanishingInfraInfo(prev, System.currentTimeMillis())); + } + } + long now = System.currentTimeMillis(); + Iterator> infraIt = vanishingInfra.entrySet().iterator(); + while (infraIt.hasNext()) { + Map.Entry entry = infraIt.next(); + if (now - entry.getValue().startTime > VANISH_DURATION_MS) { + infraIt.remove(); + } else if (!liveInfraPids.contains(entry.getKey())) { + InfraInfo ghost = entry.getValue().info; + ghost.vanishing = true; + ghost.vanishStart = entry.getValue().startTime; + infraInfos.add(ghost); + } else { + infraIt.remove(); + } + } + + infraInfos.sort((a, b) -> a.alias.compareToIgnoreCase(b.alias)); + infraData.set(infraInfos); + } + private void updateThroughputHistory(IntegrationInfo info) { // Track exchangesTotal and exchangesFailed over a 1-second sliding window long currentTotal = info.exchangesTotal; @@ -6408,9 +6737,29 @@ private IntegrationInfo findSelectedIntegration() { .findFirst().orElse(null); } + private InfraInfo findSelectedInfra() { + if (selectedPid == null) { + return null; + } + return infraData.get().stream() + .filter(i -> selectedPid.equals(i.pid) && !i.vanishing) + .findFirst().orElse(null); + } + + private boolean isInfraSelected() { + return infraTableFocused && findSelectedInfra() != null; + } + private String selectedName() { IntegrationInfo info = findSelectedIntegration(); - return info != null ? truncate(info.name, 20) : "?"; + if (info != null) { + return truncate(info.name, 20); + } + InfraInfo infra = findSelectedInfra(); + if (infra != null) { + return truncate(infra.alias, 20); + } + return "?"; } private List findPids(String name) { @@ -6754,6 +7103,19 @@ static class HistoryEntry { Map exchangeVariableTypes; } + static class InfraInfo { + String pid; + String alias; + String description; + Map properties = new TreeMap<>(); + boolean alive; + boolean vanishing; + long vanishStart; + } + record VanishingInfo(IntegrationInfo info, long startTime) { } + + record VanishingInfraInfo(InfraInfo info, long startTime) { + } } From e74ec617cac0608650ec5329a1a4718bafc0ccb6 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Wed, 20 May 2026 16:46:54 +0200 Subject: [PATCH 2/5] CAMEL-23569: Fix infra layout and log tab for infra services Collapse integrations table when empty so infra table fills the space, show info panel for selected infra without sparkline, and allow Log tab to display container logs for infra services. Co-Authored-By: Claude Opus 4.6 --- .../jbang/core/commands/tui/CamelMonitor.java | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java index c5f084fad0f59..02a386044ad63 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java @@ -1443,18 +1443,28 @@ private void renderOverview(Frame frame, Rect area) { } } - // Split: integrations (fill) + infra table (if present) + chart (14 rows) + // Split: integrations + infra table (if present) + chart or info panel boolean hasSparkline = chartMode != CHART_OFF && !throughputHistory.isEmpty(); boolean hasInfra = !infraInfos.isEmpty(); + boolean hasActiveIntegrations = infos.stream().anyMatch(i -> !i.vanishing); + boolean showInfraInfoPanel = hasInfra && infraTableFocused && findSelectedInfra() != null && !hasSparkline; // infra table height: header(1) + borders(2) + rows (capped at 6) int infraHeight = hasInfra ? Math.min(infraInfos.size(), 6) + 3 : 0; List constraints = new ArrayList<>(); - constraints.add(Constraint.fill()); - if (hasInfra) { - constraints.add(Constraint.length(infraHeight)); + if (hasInfra && !hasActiveIntegrations) { + // No active integrations: collapse integrations table, infra fills + constraints.add(Constraint.length(3)); // border + header only + constraints.add(Constraint.fill()); + } else { + constraints.add(Constraint.fill()); + if (hasInfra) { + constraints.add(Constraint.length(infraHeight)); + } } if (hasSparkline) { constraints.add(Constraint.length(14)); + } else if (showInfraInfoPanel) { + constraints.add(Constraint.length(10)); } List chunks = Layout.vertical() .constraints(constraints) @@ -1704,6 +1714,8 @@ private void renderOverview(Frame frame, Rect area) { // Info panel: heap and threads for the selected integration renderOverviewInfoPanel(frame, infoArea); + } else if (showInfraInfoPanel) { + renderOverviewInfoPanel(frame, chunks.get(chunks.size() - 1)); } } @@ -4052,7 +4064,8 @@ private void renderEndpointFlow( private void renderLog(Frame frame, Rect area) { IntegrationInfo info = findSelectedIntegration(); - if (info == null) { + InfraInfo infraSel = info == null ? findSelectedInfra() : null; + if (info == null && infraSel == null) { renderNoSelection(frame, area); return; } @@ -4064,9 +4077,14 @@ private void renderLog(Frame frame, Rect area) { String chunkSuffix = totalRead > entries.size() ? " #" + (totalRead - entries.size() + 1) + "-" + totalRead : ""; - String logTitle = info.rootLogLevel != null - ? " Log level:" + info.rootLogLevel + chunkSuffix + " " - : " Log" + chunkSuffix + " "; + String logTitle; + if (infraSel != null) { + logTitle = " Log [" + infraSel.alias + "]" + chunkSuffix + " "; + } else if (info.rootLogLevel != null) { + logTitle = " Log level:" + info.rootLogLevel + chunkSuffix + " "; + } else { + logTitle = " Log" + chunkSuffix + " "; + } Block block = Block.builder() .borderType(BorderType.ROUNDED) .title(logTitle) From b3f64f724ae8b355a026e0b072b7fdc1ecab116b Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Wed, 20 May 2026 16:56:17 +0200 Subject: [PATCH 3/5] CAMEL-23569: Toggle between integrations and infra views with 'i' key Show one table at a time on the overview instead of stacking both. Press 'i' to swap views. Auto-focus infra when no integrations exist. Co-Authored-By: Claude Opus 4.6 --- .../jbang/core/commands/tui/CamelMonitor.java | 143 +++++++++--------- 1 file changed, 69 insertions(+), 74 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java index 02a386044ad63..94e791ce2ae13 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java @@ -635,6 +635,23 @@ private boolean handleEvent(Event event, TuiRunner runner) { chartMode = (chartMode + 1) % 3; return true; } + // Overview tab: toggle focus between integrations and infra tables + if (tab == TAB_OVERVIEW && ke.isChar('i') && !infraData.get().isEmpty()) { + infraTableFocused = !infraTableFocused; + if (infraTableFocused) { + if (infraTableState.selected() == null) { + infraTableState.select(0); + } + syncSelectedPidFromInfra(); + } else { + List intInfos = sortedOverviewInfos(); + if (!intInfos.isEmpty() && overviewTableState.selected() == null) { + overviewTableState.select(0); + } + syncSelectedPidFromOverview(); + } + return true; + } // Overview tab: start/stop all routes for selected integration (not infra) if (tab == TAB_OVERVIEW && ke.isChar('p') && selectedPid != null && !infraTableFocused) { IntegrationInfo selInfo = findSelectedIntegration(); @@ -1147,18 +1164,8 @@ private void navigateUp() { switch (tabsState.selected()) { case TAB_OVERVIEW -> { if (infraTableFocused) { - Integer sel = infraTableState.selected(); - if (sel != null && sel <= 0) { - infraTableFocused = false; - List intInfos = sortedOverviewInfos(); - if (!intInfos.isEmpty()) { - overviewTableState.select(intInfos.size() - 1); - } - syncSelectedPidFromOverview(); - } else { - infraTableState.selectPrevious(); - syncSelectedPidFromInfra(); - } + infraTableState.selectPrevious(); + syncSelectedPidFromInfra(); } else { overviewTableState.selectPrevious(); syncSelectedPidFromOverview(); @@ -1191,20 +1198,11 @@ private void navigateDown() { switch (tabsState.selected()) { case TAB_OVERVIEW -> { if (infraTableFocused) { - List infraInfos = infraData.get(); - infraTableState.selectNext(infraInfos.size()); + infraTableState.selectNext(infraData.get().size()); syncSelectedPidFromInfra(); } else { - List overviewInfos = sortedOverviewInfos(); - Integer sel = overviewTableState.selected(); - if (sel != null && sel >= overviewInfos.size() - 1 && !infraData.get().isEmpty()) { - infraTableFocused = true; - infraTableState.select(0); - syncSelectedPidFromInfra(); - } else { - overviewTableState.selectNext(overviewInfos.size()); - syncSelectedPidFromOverview(); - } + overviewTableState.selectNext(sortedOverviewInfos().size()); + syncSelectedPidFromOverview(); } } case TAB_ROUTES -> { @@ -1443,27 +1441,14 @@ private void renderOverview(Frame frame, Rect area) { } } - // Split: integrations + infra table (if present) + chart or info panel - boolean hasSparkline = chartMode != CHART_OFF && !throughputHistory.isEmpty(); - boolean hasInfra = !infraInfos.isEmpty(); - boolean hasActiveIntegrations = infos.stream().anyMatch(i -> !i.vanishing); - boolean showInfraInfoPanel = hasInfra && infraTableFocused && findSelectedInfra() != null && !hasSparkline; - // infra table height: header(1) + borders(2) + rows (capped at 6) - int infraHeight = hasInfra ? Math.min(infraInfos.size(), 6) + 3 : 0; + // Split: one table (integrations or infra, toggled by 'i') + chart or info panel + boolean hasSparkline = chartMode != CHART_OFF && !throughputHistory.isEmpty() && !infraTableFocused; + boolean showInfoPanel = infraTableFocused && findSelectedInfra() != null && !hasSparkline; List constraints = new ArrayList<>(); - if (hasInfra && !hasActiveIntegrations) { - // No active integrations: collapse integrations table, infra fills - constraints.add(Constraint.length(3)); // border + header only - constraints.add(Constraint.fill()); - } else { - constraints.add(Constraint.fill()); - if (hasInfra) { - constraints.add(Constraint.length(infraHeight)); - } - } + constraints.add(Constraint.fill()); if (hasSparkline) { constraints.add(Constraint.length(14)); - } else if (showInfraInfoPanel) { + } else if (showInfoPanel) { constraints.add(Constraint.length(10)); } List chunks = Layout.vertical() @@ -1538,35 +1523,34 @@ private void renderOverview(Frame frame, Rect area) { rightCell("INFLIGHT", 8, Style.EMPTY.bold()), Cell.from(Span.styled("SINCE-LAST", Style.EMPTY.bold()))); - Style integrationHighlight = infraTableFocused - ? Style.EMPTY.fg(Color.WHITE).dim() - : Style.EMPTY.fg(Color.WHITE).bold().onBlue(); - Table table = Table.builder() - .rows(rows) - .header(header) - .widths( - Constraint.length(8), - Constraint.fill(), - Constraint.length(16), - Constraint.length(5), - Constraint.length(10), - Constraint.length(8), - Constraint.length(7), - Constraint.length(8), - Constraint.length(8), - Constraint.length(6), - Constraint.length(8), - Constraint.length(12)) - .highlightStyle(integrationHighlight) - .highlightSpacing(Table.HighlightSpacing.ALWAYS) - .block(Block.builder().borderType(BorderType.ROUNDED).title(" Integrations ").build()) - .build(); - - frame.renderStatefulWidget(table, chunks.get(0), overviewTableState); + if (infraTableFocused) { + // Show infra table only + renderInfraTable(frame, chunks.get(0), infraInfos); + } else { + // Show integrations table only + Style integrationHighlight = Style.EMPTY.fg(Color.WHITE).bold().onBlue(); + Table table = Table.builder() + .rows(rows) + .header(header) + .widths( + Constraint.length(8), + Constraint.fill(), + Constraint.length(16), + Constraint.length(5), + Constraint.length(10), + Constraint.length(8), + Constraint.length(7), + Constraint.length(8), + Constraint.length(8), + Constraint.length(6), + Constraint.length(8), + Constraint.length(12)) + .highlightStyle(integrationHighlight) + .highlightSpacing(Table.HighlightSpacing.ALWAYS) + .block(Block.builder().borderType(BorderType.ROUNDED).title(" Integrations ").build()) + .build(); - // Infrastructure services table - if (hasInfra) { - renderInfraTable(frame, chunks.get(1), infraInfos); + frame.renderStatefulWidget(table, chunks.get(0), overviewTableState); } // Split green/red throughput bar chart with Y and X axes @@ -1714,7 +1698,7 @@ private void renderOverview(Frame frame, Rect area) { // Info panel: heap and threads for the selected integration renderOverviewInfoPanel(frame, infoArea); - } else if (showInfraInfoPanel) { + } else if (showInfoPanel) { renderOverviewInfoPanel(frame, chunks.get(chunks.size() - 1)); } } @@ -1869,9 +1853,7 @@ private void renderInfraTable(Frame frame, Rect area, List infraInfos Cell.from(Span.styled("PORT", Style.EMPTY.bold())), Cell.from(Span.styled("HOST", Style.EMPTY.bold()))); - Style infraHighlight = infraTableFocused - ? Style.EMPTY.fg(Color.WHITE).bold().onBlue() - : Style.EMPTY.fg(Color.WHITE).dim(); + Style infraHighlight = Style.EMPTY.fg(Color.WHITE).bold().onBlue(); Table infraTable = Table.builder() .rows(infraRows) .header(infraHeader) @@ -5346,6 +5328,9 @@ private void renderFooter(Frame frame, Rect area) { hint(spans, "Esc", infraTableFocused ? "integrations" : "unselect"); } hint(spans, "\u2191\u2193", "navigate"); + if (!infraData.get().isEmpty()) { + hint(spans, "i", infraTableFocused ? "integrations" : "infra"); + } if (!infraTableFocused) { hint(spans, "s", "sort"); hint(spans, "a", "chart " + switch (chartMode) { @@ -5647,6 +5632,16 @@ private void refreshDataSync() { // Discover running infra services refreshInfraData(); + // Auto-focus infra table when no active integrations exist + if (!infraTableFocused && !infraData.get().isEmpty() + && infos.stream().noneMatch(i -> !i.vanishing)) { + infraTableFocused = true; + if (infraTableState.selected() == null) { + infraTableState.select(0); + } + syncSelectedPidFromInfra(); + } + // Refresh log data only when the Log tab is visible if (tabsState.selected() == TAB_LOG) { // Determine which log file to tail: infra or integration From 6ad305eeed15e8c7bf18a0ac40911375632e3a41 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Wed, 20 May 2026 17:03:21 +0200 Subject: [PATCH 4/5] CAMEL-23569: Polish infra UI - tab bar, hints, and selected color Show only Overview and Log tabs when infra is selected. Use magenta for infra selection in header. Hide Enter hint and log level shortcut for infra. Fix infra count to show "infra(s)". Co-Authored-By: Claude Opus 4.6 --- .../jbang/core/commands/tui/CamelMonitor.java | 115 ++++++++++++------ 1 file changed, 81 insertions(+), 34 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java index 94e791ce2ae13..1c36759eb0b08 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java @@ -461,49 +461,63 @@ private boolean handleEvent(Event event, TuiRunner runner) { return true; } // Tab switching with number keys + // When infra is selected, only Overview (1) and Log (2) are available if (ke.isChar('1')) { return handleTabKey(TAB_OVERVIEW); } if (ke.isChar('2')) { return handleTabKey(TAB_LOG); } - if (ke.isChar('3')) { - return handleTabKey(TAB_ROUTES); - } - if (ke.isChar('4')) { - return handleTabKey(TAB_CONSUMERS); - } - if (ke.isChar('5')) { - return handleTabKey(TAB_ENDPOINTS); - } - if (ke.isChar('6')) { - return handleTabKey(TAB_HTTP); - } - if (ke.isChar('7')) { - return handleTabKey(TAB_HEALTH); - } - if (ke.isChar('8')) { - return handleTabKey(TAB_HISTORY); - } - if (ke.isChar('9')) { - return handleTabKey(TAB_CIRCUIT_BREAKER); + if (!isInfraSelected()) { + if (ke.isChar('3')) { + return handleTabKey(TAB_ROUTES); + } + if (ke.isChar('4')) { + return handleTabKey(TAB_CONSUMERS); + } + if (ke.isChar('5')) { + return handleTabKey(TAB_ENDPOINTS); + } + if (ke.isChar('6')) { + return handleTabKey(TAB_HTTP); + } + if (ke.isChar('7')) { + return handleTabKey(TAB_HEALTH); + } + if (ke.isChar('8')) { + return handleTabKey(TAB_HISTORY); + } + if (ke.isChar('9')) { + return handleTabKey(TAB_CIRCUIT_BREAKER); + } } // Tab cycling (check Shift+Tab before Tab since Tab binding also matches Shift+Tab) if (ke.isFocusPrevious()) { - int prev = (tabsState.selected() - 1 + NUM_TABS) % NUM_TABS; - if (prev != TAB_OVERVIEW) { - selectCurrentIntegration(); + if (isInfraSelected()) { + // Cycle between Overview and Log only + int prev = tabsState.selected() == TAB_OVERVIEW ? TAB_LOG : TAB_OVERVIEW; + tabsState.select(prev); + } else { + int prev = (tabsState.selected() - 1 + NUM_TABS) % NUM_TABS; + if (prev != TAB_OVERVIEW) { + selectCurrentIntegration(); + } + tabsState.select(prev); } - tabsState.select(prev); return true; } if (ke.isFocusNext()) { - int next = (tabsState.selected() + 1) % NUM_TABS; - if (next != TAB_OVERVIEW) { - selectCurrentIntegration(); + if (isInfraSelected()) { + int next = tabsState.selected() == TAB_OVERVIEW ? TAB_LOG : TAB_OVERVIEW; + tabsState.select(next); + } else { + int next = (tabsState.selected() + 1) % NUM_TABS; + if (next != TAB_OVERVIEW) { + selectCurrentIntegration(); + } + tabsState.select(next); } - tabsState.select(next); return true; } @@ -1274,11 +1288,16 @@ private void renderHeader(Frame frame, Rect area) { long activeInfra = infraData.get().stream().filter(i -> !i.vanishing).count(); if (activeInfra > 0) { titleSpans.add(Span.raw(" ")); - titleSpans.add(Span.styled(activeInfra + " infra", Style.EMPTY.fg(Color.MAGENTA))); + titleSpans.add(Span.styled(activeInfra + " infra(s)", Style.EMPTY.fg(Color.MAGENTA))); } if (selectedPid != null) { titleSpans.add(Span.raw(" ")); - titleSpans.add(Span.styled("selected: " + selectedName(), Style.EMPTY.fg(Color.YELLOW))); + InfraInfo selInfra = findSelectedInfra(); + if (selInfra != null) { + titleSpans.add(Span.styled("selected: " + selectedName(), Style.EMPTY.fg(Color.MAGENTA))); + } else { + titleSpans.add(Span.styled("selected: " + selectedName(), Style.EMPTY.fg(Color.YELLOW))); + } } Line titleLine = Line.from(titleSpans); @@ -1288,6 +1307,32 @@ private void renderHeader(Frame frame, Rect area) { } private void renderTabs(Frame frame, Rect area) { + boolean infraSelected = isInfraSelected(); + + if (infraSelected) { + // Infra mode: only Overview and Log tabs + Line[] labels = { + Line.from(" 1 Overview "), + Line.from(" 2 Log "), + }; + + // Map real tab index to infra tab index for highlight + int infraTabIdx = tabsState.selected() == TAB_LOG ? 1 : 0; + TabsState infraTabsState = new TabsState(infraTabIdx); + + Tabs tabs = Tabs.builder() + .titles(labels) + .highlightStyle(Style.EMPTY.fg(Color.rgb(0xF6, 0x91, 0x23)).bold()) + .divider(Span.styled(" | ", Style.EMPTY.dim())) + .build(); + + Rect labelsArea = area.height() >= 2 + ? new Rect(area.x(), area.y() + 1, area.width(), 1) + : area; + frame.renderStatefulWidget(tabs, labelsArea, infraTabsState); + return; + } + // Compute notification counts (0 if no integration selected) List infos = data.get(); long activeCount = infos.stream().filter(i -> !i.vanishing).count(); @@ -5339,7 +5384,6 @@ private void renderFooter(Frame frame, Rect area) { default -> "[off]"; }); } - hint(spans, "Enter", "details"); if (selectedPid != null && !infraTableFocused) { IntegrationInfo selInfo = findSelectedIntegration(); if (selInfo != null) { @@ -5350,7 +5394,7 @@ private void renderFooter(Frame frame, Rect area) { hint(spans, "x", "stop"); hint(spans, "X", "kill"); } - hint(spans, "1-9", "tabs"); + hint(spans, isInfraSelected() ? "1-2" : "1-9", "tabs"); } else if (tab == TAB_ROUTES && showSource) { hint(spans, "c/Esc", "close"); hint(spans, "\u2191\u2193\u2190\u2192", "scroll"); @@ -5428,8 +5472,11 @@ private void renderFooter(Frame frame, Rect area) { if (!logWordWrap) { hint(spans, "\u2190\u2192", "h-scroll"); } - hint(spans, "l", "level"); - hintLast(spans, "f", "follow" + (logFollowMode ? " [on]" : " [off]")); + if (!isInfraSelected()) { + hint(spans, "l", "level"); + } + hint(spans, "f", "follow" + (logFollowMode ? " [on]" : " [off]")); + hint(spans, isInfraSelected() ? "1-2" : "1-9", "tabs"); } else if (tab == TAB_HTTP && showHttpSpec) { hint(spans, "c/Esc", "close"); hint(spans, "↑↓", "scroll"); From 6d67f18e3af5d7d5497c1ca8ff37b8622aa6a1db Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Wed, 20 May 2026 17:13:57 +0200 Subject: [PATCH 5/5] CAMEL-23569: Add kill confirm dialog and clean up orphaned files Show confirm dialog before force-killing a process. Clean up all orphaned files (.log, -status.json, -action.json, -output.json, -trace.json, -history.json, -debug.json, -receive.json) after force kill for both integrations and infra services. Co-Authored-By: Claude Opus 4.6 --- .../jbang/core/commands/tui/CamelMonitor.java | 70 +++++++++++++++++-- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java index 1c36759eb0b08..9676520457aeb 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java @@ -361,6 +361,7 @@ public class CamelMonitor extends CamelCommand { private static final String[] LOG_LEVELS = { "ERROR", "WARN", "INFO", "DEBUG", "TRACE" }; private boolean showLogLevelPopup; private final ListState logLevelListState = new ListState(); + private boolean showKillConfirm; private final AtomicBoolean refreshInProgress = new AtomicBoolean(false); private final AtomicBoolean diagramLoading = new AtomicBoolean(false); @@ -414,6 +415,17 @@ public Integer doCall() throws Exception { private boolean handleEvent(Event event, TuiRunner runner) { if (event instanceof KeyEvent ke) { + // Kill confirm dialog: Enter to confirm, Esc/any other key to cancel + if (showKillConfirm) { + if (ke.isConfirm()) { + showKillConfirm = false; + stopSelectedProcess(true); + } else { + showKillConfirm = false; + } + return true; + } + // Escape: navigate back if (ke.isCancel()) { if (showLogLevelPopup) { @@ -680,9 +692,9 @@ private boolean handleEvent(Event event, TuiRunner runner) { stopSelectedProcess(false); return true; } - // Overview tab: kill process (SIGKILL) for selected integration or infra + // Overview tab: kill process (SIGKILL) — show confirm dialog first if (tab == TAB_OVERVIEW && ke.isChar('X') && selectedPid != null) { - stopSelectedProcess(true); + showKillConfirm = true; return true; } @@ -1271,6 +1283,9 @@ private void render(Frame frame) { renderTabs(frame, mainChunks.get(2)); // mainChunks.get(3) is the empty spacer row between tabs and content renderContent(frame, mainChunks.get(4)); + if (showKillConfirm) { + renderKillConfirm(frame, mainChunks.get(4)); + } renderFooter(frame, mainChunks.get(5)); } @@ -3094,12 +3109,12 @@ private void stopSelectedProcess(boolean forceKill) { return; } if (infraTableFocused) { - // For infra services: delete the JSON file to trigger graceful shutdown + // For infra services: delete the JSON and log files to trigger graceful shutdown InfraInfo infra = findSelectedInfra(); if (infra != null) { - Path jsonFile = CommandLineHelper.getCamelDir() - .resolve("infra-" + infra.alias + "-" + infra.pid + ".json"); - PathUtils.deleteFile(jsonFile); + Path camelDir = CommandLineHelper.getCamelDir(); + PathUtils.deleteFile(camelDir.resolve("infra-" + infra.alias + "-" + infra.pid + ".json")); + PathUtils.deleteFile(camelDir.resolve("infra-" + infra.alias + "-" + infra.pid + ".log")); } if (forceKill) { ProcessHandle.of(pid).ifPresent(ProcessHandle::destroyForcibly); @@ -3109,6 +3124,16 @@ private void stopSelectedProcess(boolean forceKill) { ProcessHandle.of(pid).ifPresent(ph -> { if (forceKill) { ph.destroyForcibly(); + // Clean up orphaned files after force kill + Path camelDir = CommandLineHelper.getCamelDir(); + PathUtils.deleteFile(camelDir.resolve(selectedPid + ".log")); + PathUtils.deleteFile(camelDir.resolve(selectedPid + "-status.json")); + PathUtils.deleteFile(camelDir.resolve(selectedPid + "-action.json")); + PathUtils.deleteFile(camelDir.resolve(selectedPid + "-output.json")); + PathUtils.deleteFile(camelDir.resolve(selectedPid + "-trace.json")); + PathUtils.deleteFile(camelDir.resolve(selectedPid + "-history.json")); + PathUtils.deleteFile(camelDir.resolve(selectedPid + "-debug.json")); + PathUtils.deleteFile(camelDir.resolve(selectedPid + "-receive.json")); } else { ph.destroy(); } @@ -4217,6 +4242,39 @@ private void renderLogLevelPopup(Frame frame, Rect area) { frame.renderStatefulWidget(list, popup, logLevelListState); } + private void renderKillConfirm(Frame frame, Rect area) { + String name = selectedName(); + String msg = " Kill " + name + " (PID: " + selectedPid + ")? "; + int popupW = Math.max(34, msg.length() + 4); + int popupH = 6; + int x = area.left() + Math.max(0, (area.width() - popupW) / 2); + int y = area.top() + Math.max(0, (area.height() - popupH) / 2); + Rect popup = new Rect(x, y, Math.min(popupW, area.width()), Math.min(popupH, area.height())); + + frame.renderWidget(Clear.INSTANCE, popup); + Block block = Block.builder() + .borderType(BorderType.ROUNDED) + .borderStyle(Style.EMPTY.fg(Color.LIGHT_RED)) + .title(" Confirm Kill ") + .build(); + frame.renderWidget(block, popup); + Rect inner = block.inner(popup); + frame.renderWidget( + Paragraph.builder() + .text(Text.from( + Line.from(Span.raw("")), + Line.from(Span.styled(msg, Style.EMPTY.fg(Color.LIGHT_RED).bold())), + Line.from(Span.raw("")), + Line.from( + Span.raw(" "), + Span.styled("Enter", Style.EMPTY.bold()), + Span.raw(" confirm "), + Span.styled("Esc", Style.EMPTY.bold()), + Span.raw(" cancel")))) + .build(), + inner); + } + private void sendLoggerLevelAction(String pid, String level) { JsonObject root = new JsonObject(); root.put("action", "logger");