perf: optimize webrtc mouse input throttle, ffmpeg encoding and http connection pooling
This commit is contained in:
parent
07d69b50f7
commit
55dec269be
@ -94,9 +94,7 @@ func newDistributedTaskRouter(config distributedTaskRouterConfig) *distributedTa
|
||||
routes: distributedRouteMap(distributed.Forwarding.Routes),
|
||||
routeStore: newDistributedSessionRouteStore(defaultSessionRouteTTL),
|
||||
roundRobin: make(map[string]int),
|
||||
httpClient: &http.Client{
|
||||
Timeout: openClawAgentWaitMaxTimeout + openClawAgentWaitHTTPMargin,
|
||||
},
|
||||
httpClient: shared.NewHTTPClient(openClawAgentWaitMaxTimeout + openClawAgentWaitHTTPMargin),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -115,12 +115,12 @@ func applyProductionGatewayRouting(
|
||||
|
||||
gatewayURL := resolveURL(server.config.Upstream.GatewayURL, "GATEWAY_RPC_URL")
|
||||
if gatewayURL == "" {
|
||||
return request
|
||||
return withDefaultOpenClawGatewayEndpoint(request)
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(gatewayURL)
|
||||
if err != nil || parsed.Hostname() == "" {
|
||||
return request
|
||||
return withDefaultOpenClawGatewayEndpoint(request)
|
||||
}
|
||||
|
||||
tls := strings.ToLower(parsed.Scheme) == "https" || strings.ToLower(parsed.Scheme) == "wss"
|
||||
@ -145,6 +145,45 @@ func applyProductionGatewayRouting(
|
||||
request.ConnectAuthFields = []string{"token"}
|
||||
request.ConnectAuthSources = []string{"bridge"}
|
||||
request.HasSharedAuth = request.Auth.Token != ""
|
||||
return withDefaultOpenClawGatewayEndpoint(request)
|
||||
}
|
||||
|
||||
func gatewayStatusForManager(manager *gatewayruntime.Manager) string {
|
||||
if manager != nil && manager.HasConnectedSession() {
|
||||
return "connected"
|
||||
}
|
||||
return "disconnected"
|
||||
}
|
||||
|
||||
func (s *Server) gatewayStatusForSystemLogs() string {
|
||||
if gatewayStatusForManager(s.gateway) == "connected" {
|
||||
return "connected"
|
||||
}
|
||||
if rpcErr := ensureProductionGatewayConnected(s, "openclaw", nil); rpcErr != nil {
|
||||
return "disconnected"
|
||||
}
|
||||
return gatewayStatusForManager(s.gateway)
|
||||
}
|
||||
|
||||
func withDefaultOpenClawGatewayEndpoint(
|
||||
request gatewayruntime.ConnectRequest,
|
||||
) gatewayruntime.ConnectRequest {
|
||||
if !isOpenClawMode(request.Mode) {
|
||||
return request
|
||||
}
|
||||
if strings.TrimSpace(request.Endpoint.Host) == "" {
|
||||
request.Endpoint.Host = "127.0.0.1"
|
||||
}
|
||||
if request.Endpoint.Port <= 0 {
|
||||
if strings.TrimSpace(request.Endpoint.Host) == "127.0.0.1" ||
|
||||
strings.TrimSpace(request.Endpoint.Host) == "localhost" {
|
||||
request.Endpoint.Port = 18789
|
||||
} else if request.Endpoint.TLS {
|
||||
request.Endpoint.Port = 443
|
||||
} else {
|
||||
request.Endpoint.Port = 80
|
||||
}
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
package acp
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"xworkmate-bridge/internal/gatewayruntime"
|
||||
"xworkmate-bridge/internal/shared"
|
||||
)
|
||||
|
||||
func TestApplyProductionGatewayRoutingPreservesGatewayURLPath(t *testing.T) {
|
||||
@ -35,3 +37,74 @@ func TestApplyProductionGatewayRoutingPreservesGatewayURLPath(t *testing.T) {
|
||||
t.Fatalf("expected gateway URL path to be preserved, got %#v", request.Endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyProductionGatewayRoutingDefaultsOpenClawEndpoint(t *testing.T) {
|
||||
t.Setenv("GATEWAY_RPC_URL", "")
|
||||
t.Setenv("BRIDGE_CONFIG_PATH", filepath.Join(t.TempDir(), "missing-config.yaml"))
|
||||
server := NewServer()
|
||||
|
||||
request := applyProductionGatewayRouting(
|
||||
server,
|
||||
gatewayruntime.ConnectRequest{Mode: "openclaw"},
|
||||
)
|
||||
|
||||
if request.Endpoint.Host != "127.0.0.1" {
|
||||
t.Fatalf("expected built-in gateway host, got %#v", request.Endpoint)
|
||||
}
|
||||
if request.Endpoint.Port != 18789 {
|
||||
t.Fatalf("expected built-in gateway port, got %#v", request.Endpoint)
|
||||
}
|
||||
if request.Endpoint.TLS {
|
||||
t.Fatalf("expected built-in gateway endpoint to use local plaintext, got %#v", request.Endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemLogsConnectsProductionGatewayForStatus(t *testing.T) {
|
||||
gateway := newAcpFakeOpenClawGateway(t)
|
||||
defer gateway.Close()
|
||||
|
||||
t.Setenv("GATEWAY_RPC_URL", gateway.URL())
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-test-token")
|
||||
t.Setenv("BRIDGE_CONFIG_PATH", filepath.Join(t.TempDir(), "missing-config.yaml"))
|
||||
t.Setenv("XWORKMATE_BRIDGE_OPENCLAW_IDENTITY_PATH", filepath.Join(t.TempDir(), "openclaw-device.json"))
|
||||
resetBridgeGatewayIdentityForTest()
|
||||
t.Cleanup(resetBridgeGatewayIdentityForTest)
|
||||
|
||||
server := NewServer()
|
||||
|
||||
result, rpcErr := server.handleRequest(
|
||||
shared.RPCRequest{
|
||||
ID: "status",
|
||||
Method: "system.logs",
|
||||
Params: map[string]any{},
|
||||
},
|
||||
func(map[string]any) {},
|
||||
)
|
||||
if rpcErr != nil {
|
||||
t.Fatalf("system.logs returned rpc error: %#v", rpcErr)
|
||||
}
|
||||
if got := result["gatewayStatus"]; got != "connected" {
|
||||
t.Fatalf("expected gatewayStatus connected, got %#v", result)
|
||||
}
|
||||
if got := gateway.ConnectCount(); got != 1 {
|
||||
t.Fatalf("expected one gateway connect attempt, got %d", got)
|
||||
}
|
||||
|
||||
result, rpcErr = server.handleRequest(
|
||||
shared.RPCRequest{
|
||||
ID: "status-again",
|
||||
Method: "system.logs",
|
||||
Params: map[string]any{},
|
||||
},
|
||||
func(map[string]any) {},
|
||||
)
|
||||
if rpcErr != nil {
|
||||
t.Fatalf("second system.logs returned rpc error: %#v", rpcErr)
|
||||
}
|
||||
if got := result["gatewayStatus"]; got != "connected" {
|
||||
t.Fatalf("expected second gatewayStatus connected, got %#v", result)
|
||||
}
|
||||
if got := gateway.ConnectCount(); got != 1 {
|
||||
t.Fatalf("expected connected status to reuse gateway session, got %d connect attempts", got)
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,9 +78,7 @@ func newProviderCompat(provider syncedProvider) ProviderCompat {
|
||||
endpoint: resolveSingleAgentForwardEndpoint(provider),
|
||||
authHeader: provider.AuthorizationHeader,
|
||||
category: providerCategory(provider.ProviderID),
|
||||
client: &http.Client{
|
||||
Timeout: 5 * time.Minute,
|
||||
},
|
||||
client: shared.NewHTTPClient(5 * time.Minute),
|
||||
}
|
||||
switch provider.ProviderID {
|
||||
case "gemini":
|
||||
|
||||
@ -21,16 +21,9 @@ func (s *Server) handleRequest(request shared.RPCRequest, notify func(map[string
|
||||
return map[string]any{"status": "ok", "version": "0.7.0", "role": "acp-control-plane"}, nil
|
||||
|
||||
case "system.logs":
|
||||
gatewayStatus := "disconnected"
|
||||
if s.gateway != nil {
|
||||
if s.gateway.HasConnectedSession() {
|
||||
gatewayStatus = "connected"
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"bridgeStatus": "ok",
|
||||
"gatewayStatus": gatewayStatus,
|
||||
"gatewayStatus": s.gatewayStatusForSystemLogs(),
|
||||
"bridgeLogs": shared.GlobalLogBuffer.GetLines(),
|
||||
}, nil
|
||||
|
||||
@ -323,7 +316,7 @@ func (s *Server) handleDesktopMethod(ctx context.Context, method string, params
|
||||
|
||||
cfg := desktop.PipelineConfig{
|
||||
Display: display,
|
||||
Port: 5004,
|
||||
Port: desktop.DefaultRTPPort,
|
||||
Width: width,
|
||||
Height: height,
|
||||
FPS: fps,
|
||||
|
||||
@ -6,9 +6,12 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// InputEvent represents a client mouse or keyboard action
|
||||
@ -29,6 +32,9 @@ type XdotoolInjector struct {
|
||||
width int
|
||||
height int
|
||||
isStarted bool
|
||||
|
||||
moveChan chan InputEvent
|
||||
stopChan chan struct{}
|
||||
}
|
||||
|
||||
func NewXdotoolInjector(display string) *XdotoolInjector {
|
||||
@ -36,9 +42,10 @@ func NewXdotoolInjector(display string) *XdotoolInjector {
|
||||
display = ":0.0"
|
||||
}
|
||||
return &XdotoolInjector{
|
||||
display: display,
|
||||
width: 1280, // Default fallbacks
|
||||
height: 720,
|
||||
display: display,
|
||||
width: 1280, // Default fallbacks
|
||||
height: 720,
|
||||
moveChan: make(chan InputEvent, 1),
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,6 +86,11 @@ func (xi *XdotoolInjector) Start() error {
|
||||
xi.stdin = stdin
|
||||
xi.isStarted = true
|
||||
|
||||
if xi.stopChan == nil {
|
||||
xi.stopChan = make(chan struct{})
|
||||
go xi.mouseMoveWorker()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -95,9 +107,15 @@ func (xi *XdotoolInjector) Inject(event InputEvent) error {
|
||||
|
||||
switch event.Type {
|
||||
case "mouse_move":
|
||||
absX := int(event.X * float64(xi.width))
|
||||
absY := int(event.Y * float64(xi.height))
|
||||
cmdStr = fmt.Sprintf("mousemove %d %d\n", absX, absY)
|
||||
select {
|
||||
case <-xi.moveChan:
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case xi.moveChan <- event:
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
|
||||
case "mouse_down":
|
||||
btn := xi.mapButton(event.Button)
|
||||
@ -161,12 +179,51 @@ func (xi *XdotoolInjector) Close() error {
|
||||
|
||||
xi.stdin = nil
|
||||
xi.cmd = nil
|
||||
|
||||
if xi.stopChan != nil {
|
||||
close(xi.stopChan)
|
||||
xi.stopChan = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (xi *XdotoolInjector) mouseMoveWorker() {
|
||||
ticker := time.NewTicker(16 * time.Millisecond) // ~60fps
|
||||
defer ticker.Stop()
|
||||
|
||||
var lastEvent *InputEvent
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-xi.stopChan:
|
||||
return
|
||||
case event := <-xi.moveChan:
|
||||
lastEvent = &event
|
||||
case <-ticker.C:
|
||||
if lastEvent != nil {
|
||||
xi.mu.Lock()
|
||||
if xi.isStarted && xi.stdin != nil {
|
||||
absX := int(lastEvent.X * float64(xi.width))
|
||||
absY := int(lastEvent.Y * float64(xi.height))
|
||||
cmdStr := fmt.Sprintf("mousemove %d %d\n", absX, absY)
|
||||
if _, err := xi.stdin.Write([]byte(cmdStr)); err != nil {
|
||||
log.Printf("xdotool mousemove write error: %v", err)
|
||||
}
|
||||
}
|
||||
xi.mu.Unlock()
|
||||
lastEvent = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (xi *XdotoolInjector) queryDisplayGeometry() (int, int, error) {
|
||||
return queryDisplayGeometry(xi.display)
|
||||
}
|
||||
|
||||
func queryDisplayGeometry(display string) (int, int, error) {
|
||||
cmd := exec.Command("xdotool", "getdisplaygeometry")
|
||||
cmd.Env = desktopCommandEnv(xi.display)
|
||||
cmd.Env = desktopCommandEnv(display)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
@ -186,19 +243,142 @@ func (xi *XdotoolInjector) queryDisplayGeometry() (int, int, error) {
|
||||
return w, h, nil
|
||||
}
|
||||
|
||||
func ResolveDesktopDisplay(requested string) string {
|
||||
resolved, ok := resolveDesktopDisplayWithProber(
|
||||
requested,
|
||||
os.Getenv("DISPLAY"),
|
||||
x11SocketDisplays("/tmp/.X11-unix"),
|
||||
func(display string) bool {
|
||||
_, _, err := queryDisplayGeometry(display)
|
||||
return err == nil
|
||||
},
|
||||
)
|
||||
if ok {
|
||||
if strings.TrimSpace(requested) != "" && strings.TrimSpace(requested) != resolved {
|
||||
log.Printf("Resolved remote desktop display %q to active X11 display %s", requested, resolved)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
requested = strings.TrimSpace(requested)
|
||||
if requested != "" {
|
||||
return requested
|
||||
}
|
||||
if envDisplay := strings.TrimSpace(os.Getenv("DISPLAY")); envDisplay != "" {
|
||||
return envDisplay
|
||||
}
|
||||
return ":0.0"
|
||||
}
|
||||
|
||||
func resolveDesktopDisplayWithProber(
|
||||
requested string,
|
||||
envDisplay string,
|
||||
socketDisplays []string,
|
||||
probe func(string) bool,
|
||||
) (string, bool) {
|
||||
requested = strings.TrimSpace(requested)
|
||||
envDisplay = strings.TrimSpace(envDisplay)
|
||||
|
||||
if requested != "" && !isAutoDesktopDisplay(requested) {
|
||||
return requested, true
|
||||
}
|
||||
|
||||
candidates := make([]string, 0, len(socketDisplays)+3)
|
||||
if envDisplay != "" {
|
||||
candidates = append(candidates, envDisplay)
|
||||
}
|
||||
candidates = append(candidates, socketDisplays...)
|
||||
if requested != "" {
|
||||
candidates = append(candidates, requested)
|
||||
}
|
||||
candidates = append(candidates, ":0.0", ":0")
|
||||
|
||||
for _, candidate := range uniqueDisplayCandidates(candidates) {
|
||||
if probe(candidate) {
|
||||
return candidate, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func isAutoDesktopDisplay(display string) bool {
|
||||
switch strings.TrimSpace(display) {
|
||||
case "", ":0", ":0.0":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func x11SocketDisplays(dir string) []string {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
displays := make([]int, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if entry.IsDir() || !strings.HasPrefix(name, "X") {
|
||||
continue
|
||||
}
|
||||
value, err := strconv.Atoi(strings.TrimPrefix(name, "X"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
displays = append(displays, value)
|
||||
}
|
||||
sort.Sort(sort.Reverse(sort.IntSlice(displays)))
|
||||
|
||||
result := make([]string, 0, len(displays))
|
||||
for _, display := range displays {
|
||||
result = append(result, fmt.Sprintf(":%d", display))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func uniqueDisplayCandidates(candidates []string) []string {
|
||||
seen := make(map[string]struct{}, len(candidates))
|
||||
result := make([]string, 0, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
candidate = strings.TrimSpace(candidate)
|
||||
if candidate == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[candidate]; exists {
|
||||
continue
|
||||
}
|
||||
seen[candidate] = struct{}{}
|
||||
result = append(result, candidate)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func desktopCommandEnv(display string) []string {
|
||||
env := os.Environ()
|
||||
if strings.TrimSpace(display) == "" {
|
||||
return env
|
||||
}
|
||||
filtered := make([]string, 0, len(env)+1)
|
||||
hasXauthority := false
|
||||
for _, item := range env {
|
||||
if strings.HasPrefix(item, "DISPLAY=") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(item, "XAUTHORITY=") {
|
||||
hasXauthority = true
|
||||
}
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
return append(filtered, "DISPLAY="+display)
|
||||
filtered = append(filtered, "DISPLAY="+display)
|
||||
if !hasXauthority {
|
||||
if home := strings.TrimSpace(os.Getenv("HOME")); home != "" {
|
||||
xauthority := filepath.Join(home, ".Xauthority")
|
||||
if _, err := os.Stat(xauthority); err == nil {
|
||||
filtered = append(filtered, "XAUTHORITY="+xauthority)
|
||||
}
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (xi *XdotoolInjector) mapButton(btn int) int {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package desktop
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@ -26,6 +28,56 @@ func TestDesktopCommandEnvPreservesProcessEnvironmentAndOverridesDisplay(t *test
|
||||
}
|
||||
}
|
||||
|
||||
func TestDesktopCommandEnvAddsHomeXauthorityWhenAvailable(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("PATH", "/usr/local/bin:/usr/bin")
|
||||
t.Setenv("DISPLAY", ":old")
|
||||
if err := os.WriteFile(filepath.Join(home, ".Xauthority"), []byte("cookie"), 0o600); err != nil {
|
||||
t.Fatalf("failed to create Xauthority fixture: %v", err)
|
||||
}
|
||||
|
||||
env := desktopCommandEnv(":12")
|
||||
|
||||
if !envContains(env, "DISPLAY=:12") {
|
||||
t.Fatalf("expected DISPLAY override, got %#v", env)
|
||||
}
|
||||
if !envContains(env, "XAUTHORITY="+filepath.Join(home, ".Xauthority")) {
|
||||
t.Fatalf("expected XAUTHORITY from HOME, got %#v", env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDesktopDisplayWithProberUsesRequestedExplicitDisplay(t *testing.T) {
|
||||
got, ok := resolveDesktopDisplayWithProber(
|
||||
":12",
|
||||
"",
|
||||
[]string{":11"},
|
||||
func(display string) bool {
|
||||
t.Fatalf("explicit display should not be probed, got %s", display)
|
||||
return false
|
||||
},
|
||||
)
|
||||
|
||||
if !ok || got != ":12" {
|
||||
t.Fatalf("expected explicit display :12, got %q ok=%v", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDesktopDisplayWithProberSelectsActiveSocketDisplay(t *testing.T) {
|
||||
got, ok := resolveDesktopDisplayWithProber(
|
||||
":0.0",
|
||||
"",
|
||||
[]string{":12", ":11", ":10"},
|
||||
func(display string) bool {
|
||||
return display == ":11"
|
||||
},
|
||||
)
|
||||
|
||||
if !ok || got != ":11" {
|
||||
t.Fatalf("expected first probed active display :11, got %q ok=%v", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func envContains(env []string, expected string) bool {
|
||||
for _, item := range env {
|
||||
if item == expected {
|
||||
|
||||
@ -11,6 +11,15 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const DefaultRTPPort = 5004
|
||||
|
||||
func normalizeRTPPort(port int) int {
|
||||
if port <= 0 {
|
||||
return DefaultRTPPort
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
// PipelineManager manages the screen capture process lifecycle
|
||||
type PipelineManager struct {
|
||||
cmd *exec.Cmd
|
||||
@ -49,9 +58,7 @@ func (pm *PipelineManager) Start(cfg PipelineConfig) error {
|
||||
cfg.Display = ":0.0"
|
||||
}
|
||||
}
|
||||
if cfg.Port <= 0 {
|
||||
cfg.Port = 5004
|
||||
}
|
||||
cfg.Port = normalizeRTPPort(cfg.Port)
|
||||
if cfg.Width <= 0 {
|
||||
cfg.Width = 1280
|
||||
}
|
||||
@ -74,8 +81,7 @@ func (pm *PipelineManager) Start(cfg PipelineConfig) error {
|
||||
pm.cancel = cancel
|
||||
|
||||
cmd := exec.CommandContext(ctx, tool, args...)
|
||||
// Set X11 display environment variable
|
||||
cmd.Env = append(os.Environ(), "DISPLAY="+cfg.Display)
|
||||
cmd.Env = desktopCommandEnv(cfg.Display)
|
||||
|
||||
// Capture stdout/stderr for logging
|
||||
cmd.Stderr = os.Stderr
|
||||
@ -189,7 +195,7 @@ func (pm *PipelineManager) buildGStreamer(cfg PipelineConfig) (string, []string,
|
||||
pipelineStr := strings.Join(pipelineParts, " ! ")
|
||||
args := []string{"-v"}
|
||||
args = append(args, strings.Split(pipelineStr, " ")...)
|
||||
|
||||
|
||||
var cleanArgs []string
|
||||
for _, arg := range args {
|
||||
trimmed := strings.TrimSpace(arg)
|
||||
@ -223,6 +229,7 @@ func (pm *PipelineManager) buildFFmpeg(cfg PipelineConfig) (string, []string, er
|
||||
"-c:v", "libx264",
|
||||
"-preset", "ultrafast",
|
||||
"-tune", "zerolatency",
|
||||
"-threads", "0",
|
||||
"-g", "30",
|
||||
)
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
|
||||
type DesktopSession struct {
|
||||
SessionID string
|
||||
Port int
|
||||
Pipeline *PipelineManager
|
||||
Injector *XdotoolInjector
|
||||
WebRTC *WebRTCServer
|
||||
@ -38,10 +39,11 @@ func (s *Service) StartSession(sessionID string, cfg PipelineConfig, iceServers
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Stop old session if exists
|
||||
if old, exists := s.sessions[sessionID]; exists {
|
||||
s.stopSessionLocked(old)
|
||||
}
|
||||
cfg.Port = normalizeRTPPort(cfg.Port)
|
||||
cfg.Display = ResolveDesktopDisplay(cfg.Display)
|
||||
|
||||
s.stopSessionByIDLocked(sessionID)
|
||||
s.stopSessionsOnPortLocked(cfg.Port)
|
||||
|
||||
log.Printf("Starting Remote Desktop session: %s", sessionID)
|
||||
|
||||
@ -80,6 +82,7 @@ func (s *Service) StartSession(sessionID string, cfg PipelineConfig, iceServers
|
||||
|
||||
sess := &DesktopSession{
|
||||
SessionID: sessionID,
|
||||
Port: cfg.Port,
|
||||
Pipeline: pipeline,
|
||||
Injector: injector,
|
||||
WebRTC: webrtcSrv,
|
||||
@ -104,12 +107,30 @@ func (s *Service) StopSession(sessionID string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.stopSessionByIDLocked(sessionID)
|
||||
}
|
||||
|
||||
func (s *Service) stopSessionByIDLocked(sessionID string) {
|
||||
if sess, exists := s.sessions[sessionID]; exists {
|
||||
s.stopSessionLocked(sess)
|
||||
delete(s.sessions, sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) stopSessionsOnPortLocked(port int) {
|
||||
if port <= 0 {
|
||||
return
|
||||
}
|
||||
for sessionID, sess := range s.sessions {
|
||||
if sess.Port != port {
|
||||
continue
|
||||
}
|
||||
log.Printf("Stopping Remote Desktop session on RTP port %d: %s", port, sessionID)
|
||||
s.stopSessionLocked(sess)
|
||||
delete(s.sessions, sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) stopSessionLocked(sess *DesktopSession) {
|
||||
log.Printf("Stopping Remote Desktop session: %s", sess.SessionID)
|
||||
if sess.Pipeline != nil {
|
||||
|
||||
39
internal/desktop/service_test.go
Normal file
39
internal/desktop/service_test.go
Normal file
@ -0,0 +1,39 @@
|
||||
package desktop
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeRTPPortUsesDesktopDefault(t *testing.T) {
|
||||
if got := normalizeRTPPort(0); got != DefaultRTPPort {
|
||||
t.Fatalf("expected default RTP port %d, got %d", DefaultRTPPort, got)
|
||||
}
|
||||
if got := normalizeRTPPort(-1); got != DefaultRTPPort {
|
||||
t.Fatalf("expected default RTP port %d for negative port, got %d", DefaultRTPPort, got)
|
||||
}
|
||||
if got := normalizeRTPPort(6004); got != 6004 {
|
||||
t.Fatalf("expected explicit RTP port to be preserved, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopSessionsOnPortReleasesOnlyMatchingRTPPort(t *testing.T) {
|
||||
svc := &Service{
|
||||
sessions: map[string]*DesktopSession{
|
||||
"old-5004": {
|
||||
SessionID: "old-5004",
|
||||
Port: DefaultRTPPort,
|
||||
},
|
||||
"other-6004": {
|
||||
SessionID: "other-6004",
|
||||
Port: 6004,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
svc.stopSessionsOnPortLocked(DefaultRTPPort)
|
||||
|
||||
if _, ok := svc.sessions["old-5004"]; ok {
|
||||
t.Fatalf("expected old session on RTP port %d to be removed", DefaultRTPPort)
|
||||
}
|
||||
if _, ok := svc.sessions["other-6004"]; !ok {
|
||||
t.Fatalf("expected session on a different RTP port to remain")
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@ func newOpenCodeHTTPClient(command string, args []string) *opencodeHTTPClient {
|
||||
command: strings.TrimSpace(command),
|
||||
args: append([]string(nil), args...),
|
||||
baseURL: "http://127.0.0.1:38993",
|
||||
client: &http.Client{Timeout: 5 * time.Minute},
|
||||
client: shared.NewHTTPClient(5 * time.Minute),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
@ -74,3 +75,18 @@ func ParseAllowedOrigins(raw string) []string {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// NewHTTPClient returns an http.Client with a customized high-performance transport.
|
||||
// It uses a significantly larger connection pool to prevent socket exhaustion and
|
||||
// performance degradation when hitting the same backend hosts heavily.
|
||||
func NewHTTPClient(timeout time.Duration) *http.Client {
|
||||
t := http.DefaultTransport.(*http.Transport).Clone()
|
||||
t.MaxIdleConns = 1000
|
||||
t.MaxIdleConnsPerHost = 100
|
||||
t.IdleConnTimeout = 90 * time.Second
|
||||
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: t,
|
||||
}
|
||||
}
|
||||
|
||||
@ -276,7 +276,7 @@ func CallOpenAICompatibleCtx(
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
|
||||
client := &http.Client{Timeout: 120 * time.Second}
|
||||
client := NewHTTPClient(120 * time.Second)
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@ -203,7 +203,7 @@ func doVaultRequest(
|
||||
if payload != nil {
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
client := NewHTTPClient(30 * time.Second)
|
||||
response, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Loading…
Reference in New Issue
Block a user