001/*
002 * Copyright 2025 devteam@scivics-lab.com
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing,
011 * software distributed under the License is distributed on an
012 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
013 * either express or implied.  See the License for the
014 * specific language governing permissions and limitations
015 * under the License.
016 */
017
018package com.scivicslab.actoriac.cli;
019
020import java.io.BufferedReader;
021import java.io.File;
022import java.io.IOException;
023import java.io.InputStreamReader;
024import java.io.OutputStream;
025import java.net.InetSocketAddress;
026import java.net.Socket;
027import java.nio.charset.StandardCharsets;
028import java.sql.Connection;
029import java.sql.DriverManager;
030import java.sql.ResultSet;
031import java.sql.SQLException;
032import java.sql.Statement;
033import java.time.OffsetDateTime;
034import java.time.format.DateTimeFormatter;
035import java.util.ArrayList;
036import java.util.List;
037import java.util.concurrent.Callable;
038import java.util.concurrent.CountDownLatch;
039import java.util.logging.Logger;
040
041import com.sun.net.httpserver.HttpExchange;
042import com.sun.net.httpserver.HttpServer;
043
044import org.h2.tools.Server;
045
046import picocli.CommandLine.Command;
047import picocli.CommandLine.Option;
048
049/**
050 * CLI subcommand to start an H2 TCP server for centralized workflow logging.
051 *
052 * <p>This enables multiple actor-IaC processes on the same machine to write to
053 * a single shared log database. The server runs on localhost only - remote
054 * connections are not needed because actor-IaC operates from a single
055 * operation terminal.</p>
056 *
057 * <p><strong>Architecture:</strong></p>
058 * <pre>
059 * [Operation Terminal]
060 * ├── H2 Log Server (localhost:29090)
061 * ├── Workflow Process A ──→ TCP write
062 * ├── Workflow Process B ──→ TCP write
063 * └── Workflow Process C ──→ TCP write
064 *          │
065 *          │ SSH commands
066 *          ▼
067 * [Remote Nodes] ← Don't write directly to log server
068 * </pre>
069 *
070 * <p><strong>Usage:</strong></p>
071 * <pre>{@code
072 * # Start the log server
073 * actor-iac log-server --db ./logs/actor-iac-logs
074 *
075 * # In another terminal, run workflow with --log-server
076 * actor-iac -d ./workflows -w deploy --log-server=localhost:29090
077 * }</pre>
078 *
079 * @author devteam@scivics-lab.com
080 */
081@Command(
082    name = "log-serve",
083    mixinStandardHelpOptions = true,
084    version = "actor-IaC log-serve 2.11.0",
085    description = "Serve H2 TCP server for centralized workflow logging."
086)
087public class LogServerCLI implements Callable<Integer> {
088
089    private static final Logger LOG = Logger.getLogger(LogServerCLI.class.getName());
090
091    @Option(
092        names = {"-p", "--port"},
093        description = "TCP port for H2 server (default: ${DEFAULT-VALUE})",
094        defaultValue = "29090"
095    )
096    private int port;
097
098    @Option(
099        names = {"--db"},
100        description = "Database file path without extension (default: ${DEFAULT-VALUE})",
101        defaultValue = "./actor-iac-logs"
102    )
103    private File dbPath;
104
105    @Option(
106        names = {"-v", "--verbose"},
107        description = "Enable verbose output"
108    )
109    private boolean verbose;
110
111    @Option(
112        names = {"--find"},
113        description = "Find running H2 log servers on localhost and show port status"
114    )
115    private boolean find;
116
117    @Option(
118        names = {"--info-port"},
119        description = "HTTP port for info API (default: TCP port - 200)"
120    )
121    private Integer infoPort;
122
123    @Option(
124        names = {"--auto-shutdown"},
125        description = "Enable automatic shutdown when no connections for --idle-timeout seconds"
126    )
127    private boolean autoShutdown;
128
129    @Option(
130        names = {"--idle-timeout"},
131        description = "Idle timeout in seconds for auto-shutdown (default: ${DEFAULT-VALUE})",
132        defaultValue = "300"
133    )
134    private long idleTimeoutSeconds;
135
136    @Option(
137        names = {"--check-interval"},
138        description = "Connection check interval in seconds (default: ${DEFAULT-VALUE})",
139        defaultValue = "60"
140    )
141    private long checkIntervalSeconds;
142
143    private Server tcpServer;
144    private HttpServer httpServer;
145    private OffsetDateTime startedAt;
146
147    /** ActorSystem for actor-based shutdown management */
148    private com.scivicslab.pojoactor.core.ActorSystem actorSystem;
149
150    /** The log server actor (used when --auto-shutdown is enabled) */
151    private com.scivicslab.pojoactor.core.ActorRef<LogServerActor> logServerActorRef;
152
153    /** The connection watcher actor */
154    private ConnectionWatcherActor connectionWatcher;
155
156    /** Scheduler for periodic connection checks */
157    private java.util.concurrent.ScheduledExecutorService scheduler;
158
159    /** Default offset from TCP port to HTTP port (TCP - 200 = HTTP) */
160    private static final int HTTP_PORT_OFFSET = -200;
161
162    /** Ports to scan for H2 servers (actor-IaC reserved range: 29090-29100) */
163    private static final int[] SCAN_PORTS = {29090, 29091, 29092, 29093, 29094, 29095, 29096, 29097, 29098, 29099, 29100};
164    private final CountDownLatch shutdownLatch = new CountDownLatch(1);
165
166    @Override
167    public Integer call() {
168        // Handle --find option
169        if (find) {
170            return findLogServers();
171        }
172
173        // Calculate HTTP port
174        int httpPort = (infoPort != null) ? infoPort : port + HTTP_PORT_OFFSET;
175
176        // Choose between actor mode (with auto-shutdown) and legacy mode
177        if (autoShutdown) {
178            return runWithActorMode(httpPort);
179        } else {
180            return runLegacyMode(httpPort);
181        }
182    }
183
184    /**
185     * Runs the log server with actor-based auto-shutdown.
186     *
187     * <p>Architecture:</p>
188     * <pre>
189     * ActorSystem
190     *   ├── LogServerActor (wraps H2 TCP + HTTP servers)
191     *   └── ConnectionWatcherActor (monitors and triggers shutdown)
192     * </pre>
193     */
194    private Integer runWithActorMode(int httpPort) {
195        try {
196            // Create ActorSystem
197            actorSystem = new com.scivicslab.pojoactor.core.ActorSystem("log-server", 2);
198
199            // Create LogServerActor POJO
200            LogServerActor logServerPojo = new LogServerActor(port, httpPort, dbPath, verbose);
201
202            // Wrap with ActorRef
203            logServerActorRef = actorSystem.actorOf("log-server", logServerPojo);
204
205            // Start the server via actor
206            logServerActorRef.ask(server -> {
207                try {
208                    server.start(shutdownLatch);
209                } catch (Exception e) {
210                    throw new RuntimeException("Failed to start server", e);
211                }
212                return null;
213            }).join();
214
215            // Print connection info
216            printConnectionInfo(httpPort);
217            System.out.println("  Auto-shutdown: enabled (idle timeout: " + idleTimeoutSeconds + "s)");
218
219            // Create and start ConnectionWatcher
220            connectionWatcher = new ConnectionWatcherActor(
221                logServerActorRef,
222                checkIntervalSeconds,
223                idleTimeoutSeconds,
224                verbose
225            );
226
227            // Create scheduler for watcher
228            scheduler = java.util.concurrent.Executors.newScheduledThreadPool(1, r -> {
229                Thread t = new Thread(r, "ConnectionWatcher");
230                t.setDaemon(true);
231                return t;
232            });
233
234            connectionWatcher.start(scheduler);
235
236            // Register shutdown hook
237            Runtime.getRuntime().addShutdownHook(new Thread(this::shutdownActorMode, "LogServer-Shutdown"));
238
239            System.out.println("\nServer is running with auto-shutdown enabled.");
240            System.out.println("Will shutdown after " + idleTimeoutSeconds + " seconds of no connections.");
241
242            // Wait for shutdown signal (from either Ctrl+C or auto-shutdown)
243            shutdownLatch.await();
244
245            return 0;
246
247        } catch (Exception e) {
248            System.err.println("Failed to start log server: " + e.getMessage());
249            if (verbose) {
250                e.printStackTrace();
251            }
252            return 1;
253        }
254    }
255
256    /**
257     * Runs the log server in legacy mode (no auto-shutdown).
258     */
259    private Integer runLegacyMode(int httpPort) {
260        try {
261            // Record start time
262            startedAt = OffsetDateTime.now();
263
264            // Build server arguments (localhost only - no remote connections needed)
265            String[] serverArgs = new String[]{
266                "-tcp",
267                "-tcpPort", String.valueOf(port),
268                "-ifNotExists"
269            };
270
271            // Create and start TCP server
272            tcpServer = Server.createTcpServer(serverArgs);
273            tcpServer.start();
274
275            // Initialize database schema
276            initializeDatabase();
277
278            // Start HTTP info server
279            startHttpServer(httpPort);
280
281            // Print connection info
282            printConnectionInfo(httpPort);
283
284            // Register shutdown hook
285            Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown, "LogServer-Shutdown"));
286
287            System.out.println("\nServer is running. Press Ctrl+C to stop.");
288
289            // Wait for shutdown signal
290            shutdownLatch.await();
291
292            return 0;
293
294        } catch (SQLException e) {
295            System.err.println("Failed to initialize database: " + e.getMessage());
296            if (verbose) {
297                e.printStackTrace();
298            }
299            return 1;
300        } catch (Exception e) {
301            System.err.println("Failed to start log server: " + e.getMessage());
302            if (verbose) {
303                e.printStackTrace();
304            }
305            return 1;
306        }
307    }
308
309    private void initializeDatabase() throws SQLException {
310        String dbUrl = "jdbc:h2:tcp://localhost:" + port + "/" + dbPath.getAbsolutePath();
311
312        try (Connection conn = DriverManager.getConnection(dbUrl);
313             Statement stmt = conn.createStatement()) {
314
315            // Sessions table
316            stmt.execute("""
317                CREATE TABLE IF NOT EXISTS sessions (
318                    id IDENTITY PRIMARY KEY,
319                    started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
320                    ended_at TIMESTAMP,
321                    workflow_name VARCHAR(255),
322                    overlay_name VARCHAR(255),
323                    inventory_name VARCHAR(255),
324                    node_count INT,
325                    status VARCHAR(20) DEFAULT 'RUNNING'
326                )
327                """);
328
329            // Logs table
330            stmt.execute("""
331                CREATE TABLE IF NOT EXISTS logs (
332                    id IDENTITY PRIMARY KEY,
333                    session_id BIGINT,
334                    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
335                    node_id VARCHAR(255) NOT NULL,
336                    vertex_name CLOB,
337                    action_name CLOB,
338                    level VARCHAR(10) NOT NULL,
339                    message CLOB,
340                    exit_code INT,
341                    duration_ms BIGINT,
342                    FOREIGN KEY (session_id) REFERENCES sessions(id)
343                )
344                """);
345
346            // Node results table
347            stmt.execute("""
348                CREATE TABLE IF NOT EXISTS node_results (
349                    id IDENTITY PRIMARY KEY,
350                    session_id BIGINT,
351                    node_id VARCHAR(255) NOT NULL,
352                    status VARCHAR(20) NOT NULL,
353                    reason VARCHAR(1000),
354                    FOREIGN KEY (session_id) REFERENCES sessions(id),
355                    UNIQUE (session_id, node_id)
356                )
357                """);
358
359            // Indexes
360            stmt.execute("CREATE INDEX IF NOT EXISTS idx_logs_session ON logs(session_id)");
361            stmt.execute("CREATE INDEX IF NOT EXISTS idx_logs_node ON logs(node_id)");
362            stmt.execute("CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level)");
363            stmt.execute("CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp)");
364            stmt.execute("CREATE INDEX IF NOT EXISTS idx_sessions_workflow ON sessions(workflow_name)");
365            stmt.execute("CREATE INDEX IF NOT EXISTS idx_sessions_overlay ON sessions(overlay_name)");
366            stmt.execute("CREATE INDEX IF NOT EXISTS idx_sessions_inventory ON sessions(inventory_name)");
367            stmt.execute("CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)");
368
369            if (verbose) {
370                System.out.println("Database schema initialized: " + dbPath.getAbsolutePath());
371            }
372        }
373    }
374
375    /**
376     * Starts the HTTP server for the /info API endpoint.
377     */
378    private void startHttpServer(int httpPort) throws IOException {
379        httpServer = HttpServer.create(new InetSocketAddress("localhost", httpPort), 0);
380        httpServer.createContext("/info", this::handleInfoRequest);
381        httpServer.setExecutor(null); // Use default executor
382        httpServer.start();
383
384        if (verbose) {
385            System.out.println("HTTP info server started on port " + httpPort);
386        }
387    }
388
389    /**
390     * Handles GET /info requests, returning server information as JSON.
391     */
392    private void handleInfoRequest(HttpExchange exchange) throws IOException {
393        if (!"GET".equals(exchange.getRequestMethod())) {
394            exchange.sendResponseHeaders(405, -1);
395            return;
396        }
397
398        try {
399            // Get session count from database
400            int sessionCount = getSessionCount();
401
402            // Build JSON response
403            String json = String.format("""
404                {
405                  "server": "actor-iac-log-server",
406                  "version": "2.11.0",
407                  "port": %d,
408                  "db_path": "%s",
409                  "db_file": "%s.mv.db",
410                  "started_at": "%s",
411                  "session_count": %d
412                }
413                """,
414                port,
415                escapeJson(dbPath.getAbsolutePath()),
416                escapeJson(dbPath.getAbsolutePath()),
417                startedAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
418                sessionCount
419            );
420
421            byte[] response = json.getBytes(StandardCharsets.UTF_8);
422            exchange.getResponseHeaders().add("Content-Type", "application/json");
423            exchange.sendResponseHeaders(200, response.length);
424            try (OutputStream os = exchange.getResponseBody()) {
425                os.write(response);
426            }
427        } catch (Exception e) {
428            String error = "{\"error\": \"" + escapeJson(e.getMessage()) + "\"}";
429            byte[] response = error.getBytes(StandardCharsets.UTF_8);
430            exchange.getResponseHeaders().add("Content-Type", "application/json");
431            exchange.sendResponseHeaders(500, response.length);
432            try (OutputStream os = exchange.getResponseBody()) {
433                os.write(response);
434            }
435        }
436    }
437
438    /**
439     * Gets the current session count from the database.
440     */
441    private int getSessionCount() {
442        String dbUrl = "jdbc:h2:tcp://localhost:" + port + "/" + dbPath.getAbsolutePath();
443        try (Connection conn = DriverManager.getConnection(dbUrl);
444             Statement stmt = conn.createStatement();
445             ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM sessions")) {
446            if (rs.next()) {
447                return rs.getInt(1);
448            }
449        } catch (SQLException e) {
450            if (verbose) {
451                System.err.println("Failed to get session count: " + e.getMessage());
452            }
453        }
454        return 0;
455    }
456
457    /**
458     * Escapes special characters for JSON strings.
459     */
460    private String escapeJson(String value) {
461        if (value == null) return "";
462        return value
463            .replace("\\", "\\\\")
464            .replace("\"", "\\\"")
465            .replace("\n", "\\n")
466            .replace("\r", "\\r")
467            .replace("\t", "\\t");
468    }
469
470    private void printConnectionInfo(int httpPort) {
471        System.out.println();
472        System.out.println("=".repeat(60));
473        System.out.println("H2 Log Server Started");
474        System.out.println("=".repeat(60));
475        System.out.println("  TCP Port:   " + port);
476        System.out.println("  HTTP Port:  " + httpPort);
477        System.out.println("  Database:   " + dbPath.getAbsolutePath() + ".mv.db");
478        System.out.println();
479        System.out.println("Connect from workflows using:");
480        System.out.println("  --log-serve=localhost:" + port);
481        System.out.println();
482        System.out.println("Server info API:");
483        System.out.println("  curl http://localhost:" + httpPort + "/info");
484        System.out.println();
485        System.out.println("Query logs using:");
486        System.out.println("  actor-iac log-search --server=localhost:" + port + " --db " + dbPath.getAbsolutePath() + " --list");
487        System.out.println("=".repeat(60));
488    }
489
490    private void shutdown() {
491        System.out.println("\nShutting down servers...");
492        if (httpServer != null) {
493            httpServer.stop(0);
494        }
495        if (tcpServer != null) {
496            tcpServer.stop();
497        }
498        shutdownLatch.countDown();
499        System.out.println("Servers stopped.");
500    }
501
502    /**
503     * Shuts down the server when running in actor mode.
504     */
505    private void shutdownActorMode() {
506        System.out.println("\nShutting down (actor mode)...");
507
508        // Stop the watcher first
509        if (connectionWatcher != null) {
510            connectionWatcher.stop();
511        }
512
513        // Stop the scheduler
514        if (scheduler != null) {
515            scheduler.shutdownNow();
516        }
517
518        // Stop the log server via actor
519        if (logServerActorRef != null) {
520            try {
521                logServerActorRef.ask(server -> {
522                    server.stop();
523                    return null;
524                }).join();
525            } catch (Exception e) {
526                // Server might already be stopped
527            }
528        }
529
530        // Terminate actor system
531        if (actorSystem != null) {
532            actorSystem.terminate();
533        }
534
535        System.out.println("Servers stopped (actor mode).");
536    }
537
538    /**
539     * Finds running H2 log servers on localhost.
540     *
541     * @return exit code (0 = success)
542     */
543    private Integer findLogServers() {
544        System.out.println("=".repeat(60));
545        System.out.println("Scanning for H2 Log Servers on localhost...");
546        System.out.println("=".repeat(60));
547        System.out.println();
548
549        List<ServerInfo> h2Servers = new ArrayList<>();
550        List<ServerInfo> otherServices = new ArrayList<>();
551
552        for (int scanPort : SCAN_PORTS) {
553            ServerInfo info = checkPort(scanPort);
554            if (info != null) {
555                if (info.isH2Server) {
556                    h2Servers.add(info);
557                } else {
558                    otherServices.add(info);
559                }
560            }
561        }
562
563        // Report H2 log servers found
564        if (h2Servers.isEmpty()) {
565            System.out.println("No H2 log servers found.");
566            System.out.println();
567            System.out.println("To start a log server:");
568            System.out.println("  ./actor_iac.java log-serve --db ./logs/actor-iac-logs");
569        } else {
570            System.out.println("H2 Log Servers Found:");
571            System.out.println("-".repeat(60));
572            for (ServerInfo info : h2Servers) {
573                System.out.println("  Port " + info.port + ": H2 Database Server");
574                if (info.hasHttpApi) {
575                    System.out.println("           HTTP API: http://localhost:" + info.httpPort + "/info");
576                    if (info.version != null) {
577                        System.out.println("           Version:  " + info.version);
578                    }
579                }
580                if (info.dbPath != null) {
581                    System.out.println("           Database: " + info.dbPath);
582                }
583                if (info.sessionCount >= 0) {
584                    System.out.println("           Sessions: " + info.sessionCount);
585                }
586                if (info.startedAt != null) {
587                    System.out.println("           Started:  " + info.startedAt);
588                }
589                System.out.println("           Connect:  --log-serve=localhost:" + info.port);
590                System.out.println();
591            }
592        }
593
594        // Report other services on nearby ports
595        if (!otherServices.isEmpty()) {
596            System.out.println();
597            System.out.println("Other Services on Nearby Ports:");
598            System.out.println("-".repeat(60));
599            for (ServerInfo info : otherServices) {
600                System.out.println("  Port " + info.port + ": " + info.serviceName);
601                if (info.processInfo != null) {
602                    System.out.println("           Process: " + info.processInfo);
603                }
604            }
605        }
606
607        // Suggest available port
608        int availablePort = findAvailablePort();
609        if (availablePort > 0 && h2Servers.isEmpty()) {
610            System.out.println();
611            System.out.println("All ports in range 29090-29100 are available.");
612            System.out.println("Start a log server with:");
613            System.out.println("  ./actor_iac.java log-serve --db ./logs/actor-iac-logs");
614        } else if (availablePort > 0) {
615            System.out.println();
616            System.out.println("Next available port: " + availablePort);
617            System.out.println("  ./actor_iac.java log-serve --port " + availablePort + " --db ./logs/actor-iac-logs");
618        }
619
620        System.out.println();
621        System.out.println("=".repeat(60));
622
623        return 0;
624    }
625
626    /**
627     * Checks what's running on a port.
628     */
629    private ServerInfo checkPort(int checkPort) {
630        // First, check if port is open
631        try (Socket socket = new Socket("localhost", checkPort)) {
632            socket.setSoTimeout(1000);
633            // Port is open, try to identify the service
634        } catch (Exception e) {
635            // Port not open
636            return null;
637        }
638
639        ServerInfo info = new ServerInfo(checkPort);
640
641        // Try to connect as H2
642        try {
643            // Try to connect to H2 and query session count
644            String url = "jdbc:h2:tcp://localhost:" + checkPort + "/~/.h2/test;IFEXISTS=TRUE";
645            try (Connection conn = DriverManager.getConnection(url)) {
646                info.isH2Server = true;
647                // This DB might not have our schema, but we know it's H2
648            }
649        } catch (SQLException e) {
650            // Check if it's "Database not found" - that still means H2 server is running
651            String msg = e.getMessage();
652            if (msg != null && (msg.contains("Database") || msg.contains("not found") ||
653                    msg.contains("Connection is broken") || msg.contains("90067"))) {
654                info.isH2Server = true;
655            }
656        }
657
658        if (info.isH2Server) {
659            // Try to get info from actor-iac-logs database
660            tryGetLogServerInfo(info);
661        } else {
662            // Try to identify other services using lsof
663            info.serviceName = identifyService(checkPort);
664        }
665
666        return info;
667    }
668
669    /**
670     * Tries to get information about a log server's database.
671     * First tries HTTP API, then falls back to database queries.
672     */
673    private void tryGetLogServerInfo(ServerInfo info) {
674        // First, try HTTP API (TCP port - 200)
675        if (tryGetInfoFromHttpApi(info, info.port + HTTP_PORT_OFFSET)) {
676            return;
677        }
678
679        // Fallback: try common database paths via JDBC
680        String[] commonPaths = {
681            "./actor-iac-logs",
682            "./logs/actor-iac-logs",
683            System.getProperty("user.home") + "/actor-iac-logs"
684        };
685
686        for (String path : commonPaths) {
687            try {
688                String url = "jdbc:h2:tcp://localhost:" + info.port + "/" + path;
689                try (Connection conn = DriverManager.getConnection(url);
690                     Statement stmt = conn.createStatement();
691                     ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM sessions")) {
692                    if (rs.next()) {
693                        info.sessionCount = rs.getInt(1);
694                        info.dbPath = path;
695                        return;
696                    }
697                }
698            } catch (SQLException e) {
699                // This path doesn't have our schema, try next
700            }
701        }
702    }
703
704    /**
705     * Tries to get server info from HTTP API.
706     *
707     * @return true if successfully got info from API
708     */
709    private boolean tryGetInfoFromHttpApi(ServerInfo info, int httpPort) {
710        try {
711            java.net.URL url = new java.net.URL("http://localhost:" + httpPort + "/info");
712            java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
713            conn.setConnectTimeout(1000);
714            conn.setReadTimeout(1000);
715            conn.setRequestMethod("GET");
716
717            if (conn.getResponseCode() == 200) {
718                try (BufferedReader reader = new BufferedReader(
719                        new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
720                    StringBuilder json = new StringBuilder();
721                    String line;
722                    while ((line = reader.readLine()) != null) {
723                        json.append(line);
724                    }
725
726                    // Parse JSON manually (avoid dependency)
727                    String jsonStr = json.toString();
728                    info.hasHttpApi = true;
729                    info.httpPort = httpPort;
730                    info.dbPath = extractJsonString(jsonStr, "db_path");
731                    info.version = extractJsonString(jsonStr, "version");
732                    info.startedAt = extractJsonString(jsonStr, "started_at");
733                    info.sessionCount = extractJsonInt(jsonStr, "session_count");
734                    return true;
735                }
736            }
737        } catch (Exception e) {
738            // HTTP API not available
739        }
740        return false;
741    }
742
743    /**
744     * Extracts a string value from JSON.
745     */
746    private String extractJsonString(String json, String key) {
747        String pattern = "\"" + key + "\"\\s*:\\s*\"([^\"]+)\"";
748        java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
749        java.util.regex.Matcher m = p.matcher(json);
750        return m.find() ? m.group(1) : null;
751    }
752
753    /**
754     * Extracts an integer value from JSON.
755     */
756    private int extractJsonInt(String json, String key) {
757        String pattern = "\"" + key + "\"\\s*:\\s*(\\d+)";
758        java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
759        java.util.regex.Matcher m = p.matcher(json);
760        return m.find() ? Integer.parseInt(m.group(1)) : -1;
761    }
762
763    /**
764     * Identifies a service using lsof.
765     */
766    private String identifyService(int checkPort) {
767        try {
768            ProcessBuilder pb = new ProcessBuilder("lsof", "-i", ":" + checkPort, "-sTCP:LISTEN");
769            pb.redirectErrorStream(true);
770            Process process = pb.start();
771
772            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
773                String line;
774                while ((line = reader.readLine()) != null) {
775                    if (!line.startsWith("COMMAND")) {
776                        String[] parts = line.split("\\s+");
777                        if (parts.length > 0) {
778                            return parts[0]; // Process name
779                        }
780                    }
781                }
782            }
783            process.waitFor();
784        } catch (Exception e) {
785            // lsof not available or failed
786        }
787
788        return "Unknown service";
789    }
790
791    /**
792     * Finds an available port.
793     */
794    private int findAvailablePort() {
795        for (int scanPort : SCAN_PORTS) {
796            try (Socket socket = new Socket("localhost", scanPort)) {
797                // Port is in use
798            } catch (Exception e) {
799                // Port is available
800                return scanPort;
801            }
802        }
803        return -1;
804    }
805
806    /**
807     * Information about a service running on a port.
808     */
809    private static class ServerInfo {
810        final int port;
811        boolean isH2Server = false;
812        boolean hasHttpApi = false;
813        int httpPort = -1;
814        String dbPath = null;
815        int sessionCount = -1;
816        String startedAt = null;
817        String version = null;
818        String serviceName = null;
819        String processInfo = null;
820
821        ServerInfo(int port) {
822            this.port = port;
823        }
824    }
825}