// Command gateway runs the CrossRelay local gateway: it serves the // Anthropic Messages API for Claude Code and the management API for the // CrossRelay desktop app. package main import ( "context" "encoding/json" "fmt" "log" "net" "net/http" "os" "os/signal" "path/filepath" "syscall" "time" "gitea.rutua.cn/kate522/CrossRelay/internal/config" "gitea.rutua.cn/kate522/CrossRelay/internal/gateway" ) // watchParentShutdown exits the gateway when the parent process goes // away. The Tauri shell spawns the gateway with a piped stdin and sets // CROSSRELAY_WATCH_PARENT=1; when the app exits, crashes, or is killed, // the OS closes the pipe and the gateway shuts down instead of lingering // as an orphan. func watchParentShutdown(httpSrv *http.Server, statePath string, done chan struct{}) { if os.Getenv("CROSSRELAY_WATCH_PARENT") != "1" { return } go func() { buf := make([]byte, 512) for { if _, err := os.Stdin.Read(buf); err != nil { break } } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = httpSrv.Shutdown(ctx) _ = os.Remove(statePath) close(done) }() } func main() { log.SetFlags(log.LstdFlags | log.Lmsgprefix) log.SetPrefix("crossrelay: ") store, err := config.Load() if err != nil { log.Fatalf("load config: %v", err) } addr := "127.0.0.1:0" cfg := store.Config() if cfg.Gateway.PortMode == config.PortModeFixed && cfg.Gateway.FixedPort > 0 { addr = fmt.Sprintf("127.0.0.1:%d", cfg.Gateway.FixedPort) } ln, err := net.Listen("tcp", addr) if err != nil { fmt.Fprintf(os.Stderr, "CROSSRELAY_LISTEN_FAILED %s\n", err) os.Exit(1) } port := ln.Addr().(*net.TCPAddr).Port srv := gateway.New(store, port) httpSrv := &http.Server{Handler: srv.Handler()} statePath := filepath.Join(config.Dir(), "state.json") if err := writeState(statePath, port); err != nil { log.Printf("write state file: %v", err) } // The desktop app parses this line to learn the actual port. fmt.Printf("CROSSRELAY_LISTENING 127.0.0.1:%d\n", port) shutdownDone := make(chan struct{}) watchParentShutdown(httpSrv, statePath, shutdownDone) go func() { sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) <-sig ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = httpSrv.Shutdown(ctx) _ = os.Remove(statePath) close(shutdownDone) }() if err := httpSrv.Serve(ln); err != nil && err != http.ErrServerClosed { fmt.Fprintf(os.Stderr, "CROSSRELAY_SERVE_FAILED %s\n", err) _ = os.Remove(statePath) os.Exit(1) } <-shutdownDone } // writeState records the running gateway for diagnostics. func writeState(path string, port int) error { state := map[string]any{ "pid": os.Getpid(), "port": port, "started_at": time.Now().UTC().Format(time.RFC3339), "version": gateway.Version, } data, err := json.MarshalIndent(state, "", " ") if err != nil { return err } if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return err } return os.WriteFile(path, data, 0o600) }