fd6c1c4e0d
Full Go implementation under go/ replacing Python pyte with go-te: - HTTP server with WebSocket protocol, SSE, screenshot SVG rendering - PTY terminal sessions and Docker exec sessions - Docker watcher (label-based container discovery + event stream) - CPU stats collection with sparkline SVG rendering - Session manager with TwoWayMap routing and replay buffers - C1 normalization, DA filtering, identity generation, theme palettes Audit fixes for 9 concurrency/correctness issues: - HTTP transport leak: shared client pool for Docker socket calls - WebSocket concurrent writes: all writes routed through send channel - Closed channel panic: atomic.Bool guard on enqueueWSData - GetFirstRunningSession: use UnsafeForward under SessionManager lock - NewSession TOCTOU: re-check routeKey after re-acquiring lock - waitErr data race: protect with mutex in both session types - Replay buffer fragmentation: copy to new slice on eviction - go-te dirty tracking: check screen.Dirty before incrementing counter - Identity modulo bias: rejection sampling for uniform distribution All Go tests pass (including -race). Python baseline unchanged (397 tests). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
91 lines
2.0 KiB
Go
91 lines
2.0 KiB
Go
package webterm
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
sharedClientsMu sync.RWMutex
|
|
sharedClients = map[string]*http.Client{}
|
|
)
|
|
|
|
const defaultDockerSocket = "/var/run/docker.sock"
|
|
|
|
func DockerSocketPath() string {
|
|
dockerHost := strings.TrimSpace(os.Getenv(DockerHostEnv))
|
|
if dockerHost == "" {
|
|
return defaultDockerSocket
|
|
}
|
|
if strings.HasPrefix(dockerHost, "unix://") {
|
|
return strings.TrimPrefix(dockerHost, "unix://")
|
|
}
|
|
if strings.HasPrefix(dockerHost, "/") {
|
|
return dockerHost
|
|
}
|
|
return defaultDockerSocket
|
|
}
|
|
|
|
func newUnixHTTPClient(socketPath string, timeout time.Duration) *http.Client {
|
|
transport := &http.Transport{
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
var dialer net.Dialer
|
|
return dialer.DialContext(ctx, "unix", socketPath)
|
|
},
|
|
}
|
|
return &http.Client{Transport: transport, Timeout: timeout}
|
|
}
|
|
|
|
func sharedUnixClient(socketPath string) *http.Client {
|
|
sharedClientsMu.RLock()
|
|
client, ok := sharedClients[socketPath]
|
|
sharedClientsMu.RUnlock()
|
|
if ok {
|
|
return client
|
|
}
|
|
sharedClientsMu.Lock()
|
|
defer sharedClientsMu.Unlock()
|
|
if client, ok = sharedClients[socketPath]; ok {
|
|
return client
|
|
}
|
|
client = newUnixHTTPClient(socketPath, 15*time.Second)
|
|
sharedClients[socketPath] = client
|
|
return client
|
|
}
|
|
|
|
func unixJSONRequest(socketPath, method, path string, payload any) (int, []byte, error) {
|
|
var body io.Reader
|
|
if payload != nil {
|
|
data, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
body = bytes.NewReader(data)
|
|
}
|
|
req, err := http.NewRequest(method, "http://unix"+path, body)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
if payload != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
resp, err := sharedUnixClient(socketPath).Do(req)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
return resp.StatusCode, respBody, nil
|
|
}
|