feat: improve Windows support

This commit is contained in:
Emanuel Casco 2026-05-16 16:09:51 +02:00
parent e2d9bb5038
commit cc7e1e8921
7 changed files with 130 additions and 23 deletions

View file

@ -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)

9
build.bat Normal file
View file

@ -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%

View file

@ -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
}

View file

@ -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")
}

View file

@ -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)
}

View file

@ -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")
}

View file

@ -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{}