From cc7e1e8921117c374296ebe59608c7b2039d7f14 Mon Sep 17 00:00:00 2001 From: Emanuel Casco Date: Sat, 16 May 2026 16:09:51 +0200 Subject: [PATCH] feat: improve Windows support --- Makefile | 6 +++-- build.bat | 9 +++++++ cmd/ocgo/listener_pid_netstat.go | 40 ++++++++++++++++++++++++++++++++ cmd/ocgo/listener_pid_unix.go | 27 +++++++++++++++++++++ cmd/ocgo/listener_pid_windows.go | 19 +++++++++++++++ cmd/ocgo/main.go | 25 ++++---------------- cmd/ocgo/main_test.go | 27 +++++++++++++++++++++ 7 files changed, 130 insertions(+), 23 deletions(-) create mode 100644 build.bat create mode 100644 cmd/ocgo/listener_pid_netstat.go create mode 100644 cmd/ocgo/listener_pid_unix.go create mode 100644 cmd/ocgo/listener_pid_windows.go diff --git a/Makefile b/Makefile index 76681db..2e5877c 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,14 @@ .PHONY: build run test clean install release VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) +EXE := $(shell go env GOEXE) +OCGO_BIN := bin/ocgo$(EXE) GOBIN := $(shell go env GOBIN) GOPATH := $(shell go env GOPATH) INSTALL_DIR := $(if $(GOBIN),$(GOBIN),$(GOPATH)/bin) build: - go build -ldflags "-X main.version=$(VERSION)" -o bin/ocgo ./cmd/ocgo + go build -ldflags "-X main.version=$(VERSION)" -o $(OCGO_BIN) ./cmd/ocgo run: go run ./cmd/ocgo @@ -19,7 +21,7 @@ clean: install: build mkdir -p "$(INSTALL_DIR)" - install -m 0755 bin/ocgo "$(INSTALL_DIR)/ocgo" + install -m 0755 "$(OCGO_BIN)" "$(INSTALL_DIR)/ocgo$(EXE)" release: @[ -n "$(TAG)" ] || (echo "Usage: make release TAG=v0.1.0" && exit 1) diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..6c1c7d3 --- /dev/null +++ b/build.bat @@ -0,0 +1,9 @@ +@echo off +setlocal + +for /f "delims=" %%v in ('git describe --tags --always --dirty 2^>NUL') do set "OCGO_VERSION=%%v" +if not defined OCGO_VERSION set "OCGO_VERSION=dev" + +if not exist bin mkdir bin +go build -ldflags "-X main.version=%OCGO_VERSION%" -o bin\ocgo.exe .\cmd\ocgo +exit /b %ERRORLEVEL% diff --git a/cmd/ocgo/listener_pid_netstat.go b/cmd/ocgo/listener_pid_netstat.go new file mode 100644 index 0000000..62a8aed --- /dev/null +++ b/cmd/ocgo/listener_pid_netstat.go @@ -0,0 +1,40 @@ +package main + +import ( + "errors" + "strconv" + "strings" +) + +func parseWindowsNetstatPID(output string, port int) (int, error) { + wanted := strconv.Itoa(port) + for _, line := range strings.Split(output, "\n") { + fields := strings.Fields(line) + if len(fields) < 5 || !strings.EqualFold(fields[0], "tcp") { + continue + } + if !strings.EqualFold(fields[len(fields)-2], "listening") { + continue + } + if !netstatAddressUsesPort(fields[1], wanted) { + continue + } + pid, err := strconv.Atoi(fields[len(fields)-1]) + if err == nil && pid > 0 { + return pid, nil + } + } + return 0, errors.New("no listener found") +} + +func netstatAddressUsesPort(address, port string) bool { + address = strings.TrimSpace(address) + if address == "" { + return false + } + if strings.HasPrefix(address, "[") { + return strings.HasSuffix(address, "]:"+port) + } + idx := strings.LastIndex(address, ":") + return idx >= 0 && address[idx+1:] == port +} diff --git a/cmd/ocgo/listener_pid_unix.go b/cmd/ocgo/listener_pid_unix.go new file mode 100644 index 0000000..9e5a968 --- /dev/null +++ b/cmd/ocgo/listener_pid_unix.go @@ -0,0 +1,27 @@ +//go:build !windows + +package main + +import ( + "errors" + "os/exec" + "strconv" + "strings" +) + +func findListenerPID(port int) (int, error) { + if port == 0 { + return 0, errors.New("missing port") + } + out, err := exec.Command("lsof", "-nP", "-tiTCP:"+strconv.Itoa(port), "-sTCP:LISTEN").Output() + if err != nil { + return 0, err + } + for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { + pid, err := strconv.Atoi(strings.TrimSpace(line)) + if err == nil && pid > 0 { + return pid, nil + } + } + return 0, errors.New("no listener found") +} diff --git a/cmd/ocgo/listener_pid_windows.go b/cmd/ocgo/listener_pid_windows.go new file mode 100644 index 0000000..d77126e --- /dev/null +++ b/cmd/ocgo/listener_pid_windows.go @@ -0,0 +1,19 @@ +//go:build windows + +package main + +import ( + "errors" + "os/exec" +) + +func findListenerPID(port int) (int, error) { + if port == 0 { + return 0, errors.New("missing port") + } + out, err := exec.Command("netstat", "-ano", "-p", "tcp").Output() + if err != nil { + return 0, err + } + return parseWindowsNetstatPID(string(out), port) +} diff --git a/cmd/ocgo/main.go b/cmd/ocgo/main.go index 57ff458..17f8b91 100644 --- a/cmd/ocgo/main.go +++ b/cmd/ocgo/main.go @@ -330,6 +330,10 @@ func statusCmd() *cobra.Command { fmt.Printf("Proxy is running on %s:%d (PID %d)\n", cfg.Host, cfg.Port, pid) return } + if pid, err := findListenerPID(cfg.Port); err == nil { + fmt.Printf("Proxy is running on %s:%d (PID %d, discovered from listener)\n", cfg.Host, cfg.Port, pid) + return + } fmt.Printf("Proxy is running on %s:%d (no ocgo PID file)\n", cfg.Host, cfg.Port) }} } @@ -1508,24 +1512,3 @@ func readPID() (int, error) { _, err = fmt.Sscan(string(b), &pid) return pid, err } - -func findListenerPID(port int) (int, error) { - if port == 0 { - return 0, errors.New("missing port") - } - out, err := exec.Command("lsof", "-nP", "-tiTCP:"+strconv.Itoa(port), "-sTCP:LISTEN").Output() - if err != nil { - return 0, err - } - for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - pid, err := strconv.Atoi(line) - if err == nil && pid > 0 { - return pid, nil - } - } - return 0, errors.New("no listener found") -} diff --git a/cmd/ocgo/main_test.go b/cmd/ocgo/main_test.go index 4621517..124427a 100644 --- a/cmd/ocgo/main_test.go +++ b/cmd/ocgo/main_test.go @@ -262,6 +262,33 @@ func contentString(v any) string { return s } +func TestParseWindowsNetstatPID(t *testing.T) { + output := strings.Join([]string{ + "Proto Local Address Foreign Address State PID", + "TCP 127.0.0.1:3456 0.0.0.0:0 LISTENING 4321", + "TCP [::1]:9999 [::]:0 LISTENING 8765", + "TCP 127.0.0.1:34560 0.0.0.0:0 LISTENING 1111", + }, "\n") + pid, err := parseWindowsNetstatPID(output, 3456) + if err != nil { + t.Fatal(err) + } + if pid != 4321 { + t.Fatalf("pid = %d, want 4321", pid) + } +} + +func TestParseWindowsNetstatPIDMatchesIPv6(t *testing.T) { + output := "TCP [::]:3456 [::]:0 LISTENING 2468\n" + pid, err := parseWindowsNetstatPID(output, 3456) + if err != nil { + t.Fatal(err) + } + if pid != 2468 { + t.Fatalf("pid = %d, want 2468", pid) + } +} + func TestStreamAnthropicForwardsToolCalls(t *testing.T) { reasoningContentCache.Lock() reasoningContentCache.byCallID = map[string]string{}