From 14a5da8c45f2ceb887c70508dbe0483d0cec0abf Mon Sep 17 00:00:00 2001 From: liugangjian Date: Wed, 4 Mar 2026 21:43:58 +0800 Subject: [PATCH] Fix #197 - Wait for gateway to be responsive before reporting success MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modify handleStartGateway to poll the gateway's health endpoint to confirm the server is actually responsive before returning 'OK' to the client. This resolves the issue where the launcher reported that it started the server too early before the port was ready for requests. 🤖 AI Assisted - Human designed the solution, AI helped implement it --- .../internal/server/process.go | 56 +++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/cmd/picoclaw-launcher/internal/server/process.go b/cmd/picoclaw-launcher/internal/server/process.go index bc2129bf5..9c7c8a49f 100644 --- a/cmd/picoclaw-launcher/internal/server/process.go +++ b/cmd/picoclaw-launcher/internal/server/process.go @@ -73,7 +73,7 @@ func handleStartGateway(w http.ResponseWriter, r *http.Request) { return } - // Read stdout and stderr into the log buffer + // Start reading stdout/stderr logs go scanPipe(stdoutPipe, gatewayLogs) go scanPipe(stderrPipe, gatewayLogs) @@ -84,14 +84,62 @@ func handleStartGateway(w http.ResponseWriter, r *http.Request) { } }() - log.Printf("Started picoclaw gateway (PID: %d) from %s\n", cmd.Process.Pid, execPath) + // Get expected host and port from user's config file to check if gateway is actually running + absPath := r.URL.Query().Get("config_path") + if absPath == "" { + // Use default path to check health + absPath = config.PathFromHome(config.Filename) + } + host := "127.0.0.1" + port := 18790 + timeoutSeconds := 10 // Total timeout for waiting for health check + + // Load config to get the actual host and port the gateway is expected to run on + if cfg, cfgErr := config.LoadConfig(absPath); cfgErr == nil && cfg != nil { + if cfg.Gateway.Host != "" && cfg.Gateway.Host != "0.0.0.0" { + host = cfg.Gateway.Host + } + if cfg.Gateway.Port != 0 { + port = cfg.Gateway.Port + } + } + + // Poll the health endpoint to confirm gateway is actually responding + healthCheckURL := fmt.Sprintf("http://%s/health", net.JoinHostPort(host, strconv.Itoa(port))) + client := http.Client{Timeout: 2 * time.Second} + + // Retry for up to timeoutSeconds seconds + startTime := time.Now() + for time.Since(startTime) < time.Duration(timeoutSeconds)*time.Second { + resp, err := client.Get(healthCheckURL) + if err == nil && resp.StatusCode == http.StatusOK { + resp.Body.Close() + + // Gateway is responding, return success + log.Printf("Started picoclaw gateway (PID: %d) from %s and confirmed healthcheck on %s\n", cmd.Process.Pid, execPath, healthCheckURL) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "status": "ok", + "pid": cmd.Process.Pid, + }) + return + } else if err != nil && resp != nil { + resp.Body.Close() // Ensure response body is closed on status code error + } + + // Wait briefly before retrying + time.Sleep(500 * time.Millisecond) + } + + // Did not see the health check respond within the timeout + log.Printf("Gateway (PID: %d) started but didn't respond to health check at %s within %d seconds\n", cmd.Process.Pid, healthCheckURL, timeoutSeconds) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ - "status": "ok", + "status": "timeout", "pid": cmd.Process.Pid, + "error": fmt.Sprintf("Gateway started but didn't respond to health check at %s within %d seconds", healthCheckURL, timeoutSeconds), }) -} // scanPipe reads lines from r and appends them to buf. It returns when r reaches EOF. func scanPipe(r io.Reader, buf *LogBuffer) {