feat: complete Go port with go-te terminal emulator
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>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user