diff --git a/images/chromium-headless/image/Dockerfile b/images/chromium-headless/image/Dockerfile index d13029f2..144b4893 100644 --- a/images/chromium-headless/image/Dockerfile +++ b/images/chromium-headless/image/Dockerfile @@ -52,6 +52,7 @@ RUN set -xe; \ dbus-x11 \ xvfb \ x11-utils \ + xdotool \ software-properties-common \ supervisor; diff --git a/server/cmd/api/api/api.go b/server/cmd/api/api/api.go index 00364808..f29edf1a 100644 --- a/server/cmd/api/api/api.go +++ b/server/cmd/api/api/api.go @@ -36,6 +36,9 @@ type ApiService struct { // DevTools upstream manager (Chromium supervisord log tailer) upstreamMgr *devtoolsproxy.UpstreamManager stz scaletozero.Controller + + // inputMu serializes input-related operations (mouse, keyboard, screenshot) + inputMu sync.Mutex } var _ oapi.StrictServerInterface = (*ApiService)(nil) diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index 601c3bcd..bad48b06 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "strconv" + "time" "github.com/onkernel/kernel-images/server/lib/logger" oapi "github.com/onkernel/kernel-images/server/lib/oapi" @@ -19,9 +20,15 @@ func (s *ApiService) MoveMouse(ctx context.Context, request oapi.MoveMouseReques s.stz.Disable(ctx) defer s.stz.Enable(ctx) + // serialize input operations to avoid overlapping xdotool commands + s.inputMu.Lock() + defer s.inputMu.Unlock() + // Validate request body if request.Body == nil { - return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body is required"}}, nil + return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "request body is required"}, + }, nil } body := *request.Body @@ -29,15 +36,21 @@ func (s *ApiService) MoveMouse(ctx context.Context, request oapi.MoveMouseReques screenWidth, screenHeight, _, err := s.getCurrentResolution(ctx) if err != nil { log.Error("failed to get current resolution", "error", err) - return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to get current display resolution"}}, nil + return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to get current display resolution"}, + }, nil } // Ensure non-negative coordinates and within screen bounds if body.X < 0 || body.Y < 0 { - return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "coordinates must be non-negative"}}, nil + return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "coordinates must be non-negative"}, + }, nil } if body.X >= screenWidth || body.Y >= screenHeight { - return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: fmt.Sprintf("coordinates exceed screen bounds (max: %dx%d)", screenWidth-1, screenHeight-1)}}, nil + return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: fmt.Sprintf("coordinates exceed screen bounds (max: %dx%d)", screenWidth-1, screenHeight-1)}, + }, nil } // Build xdotool arguments @@ -65,7 +78,9 @@ func (s *ApiService) MoveMouse(ctx context.Context, request oapi.MoveMouseReques output, err := defaultXdoTool.Run(ctx, args...) if err != nil { log.Error("xdotool command failed", "err", err, "output", string(output)) - return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to move mouse"}}, nil + return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to move mouse"}, + }, nil } return oapi.MoveMouse200Response{}, nil @@ -77,9 +92,15 @@ func (s *ApiService) ClickMouse(ctx context.Context, request oapi.ClickMouseRequ s.stz.Disable(ctx) defer s.stz.Enable(ctx) + // serialize input operations to avoid overlapping xdotool commands + s.inputMu.Lock() + defer s.inputMu.Unlock() + // Validate request body if request.Body == nil { - return oapi.ClickMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body is required"}}, nil + return oapi.ClickMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "request body is required"}, + }, nil } body := *request.Body @@ -87,31 +108,39 @@ func (s *ApiService) ClickMouse(ctx context.Context, request oapi.ClickMouseRequ screenWidth, screenHeight, _, err := s.getCurrentResolution(ctx) if err != nil { log.Error("failed to get current resolution", "error", err) - return oapi.ClickMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to get current display resolution"}}, nil + return oapi.ClickMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to get current display resolution"}, + }, nil } // Ensure non-negative coordinates and within screen bounds if body.X < 0 || body.Y < 0 { - return oapi.ClickMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "coordinates must be non-negative"}}, nil + return oapi.ClickMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "coordinates must be non-negative"}, + }, nil } if body.X >= screenWidth || body.Y >= screenHeight { - return oapi.ClickMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: fmt.Sprintf("coordinates exceed screen bounds (max: %dx%d)", screenWidth-1, screenHeight-1)}}, nil + return oapi.ClickMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: fmt.Sprintf("coordinates exceed screen bounds (max: %dx%d)", screenWidth-1, screenHeight-1)}, + }, nil } // Map button enum to xdotool button code. Default to left button. btn := "1" if body.Button != nil { buttonMap := map[oapi.ClickMouseRequestButton]string{ - oapi.Left: "1", - oapi.Middle: "2", - oapi.Right: "3", - oapi.Back: "8", - oapi.Forward: "9", + oapi.ClickMouseRequestButtonLeft: "1", + oapi.ClickMouseRequestButtonMiddle: "2", + oapi.ClickMouseRequestButtonRight: "3", + oapi.ClickMouseRequestButtonBack: "8", + oapi.ClickMouseRequestButtonForward: "9", } var ok bool btn, ok = buttonMap[*body.Button] if !ok { - return oapi.ClickMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: fmt.Sprintf("unsupported button: %s", *body.Button)}}, nil + return oapi.ClickMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: fmt.Sprintf("unsupported button: %s", *body.Button)}, + }, nil } } @@ -153,7 +182,9 @@ func (s *ApiService) ClickMouse(ctx context.Context, request oapi.ClickMouseRequ } args = append(args, btn) default: - return oapi.ClickMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: fmt.Sprintf("unsupported click type: %s", clickType)}}, nil + return oapi.ClickMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: fmt.Sprintf("unsupported click type: %s", clickType)}, + }, nil } // Release modifier keys (keyup) @@ -168,7 +199,9 @@ func (s *ApiService) ClickMouse(ctx context.Context, request oapi.ClickMouseRequ output, err := defaultXdoTool.Run(ctx, args...) if err != nil { log.Error("xdotool command failed", "err", err, "output", string(output)) - return oapi.ClickMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to execute mouse action"}}, nil + return oapi.ClickMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to execute mouse action"}, + }, nil } return oapi.ClickMouse200Response{}, nil @@ -180,6 +213,10 @@ func (s *ApiService) TakeScreenshot(ctx context.Context, request oapi.TakeScreen s.stz.Disable(ctx) defer s.stz.Enable(ctx) + // serialize input operations to avoid race with other input/screen actions + s.inputMu.Lock() + defer s.inputMu.Unlock() + var body oapi.ScreenshotRequest if request.Body != nil { body = *request.Body @@ -189,7 +226,9 @@ func (s *ApiService) TakeScreenshot(ctx context.Context, request oapi.TakeScreen screenWidth, screenHeight, _, err := s.getCurrentResolution(ctx) if err != nil { log.Error("failed to get current resolution", "error", err) - return oapi.TakeScreenshot500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to get current display resolution"}}, nil + return oapi.TakeScreenshot500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to get current display resolution"}, + }, nil } // Determine display to use (align with other functions) @@ -199,10 +238,14 @@ func (s *ApiService) TakeScreenshot(ctx context.Context, request oapi.TakeScreen if body.Region != nil { r := body.Region if r.X < 0 || r.Y < 0 || r.Width <= 0 || r.Height <= 0 { - return oapi.TakeScreenshot400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "invalid region dimensions"}}, nil + return oapi.TakeScreenshot400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "invalid region dimensions"}, + }, nil } if r.X+r.Width > screenWidth || r.Y+r.Height > screenHeight { - return oapi.TakeScreenshot400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "region exceeds screen bounds"}}, nil + return oapi.TakeScreenshot400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "region exceeds screen bounds"}, + }, nil } } @@ -232,18 +275,24 @@ func (s *ApiService) TakeScreenshot(ctx context.Context, request oapi.TakeScreen stdout, err := cmd.StdoutPipe() if err != nil { log.Error("failed to create stdout pipe", "err", err) - return oapi.TakeScreenshot500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "internal error"}}, nil + return oapi.TakeScreenshot500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "internal error"}, + }, nil } stderr, err := cmd.StderrPipe() if err != nil { log.Error("failed to create stderr pipe", "err", err) - return oapi.TakeScreenshot500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "internal error"}}, nil + return oapi.TakeScreenshot500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "internal error"}, + }, nil } if err := cmd.Start(); err != nil { log.Error("failed to start ffmpeg", "err", err) - return oapi.TakeScreenshot500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start ffmpeg"}}, nil + return oapi.TakeScreenshot500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to start ffmpeg"}, + }, nil } // Start a goroutine to drain stderr for logging to avoid blocking @@ -284,15 +333,23 @@ func (s *ApiService) TypeText(ctx context.Context, request oapi.TypeTextRequestO s.stz.Disable(ctx) defer s.stz.Enable(ctx) + // serialize input operations to avoid overlapping xdotool commands + s.inputMu.Lock() + defer s.inputMu.Unlock() + // Validate request body if request.Body == nil { - return oapi.TypeText400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body is required"}}, nil + return oapi.TypeText400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "request body is required"}, + }, nil } body := *request.Body // Validate delay if provided if body.Delay != nil && *body.Delay < 0 { - return oapi.TypeText400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "delay must be >= 0 milliseconds"}}, nil + return oapi.TypeText400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "delay must be >= 0 milliseconds"}, + }, nil } // Build xdotool arguments @@ -308,8 +365,347 @@ func (s *ApiService) TypeText(ctx context.Context, request oapi.TypeTextRequestO output, err := defaultXdoTool.Run(ctx, args...) if err != nil { log.Error("xdotool command failed", "err", err, "output", string(output)) - return oapi.TypeText500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to type text"}}, nil + return oapi.TypeText500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to type text"}, + }, nil } return oapi.TypeText200Response{}, nil } + +func (s *ApiService) PressKey(ctx context.Context, request oapi.PressKeyRequestObject) (oapi.PressKeyResponseObject, error) { + log := logger.FromContext(ctx) + + s.stz.Disable(ctx) + defer s.stz.Enable(ctx) + + // serialize input operations to avoid overlapping xdotool commands + s.inputMu.Lock() + defer s.inputMu.Unlock() + + if request.Body == nil { + return oapi.PressKey400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "request body is required"}, + }, nil + } + body := *request.Body + + if len(body.Keys) == 0 { + return oapi.PressKey400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "keys must contain at least one key symbol"}, + }, nil + } + if body.Duration != nil && *body.Duration < 0 { + return oapi.PressKey400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "duration must be >= 0 milliseconds"}, + }, nil + } + + // If duration is provided and >0, hold all keys down, sleep, then release. + if body.Duration != nil && *body.Duration > 0 { + argsDown := []string{} + if body.HoldKeys != nil { + for _, key := range *body.HoldKeys { + argsDown = append(argsDown, "keydown", key) + } + } + for _, key := range body.Keys { + argsDown = append(argsDown, "keydown", key) + } + + log.Info("executing xdotool (keydown phase)", "args", argsDown) + if output, err := defaultXdoTool.Run(ctx, argsDown...); err != nil { + log.Error("xdotool keydown failed", "err", err, "output", string(output)) + // Best-effort release any keys that may be down (primary and modifiers) + argsUp := []string{} + for _, key := range body.Keys { + argsUp = append(argsUp, "keyup", key) + } + if body.HoldKeys != nil { + for _, key := range *body.HoldKeys { + argsUp = append(argsUp, "keyup", key) + } + } + _, _ = defaultXdoTool.Run(ctx, argsUp...) + return oapi.PressKey500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: fmt.Sprintf("failed to press keys (keydown). out=%s", string(output))}, + }, nil + } + + d := time.Duration(*body.Duration) * time.Millisecond + time.Sleep(d) + + argsUp := []string{} + for _, key := range body.Keys { + argsUp = append(argsUp, "keyup", key) + } + if body.HoldKeys != nil { + for _, key := range *body.HoldKeys { + argsUp = append(argsUp, "keyup", key) + } + } + + log.Info("executing xdotool (keyup phase)", "args", argsUp) + if output, err := defaultXdoTool.Run(ctx, argsUp...); err != nil { + log.Error("xdotool keyup failed", "err", err, "output", string(output)) + return oapi.PressKey500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: fmt.Sprintf("failed to release keys. out=%s", string(output))}, + }, nil + } + + return oapi.PressKey200Response{}, nil + } + + // Tap behavior: hold modifiers (if any), tap each key, then release modifiers. + args := []string{} + if body.HoldKeys != nil { + for _, key := range *body.HoldKeys { + args = append(args, "keydown", key) + } + } + for _, key := range body.Keys { + args = append(args, "key", key) + } + if body.HoldKeys != nil { + for _, key := range *body.HoldKeys { + args = append(args, "keyup", key) + } + } + + log.Info("executing xdotool", "args", args) + output, err := defaultXdoTool.Run(ctx, args...) + if err != nil { + log.Error("xdotool command failed", "err", err, "output", string(output)) + return oapi.PressKey500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: fmt.Sprintf("failed to press keys. out=%s", string(output))}, + }, nil + } + return oapi.PressKey200Response{}, nil +} + +func (s *ApiService) Scroll(ctx context.Context, request oapi.ScrollRequestObject) (oapi.ScrollResponseObject, error) { + log := logger.FromContext(ctx) + + s.stz.Disable(ctx) + defer s.stz.Enable(ctx) + + // serialize input operations to avoid overlapping xdotool commands + s.inputMu.Lock() + defer s.inputMu.Unlock() + + if request.Body == nil { + return oapi.Scroll400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "request body is required"}, + }, nil + } + body := *request.Body + + // Validate deltas + if (body.DeltaX == nil || *body.DeltaX == 0) && (body.DeltaY == nil || *body.DeltaY == 0) { + return oapi.Scroll400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "at least one of delta_x or delta_y must be non-zero"}, + }, nil + } + + // Bounds check + screenWidth, screenHeight, _, err := s.getCurrentResolution(ctx) + if err != nil { + log.Error("failed to get current resolution", "error", err) + return oapi.Scroll500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to get current display resolution"}, + }, nil + } + if body.X < 0 || body.Y < 0 { + return oapi.Scroll400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "coordinates must be non-negative"}, + }, nil + } + if body.X >= screenWidth || body.Y >= screenHeight { + return oapi.Scroll400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: fmt.Sprintf("coordinates exceed screen bounds (max: %dx%d)", screenWidth-1, screenHeight-1)}, + }, nil + } + + args := []string{} + if body.HoldKeys != nil { + for _, key := range *body.HoldKeys { + args = append(args, "keydown", key) + } + } + args = append(args, "mousemove", "--sync", strconv.Itoa(body.X), strconv.Itoa(body.Y)) + + // Apply vertical ticks first (sequential as specified) + if body.DeltaY != nil && *body.DeltaY != 0 { + count := *body.DeltaY + btn := "5" // down + if count < 0 { + btn = "4" // up + count = -count + } + args = append(args, "click", "--repeat", strconv.Itoa(count), "--delay", "0", btn) + } + // Then horizontal ticks + if body.DeltaX != nil && *body.DeltaX != 0 { + count := *body.DeltaX + btn := "7" // right + if count < 0 { + btn = "6" // left + count = -count + } + args = append(args, "click", "--repeat", strconv.Itoa(count), "--delay", "0", btn) + } + + if body.HoldKeys != nil { + for _, key := range *body.HoldKeys { + args = append(args, "keyup", key) + } + } + + log.Info("executing xdotool", "args", args) + output, err := defaultXdoTool.Run(ctx, args...) + if err != nil { + log.Error("xdotool scroll failed", "err", err, "output", string(output)) + return oapi.Scroll500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: fmt.Sprintf("failed to perform scroll: %s", string(output))}, + }, nil + } + return oapi.Scroll200Response{}, nil +} + +func (s *ApiService) DragMouse(ctx context.Context, request oapi.DragMouseRequestObject) (oapi.DragMouseResponseObject, error) { + log := logger.FromContext(ctx) + + s.stz.Disable(ctx) + defer s.stz.Enable(ctx) + + // serialize input operations to avoid overlapping xdotool commands + s.inputMu.Lock() + defer s.inputMu.Unlock() + + if request.Body == nil { + return oapi.DragMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "request body is required"}}, nil + } + body := *request.Body + + if len(body.Path) < 2 { + return oapi.DragMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "path must contain at least two points"}}, nil + } + + // Bounds check for all points + screenWidth, screenHeight, _, err := s.getCurrentResolution(ctx) + if err != nil { + log.Error("failed to get current resolution", "error", err) + return oapi.DragMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to get current display resolution"}, + }, nil + } + for i, pt := range body.Path { + if len(pt) != 2 { + return oapi.DragMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: fmt.Sprintf("path[%d] must be [x,y]", i)}, + }, nil + } + x := pt[0] + y := pt[1] + if x < 0 || y < 0 { + return oapi.DragMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "coordinates must be non-negative"}, + }, nil + } + if x >= screenWidth || y >= screenHeight { + return oapi.DragMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: fmt.Sprintf("coordinates exceed screen bounds (max: %dx%d)", screenWidth-1, screenHeight-1)}, + }, nil + } + } + + // Button mapping; default to left if unspecified + btn := "1" + if body.Button != nil { + switch *body.Button { + case oapi.DragMouseRequestButtonLeft: + btn = "1" + case oapi.DragMouseRequestButtonMiddle: + btn = "2" + case oapi.DragMouseRequestButtonRight: + btn = "3" + default: + return oapi.DragMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: fmt.Sprintf("unsupported button: %s", *body.Button)}, + }, nil + } + } + + // Phase 1: keydown modifiers, move to start, mousedown + args1 := []string{} + if body.HoldKeys != nil { + for _, key := range *body.HoldKeys { + args1 = append(args1, "keydown", key) + } + } + start := body.Path[0] + args1 = append(args1, "mousemove", "--sync", strconv.Itoa(start[0]), strconv.Itoa(start[1])) + args1 = append(args1, "mousedown", btn) + log.Info("executing xdotool (drag start)", "args", args1) + if output, err := defaultXdoTool.Run(ctx, args1...); err != nil { + log.Error("xdotool drag start failed", "err", err, "output", string(output)) + // Best-effort release modifiers + if body.HoldKeys != nil { + argsCleanup := []string{} + for _, key := range *body.HoldKeys { + argsCleanup = append(argsCleanup, "keyup", key) + } + _, _ = defaultXdoTool.Run(ctx, argsCleanup...) + } + return oapi.DragMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: fmt.Sprintf("failed to start drag: %s", string(output))}, + }, nil + } + + // Optional delay between mousedown and movement + if body.Delay != nil && *body.Delay > 0 { + time.Sleep(time.Duration(*body.Delay) * time.Millisecond) + } + + // Phase 2: move along path (excluding first point) + args2 := []string{} + for _, pt := range body.Path[1:] { + args2 = append(args2, "mousemove", "--sync", strconv.Itoa(pt[0]), strconv.Itoa(pt[1])) + } + if len(args2) > 0 { + log.Info("executing xdotool (drag move)", "args", args2) + if output, err := defaultXdoTool.Run(ctx, args2...); err != nil { + log.Error("xdotool drag move failed", "err", err, "output", string(output)) + // Try to release button and modifiers + argsCleanup := []string{"mouseup", btn} + if body.HoldKeys != nil { + for _, key := range *body.HoldKeys { + argsCleanup = append(argsCleanup, "keyup", key) + } + } + _, _ = defaultXdoTool.Run(ctx, argsCleanup...) + return oapi.DragMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: fmt.Sprintf("failed during drag movement: %s", string(output))}, + }, nil + } + } + + // Phase 3: mouseup and release modifiers + args3 := []string{"mouseup", btn} + if body.HoldKeys != nil { + for _, key := range *body.HoldKeys { + args3 = append(args3, "keyup", key) + } + } + log.Info("executing xdotool (drag end)", "args", args3) + if output, err := defaultXdoTool.Run(ctx, args3...); err != nil { + log.Error("xdotool drag end failed", "err", err, "output", string(output)) + return oapi.DragMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: fmt.Sprintf("failed to finish drag: %s", string(output))}, + }, nil + } + + return oapi.DragMouse200Response{}, nil +} diff --git a/server/e2e/e2e_chromium_test.go b/server/e2e/e2e_chromium_test.go index 4731847b..4c9029ec 100644 --- a/server/e2e/e2e_chromium_test.go +++ b/server/e2e/e2e_chromium_test.go @@ -2,14 +2,9 @@ package e2e import ( "archive/zip" - "bufio" "bytes" "context" - "crypto/rand" - "database/sql" "encoding/base64" - "encoding/hex" - "encoding/json" "fmt" "io" "mime/multipart" @@ -24,11 +19,12 @@ import ( "time" "log/slog" - "text/template" _ "github.com/glebarez/sqlite" logctx "github.com/onkernel/kernel-images/server/lib/logger" instanceoapi "github.com/onkernel/kernel-images/server/lib/oapi" + "github.com/samber/lo" + "github.com/stretchr/testify/require" ) const ( @@ -81,25 +77,11 @@ func ensurePlaywrightDeps(t *testing.T) { cmd := exec.Command("pnpm", "install") cmd.Dir = getPlaywrightPath() output, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("Failed to install playwright dependencies: %v\nOutput: %s", err, string(output)) - } + require.NoError(t, err, "Failed to install playwright dependencies: %v\nOutput: %s", err, string(output)) t.Log("Playwright dependencies installed successfully") } } -func TestChromiumHeadfulUserDataSaving(t *testing.T) { - t.Skip("flaky. TODO(raf): fix") - ensurePlaywrightDeps(t) - runChromiumUserDataSavingFlow(t, headfulImage, containerName) -} - -func TestChromiumHeadlessPersistence(t *testing.T) { - t.Skip("flaky. TODO(raf): fix") - ensurePlaywrightDeps(t) - runChromiumUserDataSavingFlow(t, headlessImage, containerName) -} - func TestDisplayResolutionChange(t *testing.T) { image := headlessImage name := containerName + "-display" @@ -108,7 +90,7 @@ func TestDisplayResolutionChange(t *testing.T) { baseCtx := logctx.AddToContext(context.Background(), logger) if _, err := exec.LookPath("docker"); err != nil { - t.Fatalf("docker not available: %v", err) + require.NoError(t, err, "docker not available: %v", err) } // Clean slate @@ -122,35 +104,25 @@ func TestDisplayResolutionChange(t *testing.T) { // Start container _, exitCh, err := runContainer(baseCtx, image, name, env) - if err != nil { - t.Fatalf("failed to start container: %v", err) - } + require.NoError(t, err, "failed to start container: %v", err) defer stopContainer(baseCtx, name) ctx, cancel := context.WithTimeout(baseCtx, 3*time.Minute) defer cancel() logger.Info("[setup]", "action", "waiting for API", "url", apiBaseURL+"/spec.yaml") - if err := waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh); err != nil { - _ = dumpContainerDiagnostics(ctx, name) - t.Fatalf("api not ready: %v", err) - } + require.NoError(t, waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh), "api not ready: %v", err) client, err := apiClient() - if err != nil { - t.Fatalf("failed to create API client: %v", err) - } + require.NoError(t, err, "failed to create API client: %v", err) // Get initial Xvfb resolution logger.Info("[test]", "action", "getting initial Xvfb resolution") initialWidth, initialHeight, err := getXvfbResolution(ctx) - if err != nil { - t.Fatalf("failed to get initial Xvfb resolution: %v", err) - } + require.NoError(t, err, "failed to get initial Xvfb resolution: %v", err) logger.Info("[test]", "initial_resolution", fmt.Sprintf("%dx%d", initialWidth, initialHeight)) - if initialWidth != 1024 || initialHeight != 768 { - t.Errorf("expected initial resolution 1024x768, got %dx%d", initialWidth, initialHeight) - } + require.Equal(t, 1024, initialWidth, "expected initial width 1024") + require.Equal(t, 768, initialHeight, "expected initial height 768") // Test first resolution change: 1920x1080 logger.Info("[test]", "action", "changing resolution to 1920x1080") @@ -161,21 +133,13 @@ func TestDisplayResolutionChange(t *testing.T) { Height: &height1, } rsp1, err := client.PatchDisplayWithResponse(ctx, req1) - if err != nil { - t.Fatalf("PATCH /display request failed: %v", err) - } - if rsp1.StatusCode() != http.StatusOK { - t.Fatalf("unexpected status: %s body=%s", rsp1.Status(), string(rsp1.Body)) - } - if rsp1.JSON200 == nil { - t.Fatalf("expected JSON200 response, got nil") - } - if rsp1.JSON200.Width == nil || *rsp1.JSON200.Width != width1 { - t.Errorf("expected width %d in response, got %v", width1, rsp1.JSON200.Width) - } - if rsp1.JSON200.Height == nil || *rsp1.JSON200.Height != height1 { - t.Errorf("expected height %d in response, got %v", height1, rsp1.JSON200.Height) - } + require.NoError(t, err, "PATCH /display request failed: %v", err) + require.Equal(t, http.StatusOK, rsp1.StatusCode(), "unexpected status: %s body=%s", rsp1.Status(), string(rsp1.Body)) + require.NotNil(t, rsp1.JSON200, "expected JSON200 response, got nil") + require.NotNil(t, rsp1.JSON200.Width, "expected width in response") + require.Equal(t, width1, *rsp1.JSON200.Width, "expected width %d in response", width1) + require.NotNil(t, rsp1.JSON200.Height, "expected height in response") + require.Equal(t, height1, *rsp1.JSON200.Height, "expected height %d in response", height1) // Wait a bit for Xvfb to fully restart logger.Info("[test]", "action", "waiting for Xvfb to stabilize") @@ -184,13 +148,10 @@ func TestDisplayResolutionChange(t *testing.T) { // Verify new resolution via ps aux logger.Info("[test]", "action", "verifying new Xvfb resolution") newWidth1, newHeight1, err := getXvfbResolution(ctx) - if err != nil { - t.Fatalf("failed to get new Xvfb resolution: %v", err) - } + require.NoError(t, err, "failed to get new Xvfb resolution: %v", err) logger.Info("[test]", "new_resolution", fmt.Sprintf("%dx%d", newWidth1, newHeight1)) - if newWidth1 != width1 || newHeight1 != height1 { - t.Errorf("expected Xvfb resolution %dx%d, got %dx%d", width1, height1, newWidth1, newHeight1) - } + require.Equal(t, width1, newWidth1, "expected Xvfb resolution %dx%d, got %dx%d", width1, height1, newWidth1, newHeight1) + require.Equal(t, height1, newHeight1, "expected Xvfb resolution %dx%d, got %dx%d", width1, height1, newWidth1, newHeight1) // Test second resolution change: 1280x720 logger.Info("[test]", "action", "changing resolution to 1280x720") @@ -201,21 +162,13 @@ func TestDisplayResolutionChange(t *testing.T) { Height: &height2, } rsp2, err := client.PatchDisplayWithResponse(ctx, req2) - if err != nil { - t.Fatalf("PATCH /display request failed: %v", err) - } - if rsp2.StatusCode() != http.StatusOK { - t.Fatalf("unexpected status: %s body=%s", rsp2.Status(), string(rsp2.Body)) - } - if rsp2.JSON200 == nil { - t.Fatalf("expected JSON200 response, got nil") - } - if rsp2.JSON200.Width == nil || *rsp2.JSON200.Width != width2 { - t.Errorf("expected width %d in response, got %v", width2, rsp2.JSON200.Width) - } - if rsp2.JSON200.Height == nil || *rsp2.JSON200.Height != height2 { - t.Errorf("expected height %d in response, got %v", height2, rsp2.JSON200.Height) - } + require.NoError(t, err, "PATCH /display request failed: %v", err) + require.Equal(t, http.StatusOK, rsp2.StatusCode(), "unexpected status: %s body=%s", rsp2.Status(), string(rsp2.Body)) + require.NotNil(t, rsp2.JSON200, "expected JSON200 response, got nil") + require.NotNil(t, rsp2.JSON200.Width, "expected width in response") + require.Equal(t, width2, *rsp2.JSON200.Width, "expected width %d in response", width2) + require.NotNil(t, rsp2.JSON200.Height, "expected height in response") + require.Equal(t, height2, *rsp2.JSON200.Height, "expected height %d in response", height2) // Wait a bit for Xvfb to fully restart logger.Info("[test]", "action", "waiting for Xvfb to stabilize") @@ -224,13 +177,10 @@ func TestDisplayResolutionChange(t *testing.T) { // Verify second resolution change via ps aux logger.Info("[test]", "action", "verifying second Xvfb resolution") newWidth2, newHeight2, err := getXvfbResolution(ctx) - if err != nil { - t.Fatalf("failed to get second Xvfb resolution: %v", err) - } + require.NoError(t, err, "failed to get second Xvfb resolution: %v", err) logger.Info("[test]", "final_resolution", fmt.Sprintf("%dx%d", newWidth2, newHeight2)) - if newWidth2 != width2 || newHeight2 != height2 { - t.Errorf("expected Xvfb resolution %dx%d, got %dx%d", width2, height2, newWidth2, newHeight2) - } + require.Equal(t, width2, newWidth2, "expected Xvfb resolution %dx%d, got %dx%d", width2, height2, newWidth2, newHeight2) + require.Equal(t, height2, newHeight2, "expected Xvfb resolution %dx%d, got %dx%d", width2, height2, newWidth2, newHeight2) logger.Info("[test]", "result", "all resolution changes verified successfully") } @@ -244,7 +194,7 @@ func TestExtensionUploadAndActivation(t *testing.T) { baseCtx := logctx.AddToContext(context.Background(), logger) if _, err := exec.LookPath("docker"); err != nil { - t.Fatalf("docker not available: %v", err) + require.NoError(t, err, "docker not available: %v", err) } // Clean slate @@ -255,23 +205,17 @@ func TestExtensionUploadAndActivation(t *testing.T) { // Start container _, exitCh, err := runContainer(baseCtx, image, name, env) - if err != nil { - t.Fatalf("failed to start container: %v", err) - } + require.NoError(t, err, "failed to start container: %v", err) defer stopContainer(baseCtx, name) ctx, cancel := context.WithTimeout(baseCtx, 3*time.Minute) defer cancel() - if err := waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh); err != nil { - _ = dumpContainerDiagnostics(ctx, name) - t.Fatalf("api not ready: %v", err) - } + require.NoError(t, waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh), "api not ready: %v", err) // Wait for DevTools - if _, err := waitDevtoolsWS(ctx); err != nil { - t.Fatalf("devtools not ready: %v", err) - } + _, err = waitDevtoolsWS(ctx) + require.NoError(t, err, "devtools not ready: %v", err) // Build simple MV3 extension zip in-memory extDir := t.TempDir() @@ -292,48 +236,33 @@ func TestExtensionUploadAndActivation(t *testing.T) { ] }` contentScript := "document.title += \" -- Title updated by browser extension\";\n" - if err := os.WriteFile(filepath.Join(extDir, "manifest.json"), []byte(manifest), 0600); err != nil { - t.Fatalf("write manifest: %v", err) - } - if err := os.WriteFile(filepath.Join(extDir, "content-script.js"), []byte(contentScript), 0600); err != nil { - t.Fatalf("write content-script: %v", err) - } + err = os.WriteFile(filepath.Join(extDir, "manifest.json"), []byte(manifest), 0600) + require.NoError(t, err, "write manifest: %v", err) + err = os.WriteFile(filepath.Join(extDir, "content-script.js"), []byte(contentScript), 0600) + require.NoError(t, err, "write content-script: %v", err) extZip, err := zipDirToBytes(extDir) - if err != nil { - t.Fatalf("zip ext: %v", err) - } + require.NoError(t, err, "zip ext: %v", err) // Use new API to upload extension and restart Chromium { client, err := apiClient() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var body bytes.Buffer w := multipart.NewWriter(&body) fw, err := w.CreateFormFile("extensions.zip_file", "ext.zip") - if err != nil { - t.Fatal(err) - } - if _, err := io.Copy(fw, bytes.NewReader(extZip)); err != nil { - t.Fatal(err) - } - if err := w.WriteField("extensions.name", "testext"); err != nil { - t.Fatal(err) - } - if err := w.Close(); err != nil { - t.Fatal(err) - } + require.NoError(t, err) + _, err = io.Copy(fw, bytes.NewReader(extZip)) + require.NoError(t, err) + err = w.WriteField("extensions.name", "testext") + require.NoError(t, err) + err = w.Close() + require.NoError(t, err) start := time.Now() rsp, err := client.UploadExtensionsAndRestartWithBodyWithResponse(ctx, w.FormDataContentType(), &body) elapsed := time.Since(start) - if err != nil { - t.Fatalf("uploadExtensionsAndRestart request error: %v", err) - } - if rsp.StatusCode() != http.StatusCreated { - t.Fatalf("unexpected status: %s body=%s", rsp.Status(), string(rsp.Body)) - } + require.NoError(t, err, "uploadExtensionsAndRestart request error: %v", err) + require.Equal(t, http.StatusCreated, rsp.StatusCode(), "unexpected status: %s body=%s", rsp.Status(), string(rsp.Body)) t.Logf("/chromium/upload-extensions-and-restart completed in %s (%d ms)", elapsed.String(), elapsed.Milliseconds()) } @@ -348,9 +277,7 @@ func TestExtensionUploadAndActivation(t *testing.T) { ) cmd.Dir = getPlaywrightPath() out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("title verify failed: %v output=%s", err, string(out)) - } + require.NoError(t, err, "title verify failed: %v output=%s", err, string(out)) } } @@ -362,7 +289,7 @@ func TestScreenshotHeadless(t *testing.T) { baseCtx := logctx.AddToContext(context.Background(), logger) if _, err := exec.LookPath("docker"); err != nil { - t.Fatalf("docker not available: %v", err) + require.NoError(t, err, "docker not available: %v", err) } // Clean slate @@ -372,36 +299,23 @@ func TestScreenshotHeadless(t *testing.T) { // Start container _, exitCh, err := runContainer(baseCtx, image, name, env) - if err != nil { - t.Fatalf("failed to start container: %v", err) - } + require.NoError(t, err, "failed to start container: %v", err) defer stopContainer(baseCtx, name) ctx, cancel := context.WithTimeout(baseCtx, 2*time.Minute) defer cancel() - if err := waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh); err != nil { - _ = dumpContainerDiagnostics(ctx, name) - t.Fatalf("api not ready: %v", err) - } + require.NoError(t, waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh), "api not ready: %v", err) client, err := apiClient() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // Whole-screen screenshot { rsp, err := client.TakeScreenshotWithResponse(ctx, instanceoapi.TakeScreenshotJSONRequestBody{}) - if err != nil { - t.Fatalf("screenshot request error: %v", err) - } - if rsp.StatusCode() != http.StatusOK { - t.Fatalf("unexpected status for full screenshot: %s body=%s", rsp.Status(), string(rsp.Body)) - } - if !isPNG(rsp.Body) { - t.Fatalf("response is not PNG (len=%d)", len(rsp.Body)) - } + require.NoError(t, err, "screenshot request error: %v", err) + require.Equal(t, http.StatusOK, rsp.StatusCode(), "unexpected status for full screenshot: %s body=%s", rsp.Status(), string(rsp.Body)) + require.True(t, isPNG(rsp.Body), "response is not PNG (len=%d)", len(rsp.Body)) } // Region screenshot (safe small region) @@ -409,15 +323,9 @@ func TestScreenshotHeadless(t *testing.T) { region := instanceoapi.ScreenshotRegion{X: 0, Y: 0, Width: 50, Height: 50} req := instanceoapi.TakeScreenshotJSONRequestBody{Region: ®ion} rsp, err := client.TakeScreenshotWithResponse(ctx, req) - if err != nil { - t.Fatalf("region screenshot request error: %v", err) - } - if rsp.StatusCode() != http.StatusOK { - t.Fatalf("unexpected status for region screenshot: %s body=%s", rsp.Status(), string(rsp.Body)) - } - if !isPNG(rsp.Body) { - t.Fatalf("region response is not PNG (len=%d)", len(rsp.Body)) - } + require.NoError(t, err, "region screenshot request error: %v", err) + require.Equal(t, http.StatusOK, rsp.StatusCode(), "unexpected status for region screenshot: %s body=%s", rsp.Status(), string(rsp.Body)) + require.True(t, isPNG(rsp.Body), "region response is not PNG (len=%d)", len(rsp.Body)) } } @@ -429,7 +337,7 @@ func TestScreenshotHeadful(t *testing.T) { baseCtx := logctx.AddToContext(context.Background(), logger) if _, err := exec.LookPath("docker"); err != nil { - t.Fatalf("docker not available: %v", err) + require.NoError(t, err, "docker not available: %v", err) } // Clean slate @@ -442,36 +350,23 @@ func TestScreenshotHeadful(t *testing.T) { // Start container _, exitCh, err := runContainer(baseCtx, image, name, env) - if err != nil { - t.Fatalf("failed to start container: %v", err) - } + require.NoError(t, err, "failed to start container: %v", err) defer stopContainer(baseCtx, name) ctx, cancel := context.WithTimeout(baseCtx, 2*time.Minute) defer cancel() - if err := waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh); err != nil { - _ = dumpContainerDiagnostics(ctx, name) - t.Fatalf("api not ready: %v", err) - } + require.NoError(t, waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh), "api not ready: %v", err) client, err := apiClient() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // Whole-screen screenshot { rsp, err := client.TakeScreenshotWithResponse(ctx, instanceoapi.TakeScreenshotJSONRequestBody{}) - if err != nil { - t.Fatalf("screenshot request error: %v", err) - } - if rsp.StatusCode() != http.StatusOK { - t.Fatalf("unexpected status for full screenshot: %s body=%s", rsp.Status(), string(rsp.Body)) - } - if !isPNG(rsp.Body) { - t.Fatalf("response is not PNG (len=%d)", len(rsp.Body)) - } + require.NoError(t, err, "screenshot request error: %v", err) + require.Equal(t, http.StatusOK, rsp.StatusCode(), "unexpected status for full screenshot: %s body=%s", rsp.Status(), string(rsp.Body)) + require.True(t, isPNG(rsp.Body), "response is not PNG (len=%d)", len(rsp.Body)) } // Region screenshot @@ -479,220 +374,74 @@ func TestScreenshotHeadful(t *testing.T) { region := instanceoapi.ScreenshotRegion{X: 0, Y: 0, Width: 80, Height: 60} req := instanceoapi.TakeScreenshotJSONRequestBody{Region: ®ion} rsp, err := client.TakeScreenshotWithResponse(ctx, req) - if err != nil { - t.Fatalf("region screenshot request error: %v", err) - } - if rsp.StatusCode() != http.StatusOK { - t.Fatalf("unexpected status for region screenshot: %s body=%s", rsp.Status(), string(rsp.Body)) - } - if !isPNG(rsp.Body) { - t.Fatalf("region response is not PNG (len=%d)", len(rsp.Body)) - } + require.NoError(t, err, "region screenshot request error: %v", err) + require.Equal(t, http.StatusOK, rsp.StatusCode(), "unexpected status for region screenshot: %s body=%s", rsp.Status(), string(rsp.Body)) + require.True(t, isPNG(rsp.Body), "region response is not PNG (len=%d)", len(rsp.Body)) } } -// isPNG returns true if data starts with the PNG magic header -func isPNG(data []byte) bool { - if len(data) < 8 { - return false - } - sig := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} - for i := 0; i < 8; i++ { - if data[i] != sig[i] { - return false - } - } - return true -} +func TestInputEndpointsSmoke(t *testing.T) { + image := headlessImage + name := containerName + "-input-smoke" -func runChromiumUserDataSavingFlow(t *testing.T, image, containerName string) { - t.Helper() - logger := slog.New(slog.NewTextHandler(t.Output(), &slog.HandlerOptions{ - Level: slog.LevelInfo, - AddSource: false, - ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { - if a.Key == slog.TimeKey { - ts := a.Value.Time() - return slog.String(slog.TimeKey, ts.UTC().Format(time.RFC3339)) - } - return a - }, - })) + logger := slog.New(slog.NewTextHandler(t.Output(), &slog.HandlerOptions{Level: slog.LevelInfo})) baseCtx := logctx.AddToContext(context.Background(), logger) - logger.Info("[e2e]", "action", "starting chromium cookie saving flow", "image", image, "name", containerName) - if _, err := exec.LookPath("docker"); err != nil { - t.Fatalf("[precheck] docker not available: %v", err) - } - // Setup Phase - layout := createTestTempLayout(t) - logger.Info("[setup]", "base", layout.BaseDir, "zips", layout.ZipsDir, "restored", layout.RestoreDir) - logger.Info("[setup]", "action", "ensuring container is not running", "container", containerName) - if err := stopContainer(baseCtx, containerName); err != nil { - t.Fatalf("[setup] failed to stop container %s: %v", containerName, err) - } - env := map[string]string{ - "WIDTH": "1024", - "HEIGHT": "768", - "ENABLE_WEBRTC": os.Getenv("ENABLE_WEBRTC"), - "NEKO_ICESERVERS": os.Getenv("NEKO_ICESERVERS"), - } - if strings.Contains(image, "headful") { - // headless image sets its own flags, so only do this for headful - env["CHROMIUM_FLAGS"] = "--no-sandbox --disable-dev-shm-usage --disable-gpu --start-maximized --disable-software-rasterizer --remote-allow-origins=* --no-zygote --password-store=basic --no-first-run" - } - logger.Info("[setup]", "action", "starting container", "image", image, "name", containerName) - _, exitCh, err := runContainer(baseCtx, image, containerName, env) - if err != nil { - t.Fatalf("[setup] failed to start container %s: %v", image, err) - } - defer stopContainer(baseCtx, containerName) - - ctx, cancel := context.WithTimeout(baseCtx, 3*time.Minute) - defer cancel() - logger.Info("[setup]", "action", "waiting for API", "url", apiBaseURL+"/spec.yaml") - if err := waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh); err != nil { - _ = dumpContainerDiagnostics(ctx, containerName) - t.Fatalf("[setup] api not ready: %v", err) - } - logger.Info("[setup]", "action", "waiting for DevTools WebSocket") - wsURL, err := waitDevtoolsWS(ctx) - if err != nil { - t.Fatalf("[setup] devtools not ready: %v", err) - } - - // Diagnostic Phase - Check file ownership and permissions before any navigations - logger.Info("[diagnostic]", "action", "checking file ownership and permissions") - if err := runCookieDebugScript(ctx, t); err != nil { - logger.Warn("[diagnostic]", "action", "cookie debug script failed", "error", err) - } else { - logger.Info("[diagnostic]", "action", "cookie debug script completed successfully") - } - - // Cookie Setting Phase - cookieName := "e2e_cookie" - randBytes := make([]byte, 16) - rand.Read(randBytes) - cookieValue := hex.EncodeToString(randBytes) - serverURL, stopServer := startCookieTestServer(t, cookieName, cookieValue) - defer stopServer() - - logger.Info("[cookies]", "action", "navigate set-cookie", "cookieName", cookieName, "cookieValue", cookieValue) - if err := navigateAndEnsureCookie(ctx, wsURL, serverURL+"/set-cookie", cookieName, cookieValue, "initial"); err != nil { - t.Fatalf("[cookies] failed to set/verify cookie: %v", err) - } - logger.Info("[cookies]", "action", "navigate get-cookies") - if err := navigateAndEnsureCookie(ctx, wsURL, serverURL+"/get-cookies", cookieName, cookieValue, "initial-get-page"); err != nil { - t.Fatalf("[cookies] failed to verify cookie on get-cookies: %v", err) - } - - // Local Storage Setting Phase - localStorageKey := "e2e_localstorage_key" - randBytes = make([]byte, 16) - rand.Read(randBytes) - localStorageValue := hex.EncodeToString(randBytes) - logger.Info("[localstorage]", "action", "set and verify localStorage") - if err := setAndVerifyLocalStorage(ctx, wsURL, serverURL+"/set-cookie", localStorageKey, localStorageValue, "initial"); err != nil { - t.Fatalf("[localstorage] failed to set/verify localStorage: %v", err) - } - - // x.com Cookie Generation Phase - logger.Info("[x-cookies]", "action", "navigate to x.com and verify guest_id cookie") - if err := navigateToXAndVerifyCookie(ctx, wsURL, "initial"); err != nil { - logger.Warn("[x-cookies]", "message", fmt.Sprintf("failed to navigate to x.com and verify cookie: %v", err)) + if _, err := exec.LookPath("docker"); err != nil { + require.NoError(t, err, "docker not available: %v", err) } - // Restart & Persistence Testing Phase - logger.Info("[restart]", "action", "stop chromium via supervisorctl") - if err := stopChromiumViaSupervisord(ctx); err != nil { - t.Fatalf("[restart] failed to stop chromium via supervisorctl: %v", err) - } + _ = stopContainer(baseCtx, name) - // Check file state after stopping - logger.Info("[restart]", "action", "checking file state after stop") - if err := runCookieDebugScript(ctx, t); err != nil { - logger.Warn("[restart]", "action", "post-stop debug script failed", "error", err) - } else { - logger.Info("[restart]", "action", "post-stop debug script completed") - } + width, height := 1024, 768 + _, exitCh, err := runContainer(baseCtx, image, name, map[string]string{"WIDTH": strconv.Itoa(width), "HEIGHT": strconv.Itoa(height)}) + require.NoError(t, err, "failed to start container: %v", err) + defer stopContainer(baseCtx, name) - logger.Info("[snapshot]", "action", "download user-data zip") - zipBytes, err := downloadUserDataZip(ctx) - if err != nil { - t.Fatalf("[snapshot] download zip: %v", err) - } - if err := validateZip(zipBytes); err != nil { - t.Fatalf("[snapshot] invalid zip: %v", err) - } - zipPath := filepath.Join(layout.ZipsDir, "user-data-original.zip") - if err := os.WriteFile(zipPath, zipBytes, 0600); err != nil { - t.Fatalf("[snapshot] write zip: %v", err) - } - if err := unzipBytesToDir(zipBytes, layout.RestoreDir); err != nil { - t.Fatalf("[snapshot] unzip: %v", err) - } + ctx, cancel := context.WithTimeout(baseCtx, 2*time.Minute) + defer cancel() - if err := verifyCookieInLocalSnapshot(ctx, layout.RestoreDir, cookieName, cookieValue); err != nil { - logger.Warn("[snapshot]", "message", fmt.Sprintf("verify cookie in sqlite: %v", err)) - } - if err := deleteLocalSingletonLockFiles(layout.RestoreDir); err != nil { - t.Fatalf("[snapshot] delete local singleton locks: %v", err) - } - cleanZipBytes, err := zipDirToBytes(layout.RestoreDir) - if err != nil { - t.Fatalf("[snapshot] zip cleaned snapshot: %v", err) - } - cleanZipPath := filepath.Join(layout.ZipsDir, "user-data-cleaned.zip") - if err := os.WriteFile(cleanZipPath, cleanZipBytes, 0600); err != nil { - t.Fatalf("[snapshot] write cleaned zip: %v", err) - } - logger.Info("[snapshot]", "action", "delete remote user-data") - if err := deleteDirectoryViaAPI(ctx, "/home/kernel/user-data"); err != nil { - t.Fatalf("[snapshot] delete remote user-data: %v", err) - } - logger.Info("[snapshot]", "action", "upload cleaned zip", "bytes", len(cleanZipBytes)) - if err := uploadUserDataZip(ctx, cleanZipBytes); err != nil { - t.Fatalf("[snapshot] upload cleaned zip: %v", err) - } + require.NoError(t, waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh), "api not ready: %v", err) - // Check file state after uploading - logger.Info("[restart]", "action", "checking file state after uploading") - if err := runCookieDebugScript(ctx, t); err != nil { - logger.Warn("[restart]", "action", "post-stop debug script failed", "error", err) - } else { - logger.Info("[restart]", "action", "post-stop debug script completed") - } + client, err := apiClient() + require.NoError(t, err) - // Verify that the cookie exists in the container's cookies database after upload - logger.Info("[snapshot]", "action", "verifying cookie in container database", "cookieName", cookieName) - if err := verifyCookieInContainerDB(ctx, cookieName); err != nil { - logger.Warn("[snapshot]", "message", fmt.Sprintf("cookie not found in container database: %v", err)) + // press_key: tap Return + { + rsp, err := client.PressKeyWithResponse(ctx, instanceoapi.PressKeyJSONRequestBody{Keys: []string{"Return"}}) + require.NoError(t, err, "press_key request error: %v", err) + require.Equal(t, http.StatusOK, rsp.StatusCode(), "unexpected status for press_key: %s body=%s", rsp.Status(), string(rsp.Body)) } - if err := startChromiumViaAPI(ctx); err != nil { - t.Fatalf("[restart] start chromium: %v", err) + // scroll: small vertical and horizontal ticks at center + cx, cy := width/2, height/2 + { + rsp, err := client.ScrollWithResponse(ctx, instanceoapi.ScrollJSONRequestBody{X: cx, Y: cy, DeltaX: lo.ToPtr(2), DeltaY: lo.ToPtr(-3)}) + require.NoError(t, err, "scroll request error: %v", err) + require.Equal(t, http.StatusOK, rsp.StatusCode(), "unexpected status for scroll: %s body=%s", rsp.Status(), string(rsp.Body)) } - logger.Info("[restart]", "action", "wait for DevTools") - wsURL, err = waitDevtoolsWS(ctx) - if err != nil { - t.Fatalf("[restart] devtools not ready: %v", err) + // drag_mouse: simple short drag path + { + rsp, err := client.DragMouseWithResponse(ctx, instanceoapi.DragMouseJSONRequestBody{ + Path: [][]int{{cx - 10, cy - 10}, {cx + 10, cy + 10}}, + }) + require.NoError(t, err, "drag_mouse request error: %v", err) + require.Equal(t, http.StatusOK, rsp.StatusCode(), "unexpected status for drag_mouse: %s body=%s", rsp.Status(), string(rsp.Body)) } - logger.Info("[restart]", "action", "sleep to init", "seconds", 5) - time.Sleep(5 * time.Second) +} - if err := navigateAndEnsureCookie(ctx, wsURL, serverURL+"/get-cookies", cookieName, cookieValue, "after-restart"); err != nil { - t.Fatalf("[final] cookie not persisted after restart: %v", err) +// isPNG returns true if data starts with the PNG magic header +func isPNG(data []byte) bool { + if len(data) < 8 { + return false } - logger.Info("[final]", "result", "cookie verified after restart") - - // Verify Local Storage persistence - logger.Info("[final]", "action", "verifying localStorage persistence") - if err := verifyLocalStorage(ctx, wsURL, serverURL+"/set-cookie", localStorageKey, localStorageValue, "after-restart"); err != nil { - t.Fatalf("[final] localStorage not persisted after restart: %v", err) + sig := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} + for i := 0; i < 8; i++ { + if data[i] != sig[i] { + return false + } } - logger.Info("[final]", "result", "localStorage verified after restart") - - logger.Info("[final]", "result", "all persistence mechanisms verified after restart") + return true } func runContainer(ctx context.Context, image, name string, env map[string]string) (*exec.Cmd, <-chan error, error) { @@ -765,35 +514,6 @@ func stopContainer(ctx context.Context, name string) error { return nil } -// dumpContainerDiagnostics prints container logs and inspect to structured logger for debugging startup failures -func dumpContainerDiagnostics(ctx context.Context, name string) error { - logger := logctx.FromContext(ctx) - logger.Info("[docker]", "action", "collecting logs", "name", name) - logsCmd := exec.CommandContext(ctx, "docker", "logs", name) - logsOut, _ := logsCmd.CombinedOutput() - if len(logsOut) > 0 { - scanner := bufio.NewScanner(bytes.NewReader(logsOut)) - for scanner.Scan() { - logger.Info("[docker]", "action", "diag logs", "line", scanner.Text()) - } - } - logger.Info("[docker]", "action", "inspect", "name", name) - inspectCmd := exec.CommandContext(ctx, "docker", "inspect", name) - inspectOut, _ := inspectCmd.CombinedOutput() - if len(inspectOut) > 0 { - // Trim to a reasonable size - const max = 64 * 1024 - if len(inspectOut) > max { - inspectOut = inspectOut[:max] - } - scanner := bufio.NewScanner(bytes.NewReader(inspectOut)) - for scanner.Scan() { - logger.Info("[docker]", "action", "diag inspect", "line", scanner.Text()) - } - } - return nil -} - func waitHTTPOrExit(ctx context.Context, url string, exitCh <-chan error) error { client := &http.Client{Timeout: 5 * time.Second} ticker := time.NewTicker(500 * time.Millisecond) @@ -848,239 +568,6 @@ func waitDevtoolsWS(ctx context.Context) (string, error) { return "ws://127.0.0.1:9222/", nil } -// startCookieTestServer starts an HTTP server listening on 0.0.0.0 -// It serves two pages: -// - /set-cookie: sets a deterministic (passed in to server initialization) cookie if not present and displays cookie state -// - /get-cookies: just displays existing cookies without setting anything -func startCookieTestServer(t *testing.T, cookieName, cookieValue string) (url string, stop func()) { - mux := http.NewServeMux() - nameJS, err := json.Marshal(cookieName) - if err != nil { - t.Fatalf("failed to marshal cookieName: %v", err) - } - valueJS, err := json.Marshal(cookieValue) - if err != nil { - t.Fatalf("failed to marshal cookieValue: %v", err) - } - - // Template for setting cookies - const setCookieHTML = ` - -
This page only displays cookies, it does not set any.
- - - -` - - setCookieTmpl := template.Must(template.New("set_cookie_page").Parse(setCookieHTML)) - var setCookieBuf bytes.Buffer - if err := setCookieTmpl.Execute(&setCookieBuf, map[string]interface{}{ - "NameJS": string(nameJS), - "ValueJS": string(valueJS), - }); err != nil { - t.Fatalf("failed to execute set cookie page template: %v", err) - } - - // Route that sets the cookie - mux.HandleFunc("/set-cookie", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - _, _ = io.WriteString(w, setCookieBuf.String()) - }) - - // Route that only displays cookies - mux.HandleFunc("/get-cookies", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - _, _ = io.WriteString(w, getCookiesHTML) - }) - - ln, err := net.Listen("tcp", "0.0.0.0:0") - if err != nil { - t.Fatalf("failed to start cookie test server: %v", err) - } - srv := &http.Server{Handler: mux} - go func() { _ = srv.Serve(ln) }() - - // figure out the random port assigned - _, port, _ := net.SplitHostPort(ln.Addr().String()) - url = "http://127.0.0.1:" + port - stop = func() { - _ = srv.Shutdown(context.Background()) - } - return url, stop -} - -// navigateAndEnsureCookie opens the given URL and asserts that the page's #cookies -// element contains name=value. It is idempotent and used before/after restarts. -func navigateAndEnsureCookie(ctx context.Context, wsURL, url, cookieName, cookieValue string, label string) error { - logger := logctx.FromContext(ctx) - - // Run playwright script - cmd := exec.CommandContext(ctx, "pnpm", "exec", "tsx", "index.ts", - "navigate-and-ensure-cookie", - "--url", url, - "--cookie-name", cookieName, - "--cookie-value", cookieValue, - "--label", label, - "--ws-url", wsURL, - "--timeout", "45000", - ) - cmd.Dir = getPlaywrightPath() - - output, err := cmd.CombinedOutput() - if err != nil { - logger.Info("[playwright]", "action", "navigate-and-ensure-cookie failed", "output", string(output)) - return fmt.Errorf("playwright navigate-and-ensure-cookie failed: %w, output: %s", err, string(output)) - } - - logger.Info("[playwright]", "action", "navigate-and-ensure-cookie success", "output", string(output)) - return nil -} - -// setAndVerifyLocalStorage sets a localStorage key-value pair and verifies it was set correctly -func setAndVerifyLocalStorage(ctx context.Context, wsURL, url, key, value, label string) error { - logger := logctx.FromContext(ctx) - - // Run playwright script - cmd := exec.CommandContext(ctx, "pnpm", "exec", "tsx", "index.ts", - "set-localstorage", - "--url", url, - "--key", key, - "--value", value, - "--label", label, - "--ws-url", wsURL, - "--timeout", "45000", - ) - cmd.Dir = getPlaywrightPath() - - output, err := cmd.CombinedOutput() - if err != nil { - logger.Info("[playwright]", "action", "set-localstorage failed", "output", string(output)) - return fmt.Errorf("playwright set-localstorage failed: %w, output: %s", err, string(output)) - } - - logger.Info("[playwright]", "action", "set-localstorage success", "output", string(output)) - return nil -} - -// verifyLocalStorage verifies that a localStorage key-value pair exists -func verifyLocalStorage(ctx context.Context, wsURL, url, key, value, label string) error { - logger := logctx.FromContext(ctx) - - // Run playwright script - cmd := exec.CommandContext(ctx, "pnpm", "exec", "tsx", "index.ts", - "verify-localstorage", - "--url", url, - "--key", key, - "--value", value, - "--label", label, - "--ws-url", wsURL, - "--timeout", "45000", - ) - cmd.Dir = getPlaywrightPath() - - output, err := cmd.CombinedOutput() - if err != nil { - logger.Info("[playwright]", "action", "verify-localstorage failed", "output", string(output)) - return fmt.Errorf("playwright verify-localstorage failed: %w, output: %s", err, string(output)) - } - - logger.Info("[playwright]", "action", "verify-localstorage success", "output", string(output)) - return nil -} - -// navigateToXAndVerifyCookie navigates to x.com and then to news.ycombinator.com to generate cookies, -// then verifies that the guest_id cookie was created for .x.com -func navigateToXAndVerifyCookie(ctx context.Context, wsURL string, label string) error { - logger := logctx.FromContext(ctx) - - // Run playwright script to navigate to x.com and back - cmd := exec.CommandContext(ctx, "pnpm", "exec", "tsx", "index.ts", - "navigate-to-x-and-back", - "--label", label, - "--ws-url", wsURL, - "--timeout", "45000", - ) - cmd.Dir = getPlaywrightPath() - - output, err := cmd.CombinedOutput() - if err != nil { - logger.Info("[playwright]", "action", "navigate-to-x-and-back failed", "output", string(output)) - return fmt.Errorf("playwright navigate-to-x-and-back failed: %w, output: %s", err, string(output)) - } - - logger.Info("[playwright]", "action", "navigate-to-x-and-back success", "output", string(output)) - - // Now verify the cookie was created by querying the database - logger.Info("[cookie-verify]", "action", "verifying guest_id cookie for .x.com") - - // wild: it takes about 10 seconds for cookies to flush to disk, and a supervisorctl stop / sigterm does not force it either. So we sleep - time.Sleep(15 * time.Second) - - // Execute SQLite query to check for the cookie - sqlQuery := `SELECT creation_utc,host_key,name,value,encrypted_value,last_update_utc FROM cookies WHERE host_key=".x.com" AND name="guest_id";` - - // Find the Cookies database file path - cookiesDBPath := "/home/kernel/user-data/Default/Cookies" - - stdout, err := execCombinedOutput(ctx, "sqlite3", []string{cookiesDBPath, "-header", "-column", sqlQuery}) - if err != nil { - return fmt.Errorf("failed to execute sqlite3 query on primary path: %w, output: %s", err, stdout) - } - - // Log the raw output for debugging - logger.Info("[cookie-verify]", "action", "sqlite3 output", "stdout", stdout) - - // Check if the output contains the expected cookie - if !strings.Contains(stdout, ".x.com") || !strings.Contains(stdout, "guest_id") { - logger.Error("[cookie-verify]", "action", "guest_id cookie not found", "output", stdout) - return fmt.Errorf("guest_id cookie for .x.com not found in database output: %s", stdout) - } - - logger.Info("[cookie-verify]", "action", "guest_id cookie verified successfully", "output", stdout) - return nil -} - func apiClient() (*instanceoapi.ClientWithResponses, error) { return instanceoapi.NewClientWithResponses(apiBaseURL, instanceoapi.WithHTTPClient(http.DefaultClient)) } @@ -1140,168 +627,6 @@ func execCombinedOutput(ctx context.Context, command string, args []string) (str return combined, nil } -func downloadUserDataZip(ctx context.Context) ([]byte, error) { - client, err := apiClient() - if err != nil { - return nil, err - } - params := &instanceoapi.DownloadDirZipParams{Path: "/home/kernel/user-data"} - rsp, err := client.DownloadDirZipWithResponse(ctx, params) - if err != nil { - return nil, err - } - if rsp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf("unexpected status downloading zip: %s body=%s", rsp.Status(), string(rsp.Body)) - } - return rsp.Body, nil -} - -func uploadUserDataZip(ctx context.Context, zipBytes []byte) error { - client, err := apiClient() - if err != nil { - return err - } - var body bytes.Buffer - w := multipart.NewWriter(&body) - fw, err := w.CreateFormFile("zip_file", "user-data.zip") - if err != nil { - return err - } - if _, err := io.Copy(fw, bytes.NewReader(zipBytes)); err != nil { - return err - } - if err := w.WriteField("dest_path", "/home/kernel/user-data"); err != nil { - return err - } - if err := w.Close(); err != nil { - return err - } - _, err = client.UploadZipWithBodyWithResponse(ctx, w.FormDataContentType(), &body) - if err != nil { - return err - } - if _, err := execCombinedOutput(ctx, "chown", []string{"-R", "kernel:kernel", "/home/kernel/user-data"}); err != nil { - return err - } - return nil -} - -func startChromiumViaAPI(ctx context.Context) error { - if out, err := execCombinedOutput(ctx, "supervisorctl", []string{"-c", "/etc/supervisor/supervisord.conf", "start", "chromium"}); err != nil { - return fmt.Errorf("failed to start chromium: %w, output: %s", err, out) - } - // Ensure process fully running before proceeding - if err := waitForProgramStates(ctx, "chromium", []string{"RUNNING"}, 10*time.Second); err != nil { - return err - } - return nil -} - -func deleteDirectoryViaAPI(ctx context.Context, path string) error { - client, err := apiClient() - if err != nil { - return err - } - body := instanceoapi.DeleteDirectoryJSONRequestBody{Path: path} - rsp, err := client.DeleteDirectoryWithResponse(ctx, body) - if err != nil { - return err - } - if rsp.StatusCode() != http.StatusOK { - return fmt.Errorf("unexpected status deleting directory: %s body=%s", rsp.Status(), string(rsp.Body)) - } - return nil -} - -func validateZip(b []byte) error { - r, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) - if err != nil { - return err - } - // Ensure at least one file - if len(r.File) == 0 { - return fmt.Errorf("empty zip") - } - // Try opening first file header to sanity-check - f := r.File[0] - rc, err := f.Open() - if err != nil { - return err - } - _, _ = io.Copy(io.Discard, rc) - rc.Close() - return nil -} - -type testLayout struct { - BaseDir string - ZipsDir string - RestoreDir string -} - -// createTestTempLayout creates .tmp/userdata-test-{timestamp}/ with subdirs for zips and the restored userdata directory (i.e. after saving and preparing for reuse) -func createTestTempLayout(t *testing.T) testLayout { - // Base under repo local .tmp - base := filepath.Join(".tmp", fmt.Sprintf("userdata-test-%d", time.Now().UnixNano())) - paths := []string{ - base, - filepath.Join(base, "zips"), - filepath.Join(base, "restored"), - } - for _, p := range paths { - if err := os.MkdirAll(p, 0700); err != nil { - t.Fatalf("create temp dir %s: %v", p, err) - } - } - return testLayout{ - BaseDir: base, - ZipsDir: filepath.Join(base, "zips"), - RestoreDir: filepath.Join(base, "restored"), - } -} - -// unzipBytesToDir extracts a zip archive (in-memory) into destDir -func unzipBytesToDir(b []byte, destDir string) error { - r, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) - if err != nil { - return err - } - for _, f := range r.File { - // Sanitize name - name := filepath.Clean(f.Name) - if strings.HasPrefix(name, "..") { - return fmt.Errorf("invalid zip path: %s", f.Name) - } - abs := filepath.Join(destDir, name) - if f.FileInfo().IsDir() { - if err := os.MkdirAll(abs, 0755); err != nil { - return err - } - continue - } - if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil { - return err - } - rc, err := f.Open() - if err != nil { - return err - } - w, err := os.OpenFile(abs, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode()) - if err != nil { - rc.Close() - return err - } - if _, err := io.Copy(w, rc); err != nil { - w.Close() - rc.Close() - return err - } - w.Close() - rc.Close() - } - return nil -} - // zipDirToBytes zips the contents of dir (no extra top-level folder) to bytes func zipDirToBytes(dir string) ([]byte, error) { var buf bytes.Buffer @@ -1353,232 +678,6 @@ func zipDirToBytes(dir string) ([]byte, error) { return buf.Bytes(), nil } -// verifyCookieInLocalSnapshot verifies cookie presence in local unzipped snapshot -func verifyCookieInLocalSnapshot(ctx context.Context, root string, cookieName, wantValue string) error { - logger := logctx.FromContext(ctx) - candidates := []string{ - filepath.Join(root, "Default", "Network", "Cookies"), - filepath.Join(root, "Default", "Cookies"), - } - - logger.Info("[verify]", "action", "checking cookie database", "cookieName", cookieName, "wantValue", wantValue) - - for _, p := range candidates { - logger.Info("[verify]", "action", "checking database", "path", p) - ok, err := inspectLocalCookiesDB(ctx, p, cookieName, wantValue) - if err == nil && ok { - logger.Info("[verify]", "action", "cookie found", "path", p) - return nil - } - if err != nil { - logger.Warn("[verify]", "action", "database check failed", "path", p, "error", err) - } - } - return fmt.Errorf("cookie %q not found in local snapshot", cookieName) -} - -func inspectLocalCookiesDB(ctx context.Context, dbPath, cookieName, wantValue string) (bool, error) { - logger := logctx.FromContext(ctx) - - // If db does not exist, skip - if _, err := os.Stat(dbPath); err != nil { - logger.Info("[inspect]", "action", "database file not found", "path", dbPath, "error", err) - return false, nil - } - - logger.Info("[inspect]", "action", "opening database", "path", dbPath) - db, err := sql.Open("sqlite", dbPath+"?_pragma=query_only(1)&_pragma=journal_mode(wal)") - if err != nil { - logger.Warn("[inspect]", "action", "failed to open database", "path", dbPath, "error", err) - return false, err - } - defer db.Close() - - rows, err := db.QueryContext(ctx, "SELECT name, value, length(encrypted_value) FROM cookies") - if err != nil { - logger.Warn("[inspect]", "action", "failed to query cookies", "path", dbPath, "error", err) - return false, err - } - defer rows.Close() - - logger.Info("[inspect]", "action", "scanning cookies from database", "path", dbPath) - cookieCount := 0 - for rows.Next() { - var name, value string - var encLen int64 - if err := rows.Scan(&name, &value, &encLen); err != nil { - logger.Warn("[inspect]", "action", "failed to scan cookie row", "error", err) - continue - } - cookieCount++ - logger.Info("[inspect]", "action", "found cookie", "name", name, "value", value, "encrypted", encLen > 0) - - if name == cookieName { - if value == wantValue || encLen > 0 { - logger.Info("[inspect]", "action", "target cookie found", "name", name, "value", value, "encrypted", encLen > 0) - return true, nil - } - } - } - - logger.Info("[inspect]", "action", "database scan complete", "path", dbPath, "totalCookies", cookieCount) - return false, rows.Err() -} - -// deleteLocalSingletonLockFiles removes Chromium singleton files in a local snapshot -func deleteLocalSingletonLockFiles(root string) error { - for _, name := range []string{"SingletonLock", "SingletonCookie", "SingletonSocket", "RunningChromeVersion"} { - p := filepath.Join(root, name) - _ = os.Remove(p) - } - return nil -} - -// execCommandWithResponse is a helper function that executes a command via the remote API -// and handles the response parsing consistently across all callers -// Deprecated: use execCombinedOutput instead -func execCommandWithResponse(ctx context.Context, command string, args []string) (*instanceoapi.ProcessExecResponse, error) { - client, err := apiClient() - if err != nil { - return nil, err - } - - req := instanceoapi.ProcessExecJSONRequestBody{ - Command: command, - Args: &args, - } - - return client.ProcessExecWithResponse(ctx, req) -} - -// stopChromiumViaSupervisord stops chromium using supervisord via the remote API -func stopChromiumViaSupervisord(ctx context.Context) error { - logger := logctx.FromContext(ctx) - - // Wait a bit for any pending I/O to complete - logger.Info("[stop]", "action", "waiting for I/O flush", "seconds", 3) - time.Sleep(3 * time.Second) - - // Now use supervisorctl to ensure it's fully stopped - logger.Info("[stop]", "action", "stopping via supervisorctl") - if out, stopErr := execCombinedOutput(ctx, "supervisorctl", []string{"-c", "/etc/supervisor/supervisord.conf", "stop", "chromium"}); stopErr != nil { - return fmt.Errorf("failed to stop chromium via supervisorctl: %w, output: %s", stopErr, out) - } - - // Accept either STOPPED or EXITED as terminal stopped states - desiredStates := []string{"STOPPED", "EXITED"} - if waitErr := waitForProgramStates(ctx, "chromium", desiredStates, 5*time.Second); waitErr != nil { - return fmt.Errorf("chromium did not reach a stopped state: %w", waitErr) - } - - // Allow a short grace period for I/O flush - time.Sleep(1 * time.Second) - return nil -} - -// getProgramState returns the current supervisor state (e.g. RUNNING, STOPPED, EXITED) for the given program. -// It parses the output of `supervisorctl status` even if the command exits with a non-zero status code, which -// supervisorctl does when the target program is not in the RUNNING state. -func getProgramState(ctx context.Context, programName string) (string, error) { - stdout, err := execCombinedOutput(ctx, "supervisorctl", []string{"-c", "/etc/supervisor/supervisord.conf", "status", programName}) - if err != nil { - if execErr, ok := err.(*RemoteExecError); ok && execErr.ExitCode == 3 { - stdout = execErr.Output - } else { - return "", err - } - } - - // Expected output example: - // "chromium STOPPED Sep 21 10:05 AM" - // "chromium EXITED Sep 21 10:05 AM (exit status 0)" - fields := strings.Fields(stdout) - if len(fields) < 2 { - return "", fmt.Errorf("unexpected supervisorctl status output: %s", stdout) - } - return fields[1], nil -} - -// waitForProgramStates polls supervisorctl status until the program reaches any of the desired states -// or the timeout expires. -func waitForProgramStates(ctx context.Context, programName string, desiredStates []string, timeout time.Duration) error { - deadline := time.Now().Add(timeout) - - contains := func(list []string, s string) bool { - for _, v := range list { - if v == s { - return true - } - } - return false - } - - for { - state, err := getProgramState(ctx, programName) - if err == nil && contains(desiredStates, state) { - return nil - } - - if time.Now().After(deadline) { - if err != nil { - return err - } - return fmt.Errorf("timeout waiting for %s to reach states %v (last state %s)", programName, desiredStates, state) - } - time.Sleep(500 * time.Millisecond) - } -} - -// runCookieDebugScript executes the cookie debug script in the container to check file ownership and permissions -func runCookieDebugScript(ctx context.Context, t *testing.T) error { - logger := logctx.FromContext(ctx) - - // Read the debug script content - scriptContent, err := os.ReadFile("cookie_debug.sh") - if err != nil { - return fmt.Errorf("failed to read debug script: %w", err) - } - - // Execute the script content directly via bash - args := []string{"-c", string(scriptContent)} - stdout, err := execCombinedOutput(ctx, "bash", args) - if err != nil { - return fmt.Errorf("failed to execute debug script: %w, output: %s", err, stdout) - } - - logger.Info("[diagnostic]", "action", "debug script output") - fmt.Fprint(t.Output(), stdout) - return nil -} - -// verifyCookieInContainerDB checks that the specified cookie exists in the cookies database on the container -func verifyCookieInContainerDB(ctx context.Context, cookieName string) error { - logger := logctx.FromContext(ctx) - - // Execute SQLite query to check for the cookie - sqlQuery := fmt.Sprintf(`SELECT creation_utc,host_key,name,value,encrypted_value,last_update_utc FROM cookies WHERE name="%s";`, cookieName) - - // Find the Cookies database file path - cookiesDBPath := "/home/kernel/user-data/Default/Cookies" - - stdout, err := execCombinedOutput(ctx, "sqlite3", []string{cookiesDBPath, "-header", "-column", sqlQuery}) - if err != nil { - return fmt.Errorf("failed to execute sqlite3 query: %w, output: %s", err, stdout) - } - - // Log the raw output for debugging - logger.Info("[container-cookie-verify]", "action", "sqlite3 output", "stdout", stdout) - - // Check if the output contains the expected cookie - if !strings.Contains(stdout, cookieName) { - logger.Error("[container-cookie-verify]", "action", "cookie not found", "cookieName", cookieName, "output", stdout) - return fmt.Errorf("cookie %q not found in container database output: %s", cookieName, stdout) - } - - logger.Info("[container-cookie-verify]", "action", "cookie verified successfully", "cookieName", cookieName, "output", stdout) - return nil -} - // getXvfbResolution extracts the Xvfb resolution from the ps aux output // It looks for the Xvfb command line which contains "-screen 0 WIDTHxHEIGHTx24" func getXvfbResolution(ctx context.Context) (width, height int, err error) { diff --git a/server/go.mod b/server/go.mod index 66ede4e5..9b16ea6c 100644 --- a/server/go.mod +++ b/server/go.mod @@ -14,8 +14,9 @@ require ( github.com/m1k1o/neko/server v0.0.0-20251008185748-46e2fc7d3866 github.com/nrednav/cuid2 v1.1.0 github.com/oapi-codegen/runtime v1.1.2 - github.com/stretchr/testify v1.10.0 - golang.org/x/sync v0.15.0 + github.com/samber/lo v1.52.0 + github.com/stretchr/testify v1.11.1 + golang.org/x/sync v0.17.0 ) require ( @@ -38,6 +39,7 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/crypto v0.40.0 // indirect golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/gorm v1.25.7 // indirect diff --git a/server/go.sum b/server/go.sum index e12ca801..95301225 100644 --- a/server/go.sum +++ b/server/go.sum @@ -70,20 +70,24 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 0d47670c..6ef6ba33 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -27,11 +27,11 @@ import ( // Defines values for ClickMouseRequestButton. const ( - Back ClickMouseRequestButton = "back" - Forward ClickMouseRequestButton = "forward" - Left ClickMouseRequestButton = "left" - Middle ClickMouseRequestButton = "middle" - Right ClickMouseRequestButton = "right" + ClickMouseRequestButtonBack ClickMouseRequestButton = "back" + ClickMouseRequestButtonForward ClickMouseRequestButton = "forward" + ClickMouseRequestButtonLeft ClickMouseRequestButton = "left" + ClickMouseRequestButtonMiddle ClickMouseRequestButton = "middle" + ClickMouseRequestButtonRight ClickMouseRequestButton = "right" ) // Defines values for ClickMouseRequestClickType. @@ -41,6 +41,13 @@ const ( Up ClickMouseRequestClickType = "up" ) +// Defines values for DragMouseRequestButton. +const ( + DragMouseRequestButtonLeft DragMouseRequestButton = "left" + DragMouseRequestButtonMiddle DragMouseRequestButton = "middle" + DragMouseRequestButtonRight DragMouseRequestButton = "right" +) + // Defines values for FileSystemEventType. const ( CREATE FileSystemEventType = "CREATE" @@ -148,6 +155,24 @@ type DisplayConfig struct { Width *int `json:"width,omitempty"` } +// DragMouseRequest defines model for DragMouseRequest. +type DragMouseRequest struct { + // Button Mouse button to drag with + Button *DragMouseRequestButton `json:"button,omitempty"` + + // Delay Delay in milliseconds between button down and starting to move along the path. + Delay *int `json:"delay,omitempty"` + + // HoldKeys Modifier keys to hold during the drag + HoldKeys *[]string `json:"hold_keys,omitempty"` + + // Path Ordered list of [x, y] coordinate pairs to move through while dragging. Must contain at least 2 points. + Path [][]int `json:"path"` +} + +// DragMouseRequestButton Mouse button to drag with +type DragMouseRequestButton string + // Error defines model for Error. type Error struct { Message string `json:"message"` @@ -252,6 +277,19 @@ type PatchDisplayRequest struct { // PatchDisplayRequestRefreshRate Display refresh rate in Hz. If omitted, uses the highest available rate for the resolution. type PatchDisplayRequestRefreshRate int +// PressKeyRequest defines model for PressKeyRequest. +type PressKeyRequest struct { + // Duration Duration to hold the keys down in milliseconds. If omitted or 0, keys are tapped. + Duration *int `json:"duration,omitempty"` + + // HoldKeys Optional modifier keys to hold during the key press sequence. + HoldKeys *[]string `json:"hold_keys,omitempty"` + + // Keys List of key symbols to press. Each item should be a key symbol supported by xdotool + // (see X11 keysym definitions). Examples include "Return", "Shift", "Ctrl", "Alt", "F5". + Keys []string `json:"keys"` +} + // ProcessExecRequest Request to execute a command synchronously. type ProcessExecRequest struct { // Args Command arguments. @@ -397,6 +435,24 @@ type ScreenshotRequest struct { Region *ScreenshotRegion `json:"region,omitempty"` } +// ScrollRequest defines model for ScrollRequest. +type ScrollRequest struct { + // DeltaX Horizontal scroll amount. Positive scrolls right, negative scrolls left. + DeltaX *int `json:"delta_x,omitempty"` + + // DeltaY Vertical scroll amount. Positive scrolls down, negative scrolls up. + DeltaY *int `json:"delta_y,omitempty"` + + // HoldKeys Modifier keys to hold during the scroll + HoldKeys *[]string `json:"hold_keys,omitempty"` + + // X X coordinate at which to perform the scroll + X int `json:"x"` + + // Y Y coordinate at which to perform the scroll + Y int `json:"y"` +} + // SetFilePermissionsRequest defines model for SetFilePermissionsRequest. type SetFilePermissionsRequest struct { // Group New group name or GID. @@ -563,12 +619,21 @@ type UploadExtensionsAndRestartMultipartRequestBody UploadExtensionsAndRestartMu // ClickMouseJSONRequestBody defines body for ClickMouse for application/json ContentType. type ClickMouseJSONRequestBody = ClickMouseRequest +// DragMouseJSONRequestBody defines body for DragMouse for application/json ContentType. +type DragMouseJSONRequestBody = DragMouseRequest + // MoveMouseJSONRequestBody defines body for MoveMouse for application/json ContentType. type MoveMouseJSONRequestBody = MoveMouseRequest +// PressKeyJSONRequestBody defines body for PressKey for application/json ContentType. +type PressKeyJSONRequestBody = PressKeyRequest + // TakeScreenshotJSONRequestBody defines body for TakeScreenshot for application/json ContentType. type TakeScreenshotJSONRequestBody = ScreenshotRequest +// ScrollJSONRequestBody defines body for Scroll for application/json ContentType. +type ScrollJSONRequestBody = ScrollRequest + // TypeTextJSONRequestBody defines body for TypeText for application/json ContentType. type TypeTextJSONRequestBody = TypeTextRequest @@ -706,16 +771,31 @@ type ClientInterface interface { ClickMouse(ctx context.Context, body ClickMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // DragMouseWithBody request with any body + DragMouseWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + DragMouse(ctx context.Context, body DragMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // MoveMouseWithBody request with any body MoveMouseWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) MoveMouse(ctx context.Context, body MoveMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // PressKeyWithBody request with any body + PressKeyWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PressKey(ctx context.Context, body PressKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // TakeScreenshotWithBody request with any body TakeScreenshotWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) TakeScreenshot(ctx context.Context, body TakeScreenshotJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ScrollWithBody request with any body + ScrollWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + Scroll(ctx context.Context, body ScrollJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // TypeTextWithBody request with any body TypeTextWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -894,6 +974,30 @@ func (c *Client) ClickMouse(ctx context.Context, body ClickMouseJSONRequestBody, return c.Client.Do(req) } +func (c *Client) DragMouseWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDragMouseRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DragMouse(ctx context.Context, body DragMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDragMouseRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) MoveMouseWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewMoveMouseRequestWithBody(c.Server, contentType, body) if err != nil { @@ -918,6 +1022,30 @@ func (c *Client) MoveMouse(ctx context.Context, body MoveMouseJSONRequestBody, r return c.Client.Do(req) } +func (c *Client) PressKeyWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPressKeyRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PressKey(ctx context.Context, body PressKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPressKeyRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) TakeScreenshotWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewTakeScreenshotRequestWithBody(c.Server, contentType, body) if err != nil { @@ -942,6 +1070,30 @@ func (c *Client) TakeScreenshot(ctx context.Context, body TakeScreenshotJSONRequ return c.Client.Do(req) } +func (c *Client) ScrollWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewScrollRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) Scroll(ctx context.Context, body ScrollJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewScrollRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) TypeTextWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewTypeTextRequestWithBody(c.Server, contentType, body) if err != nil { @@ -1579,6 +1731,46 @@ func NewClickMouseRequestWithBody(server string, contentType string, body io.Rea return req, nil } +// NewDragMouseRequest calls the generic DragMouse builder with application/json body +func NewDragMouseRequest(server string, body DragMouseJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewDragMouseRequestWithBody(server, "application/json", bodyReader) +} + +// NewDragMouseRequestWithBody generates requests for DragMouse with any type of body +func NewDragMouseRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/computer/drag_mouse") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewMoveMouseRequest calls the generic MoveMouse builder with application/json body func NewMoveMouseRequest(server string, body MoveMouseJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -1619,6 +1811,46 @@ func NewMoveMouseRequestWithBody(server string, contentType string, body io.Read return req, nil } +// NewPressKeyRequest calls the generic PressKey builder with application/json body +func NewPressKeyRequest(server string, body PressKeyJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPressKeyRequestWithBody(server, "application/json", bodyReader) +} + +// NewPressKeyRequestWithBody generates requests for PressKey with any type of body +func NewPressKeyRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/computer/press_key") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewTakeScreenshotRequest calls the generic TakeScreenshot builder with application/json body func NewTakeScreenshotRequest(server string, body TakeScreenshotJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -1659,6 +1891,46 @@ func NewTakeScreenshotRequestWithBody(server string, contentType string, body io return req, nil } +// NewScrollRequest calls the generic Scroll builder with application/json body +func NewScrollRequest(server string, body ScrollJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewScrollRequestWithBody(server, "application/json", bodyReader) +} + +// NewScrollRequestWithBody generates requests for Scroll with any type of body +func NewScrollRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/computer/scroll") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewTypeTextRequest calls the generic TypeText builder with application/json body func NewTypeTextRequest(server string, body TypeTextJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -2935,16 +3207,31 @@ type ClientWithResponsesInterface interface { ClickMouseWithResponse(ctx context.Context, body ClickMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*ClickMouseResponse, error) + // DragMouseWithBodyWithResponse request with any body + DragMouseWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DragMouseResponse, error) + + DragMouseWithResponse(ctx context.Context, body DragMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*DragMouseResponse, error) + // MoveMouseWithBodyWithResponse request with any body MoveMouseWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MoveMouseResponse, error) MoveMouseWithResponse(ctx context.Context, body MoveMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*MoveMouseResponse, error) + // PressKeyWithBodyWithResponse request with any body + PressKeyWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PressKeyResponse, error) + + PressKeyWithResponse(ctx context.Context, body PressKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*PressKeyResponse, error) + // TakeScreenshotWithBodyWithResponse request with any body TakeScreenshotWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*TakeScreenshotResponse, error) TakeScreenshotWithResponse(ctx context.Context, body TakeScreenshotJSONRequestBody, reqEditors ...RequestEditorFn) (*TakeScreenshotResponse, error) + // ScrollWithBodyWithResponse request with any body + ScrollWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ScrollResponse, error) + + ScrollWithResponse(ctx context.Context, body ScrollJSONRequestBody, reqEditors ...RequestEditorFn) (*ScrollResponse, error) + // TypeTextWithBodyWithResponse request with any body TypeTextWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*TypeTextResponse, error) @@ -3132,6 +3419,29 @@ func (r ClickMouseResponse) StatusCode() int { return 0 } +type DragMouseResponse struct { + Body []byte + HTTPResponse *http.Response + JSON400 *BadRequestError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r DragMouseResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DragMouseResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type MoveMouseResponse struct { Body []byte HTTPResponse *http.Response @@ -3155,6 +3465,29 @@ func (r MoveMouseResponse) StatusCode() int { return 0 } +type PressKeyResponse struct { + Body []byte + HTTPResponse *http.Response + JSON400 *BadRequestError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r PressKeyResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PressKeyResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type TakeScreenshotResponse struct { Body []byte HTTPResponse *http.Response @@ -3178,6 +3511,29 @@ func (r TakeScreenshotResponse) StatusCode() int { return 0 } +type ScrollResponse struct { + Body []byte + HTTPResponse *http.Response + JSON400 *BadRequestError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r ScrollResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ScrollResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type TypeTextResponse struct { Body []byte HTTPResponse *http.Response @@ -3920,6 +4276,23 @@ func (c *ClientWithResponses) ClickMouseWithResponse(ctx context.Context, body C return ParseClickMouseResponse(rsp) } +// DragMouseWithBodyWithResponse request with arbitrary body returning *DragMouseResponse +func (c *ClientWithResponses) DragMouseWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DragMouseResponse, error) { + rsp, err := c.DragMouseWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseDragMouseResponse(rsp) +} + +func (c *ClientWithResponses) DragMouseWithResponse(ctx context.Context, body DragMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*DragMouseResponse, error) { + rsp, err := c.DragMouse(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseDragMouseResponse(rsp) +} + // MoveMouseWithBodyWithResponse request with arbitrary body returning *MoveMouseResponse func (c *ClientWithResponses) MoveMouseWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MoveMouseResponse, error) { rsp, err := c.MoveMouseWithBody(ctx, contentType, body, reqEditors...) @@ -3937,6 +4310,23 @@ func (c *ClientWithResponses) MoveMouseWithResponse(ctx context.Context, body Mo return ParseMoveMouseResponse(rsp) } +// PressKeyWithBodyWithResponse request with arbitrary body returning *PressKeyResponse +func (c *ClientWithResponses) PressKeyWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PressKeyResponse, error) { + rsp, err := c.PressKeyWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePressKeyResponse(rsp) +} + +func (c *ClientWithResponses) PressKeyWithResponse(ctx context.Context, body PressKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*PressKeyResponse, error) { + rsp, err := c.PressKey(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePressKeyResponse(rsp) +} + // TakeScreenshotWithBodyWithResponse request with arbitrary body returning *TakeScreenshotResponse func (c *ClientWithResponses) TakeScreenshotWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*TakeScreenshotResponse, error) { rsp, err := c.TakeScreenshotWithBody(ctx, contentType, body, reqEditors...) @@ -3954,6 +4344,23 @@ func (c *ClientWithResponses) TakeScreenshotWithResponse(ctx context.Context, bo return ParseTakeScreenshotResponse(rsp) } +// ScrollWithBodyWithResponse request with arbitrary body returning *ScrollResponse +func (c *ClientWithResponses) ScrollWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ScrollResponse, error) { + rsp, err := c.ScrollWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseScrollResponse(rsp) +} + +func (c *ClientWithResponses) ScrollWithResponse(ctx context.Context, body ScrollJSONRequestBody, reqEditors ...RequestEditorFn) (*ScrollResponse, error) { + rsp, err := c.Scroll(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseScrollResponse(rsp) +} + // TypeTextWithBodyWithResponse request with arbitrary body returning *TypeTextResponse func (c *ClientWithResponses) TypeTextWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*TypeTextResponse, error) { rsp, err := c.TypeTextWithBody(ctx, contentType, body, reqEditors...) @@ -4307,43 +4714,142 @@ func (c *ClientWithResponses) StartRecordingWithBodyWithResponse(ctx context.Con if err != nil { return nil, err } - return ParseStartRecordingResponse(rsp) + return ParseStartRecordingResponse(rsp) +} + +func (c *ClientWithResponses) StartRecordingWithResponse(ctx context.Context, body StartRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*StartRecordingResponse, error) { + rsp, err := c.StartRecording(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseStartRecordingResponse(rsp) +} + +// StopRecordingWithBodyWithResponse request with arbitrary body returning *StopRecordingResponse +func (c *ClientWithResponses) StopRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StopRecordingResponse, error) { + rsp, err := c.StopRecordingWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseStopRecordingResponse(rsp) +} + +func (c *ClientWithResponses) StopRecordingWithResponse(ctx context.Context, body StopRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*StopRecordingResponse, error) { + rsp, err := c.StopRecording(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseStopRecordingResponse(rsp) +} + +// ParsePatchChromiumFlagsResponse parses an HTTP response from a PatchChromiumFlagsWithResponse call +func ParsePatchChromiumFlagsResponse(rsp *http.Response) (*PatchChromiumFlagsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PatchChromiumFlagsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseUploadExtensionsAndRestartResponse parses an HTTP response from a UploadExtensionsAndRestartWithResponse call +func ParseUploadExtensionsAndRestartResponse(rsp *http.Response) (*UploadExtensionsAndRestartResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &UploadExtensionsAndRestartResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil } -func (c *ClientWithResponses) StartRecordingWithResponse(ctx context.Context, body StartRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*StartRecordingResponse, error) { - rsp, err := c.StartRecording(ctx, body, reqEditors...) +// ParseClickMouseResponse parses an HTTP response from a ClickMouseWithResponse call +func ParseClickMouseResponse(rsp *http.Response) (*ClickMouseResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - return ParseStartRecordingResponse(rsp) -} -// StopRecordingWithBodyWithResponse request with arbitrary body returning *StopRecordingResponse -func (c *ClientWithResponses) StopRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StopRecordingResponse, error) { - rsp, err := c.StopRecordingWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err + response := &ClickMouseResponse{ + Body: bodyBytes, + HTTPResponse: rsp, } - return ParseStopRecordingResponse(rsp) -} -func (c *ClientWithResponses) StopRecordingWithResponse(ctx context.Context, body StopRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*StopRecordingResponse, error) { - rsp, err := c.StopRecording(ctx, body, reqEditors...) - if err != nil { - return nil, err + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + } - return ParseStopRecordingResponse(rsp) + + return response, nil } -// ParsePatchChromiumFlagsResponse parses an HTTP response from a PatchChromiumFlagsWithResponse call -func ParsePatchChromiumFlagsResponse(rsp *http.Response) (*PatchChromiumFlagsResponse, error) { +// ParseDragMouseResponse parses an HTTP response from a DragMouseWithResponse call +func ParseDragMouseResponse(rsp *http.Response) (*DragMouseResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &PatchChromiumFlagsResponse{ + response := &DragMouseResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -4368,15 +4874,15 @@ func ParsePatchChromiumFlagsResponse(rsp *http.Response) (*PatchChromiumFlagsRes return response, nil } -// ParseUploadExtensionsAndRestartResponse parses an HTTP response from a UploadExtensionsAndRestartWithResponse call -func ParseUploadExtensionsAndRestartResponse(rsp *http.Response) (*UploadExtensionsAndRestartResponse, error) { +// ParseMoveMouseResponse parses an HTTP response from a MoveMouseWithResponse call +func ParseMoveMouseResponse(rsp *http.Response) (*MoveMouseResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &UploadExtensionsAndRestartResponse{ + response := &MoveMouseResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -4401,15 +4907,15 @@ func ParseUploadExtensionsAndRestartResponse(rsp *http.Response) (*UploadExtensi return response, nil } -// ParseClickMouseResponse parses an HTTP response from a ClickMouseWithResponse call -func ParseClickMouseResponse(rsp *http.Response) (*ClickMouseResponse, error) { +// ParsePressKeyResponse parses an HTTP response from a PressKeyWithResponse call +func ParsePressKeyResponse(rsp *http.Response) (*PressKeyResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ClickMouseResponse{ + response := &PressKeyResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -4434,15 +4940,15 @@ func ParseClickMouseResponse(rsp *http.Response) (*ClickMouseResponse, error) { return response, nil } -// ParseMoveMouseResponse parses an HTTP response from a MoveMouseWithResponse call -func ParseMoveMouseResponse(rsp *http.Response) (*MoveMouseResponse, error) { +// ParseTakeScreenshotResponse parses an HTTP response from a TakeScreenshotWithResponse call +func ParseTakeScreenshotResponse(rsp *http.Response) (*TakeScreenshotResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &MoveMouseResponse{ + response := &TakeScreenshotResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -4467,15 +4973,15 @@ func ParseMoveMouseResponse(rsp *http.Response) (*MoveMouseResponse, error) { return response, nil } -// ParseTakeScreenshotResponse parses an HTTP response from a TakeScreenshotWithResponse call -func ParseTakeScreenshotResponse(rsp *http.Response) (*TakeScreenshotResponse, error) { +// ParseScrollResponse parses an HTTP response from a ScrollWithResponse call +func ParseScrollResponse(rsp *http.Response) (*ScrollResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &TakeScreenshotResponse{ + response := &ScrollResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5671,12 +6177,21 @@ type ServerInterface interface { // Simulate a mouse click action on the host computer // (POST /computer/click_mouse) ClickMouse(w http.ResponseWriter, r *http.Request) + // Drag the mouse along a path + // (POST /computer/drag_mouse) + DragMouse(w http.ResponseWriter, r *http.Request) // Move the mouse cursor to the specified coordinates on the host computer // (POST /computer/move_mouse) MoveMouse(w http.ResponseWriter, r *http.Request) + // Press one or more keys on the host computer + // (POST /computer/press_key) + PressKey(w http.ResponseWriter, r *http.Request) // Capture a screenshot of the host computer // (POST /computer/screenshot) TakeScreenshot(w http.ResponseWriter, r *http.Request) + // Scroll the mouse wheel at a position on the host computer + // (POST /computer/scroll) + Scroll(w http.ResponseWriter, r *http.Request) // Type text on the host computer // (POST /computer/type) TypeText(w http.ResponseWriter, r *http.Request) @@ -5788,18 +6303,36 @@ func (_ Unimplemented) ClickMouse(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// Drag the mouse along a path +// (POST /computer/drag_mouse) +func (_ Unimplemented) DragMouse(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Move the mouse cursor to the specified coordinates on the host computer // (POST /computer/move_mouse) func (_ Unimplemented) MoveMouse(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// Press one or more keys on the host computer +// (POST /computer/press_key) +func (_ Unimplemented) PressKey(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Capture a screenshot of the host computer // (POST /computer/screenshot) func (_ Unimplemented) TakeScreenshot(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// Scroll the mouse wheel at a position on the host computer +// (POST /computer/scroll) +func (_ Unimplemented) Scroll(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Type text on the host computer // (POST /computer/type) func (_ Unimplemented) TypeText(w http.ResponseWriter, r *http.Request) { @@ -6025,6 +6558,20 @@ func (siw *ServerInterfaceWrapper) ClickMouse(w http.ResponseWriter, r *http.Req handler.ServeHTTP(w, r) } +// DragMouse operation middleware +func (siw *ServerInterfaceWrapper) DragMouse(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DragMouse(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // MoveMouse operation middleware func (siw *ServerInterfaceWrapper) MoveMouse(w http.ResponseWriter, r *http.Request) { @@ -6039,6 +6586,20 @@ func (siw *ServerInterfaceWrapper) MoveMouse(w http.ResponseWriter, r *http.Requ handler.ServeHTTP(w, r) } +// PressKey operation middleware +func (siw *ServerInterfaceWrapper) PressKey(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PressKey(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // TakeScreenshot operation middleware func (siw *ServerInterfaceWrapper) TakeScreenshot(w http.ResponseWriter, r *http.Request) { @@ -6053,6 +6614,20 @@ func (siw *ServerInterfaceWrapper) TakeScreenshot(w http.ResponseWriter, r *http handler.ServeHTTP(w, r) } +// Scroll operation middleware +func (siw *ServerInterfaceWrapper) Scroll(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.Scroll(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // TypeText operation middleware func (siw *ServerInterfaceWrapper) TypeText(w http.ResponseWriter, r *http.Request) { @@ -6812,12 +7387,21 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/computer/click_mouse", wrapper.ClickMouse) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/computer/drag_mouse", wrapper.DragMouse) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/computer/move_mouse", wrapper.MoveMouse) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/computer/press_key", wrapper.PressKey) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/computer/screenshot", wrapper.TakeScreenshot) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/computer/scroll", wrapper.Scroll) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/computer/type", wrapper.TypeText) }) @@ -7019,6 +7603,40 @@ func (response ClickMouse500JSONResponse) VisitClickMouseResponse(w http.Respons return json.NewEncoder(w).Encode(response) } +type DragMouseRequestObject struct { + Body *DragMouseJSONRequestBody +} + +type DragMouseResponseObject interface { + VisitDragMouseResponse(w http.ResponseWriter) error +} + +type DragMouse200Response struct { +} + +func (response DragMouse200Response) VisitDragMouseResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type DragMouse400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response DragMouse400JSONResponse) VisitDragMouseResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type DragMouse500JSONResponse struct{ InternalErrorJSONResponse } + +func (response DragMouse500JSONResponse) VisitDragMouseResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type MoveMouseRequestObject struct { Body *MoveMouseJSONRequestBody } @@ -7053,6 +7671,40 @@ func (response MoveMouse500JSONResponse) VisitMoveMouseResponse(w http.ResponseW return json.NewEncoder(w).Encode(response) } +type PressKeyRequestObject struct { + Body *PressKeyJSONRequestBody +} + +type PressKeyResponseObject interface { + VisitPressKeyResponse(w http.ResponseWriter) error +} + +type PressKey200Response struct { +} + +func (response PressKey200Response) VisitPressKeyResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type PressKey400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response PressKey400JSONResponse) VisitPressKeyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type PressKey500JSONResponse struct{ InternalErrorJSONResponse } + +func (response PressKey500JSONResponse) VisitPressKeyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type TakeScreenshotRequestObject struct { Body *TakeScreenshotJSONRequestBody } @@ -7098,6 +7750,40 @@ func (response TakeScreenshot500JSONResponse) VisitTakeScreenshotResponse(w http return json.NewEncoder(w).Encode(response) } +type ScrollRequestObject struct { + Body *ScrollJSONRequestBody +} + +type ScrollResponseObject interface { + VisitScrollResponse(w http.ResponseWriter) error +} + +type Scroll200Response struct { +} + +func (response Scroll200Response) VisitScrollResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type Scroll400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response Scroll400JSONResponse) VisitScrollResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type Scroll500JSONResponse struct{ InternalErrorJSONResponse } + +func (response Scroll500JSONResponse) VisitScrollResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type TypeTextRequestObject struct { Body *TypeTextJSONRequestBody } @@ -8453,12 +9139,21 @@ type StrictServerInterface interface { // Simulate a mouse click action on the host computer // (POST /computer/click_mouse) ClickMouse(ctx context.Context, request ClickMouseRequestObject) (ClickMouseResponseObject, error) + // Drag the mouse along a path + // (POST /computer/drag_mouse) + DragMouse(ctx context.Context, request DragMouseRequestObject) (DragMouseResponseObject, error) // Move the mouse cursor to the specified coordinates on the host computer // (POST /computer/move_mouse) MoveMouse(ctx context.Context, request MoveMouseRequestObject) (MoveMouseResponseObject, error) + // Press one or more keys on the host computer + // (POST /computer/press_key) + PressKey(ctx context.Context, request PressKeyRequestObject) (PressKeyResponseObject, error) // Capture a screenshot of the host computer // (POST /computer/screenshot) TakeScreenshot(ctx context.Context, request TakeScreenshotRequestObject) (TakeScreenshotResponseObject, error) + // Scroll the mouse wheel at a position on the host computer + // (POST /computer/scroll) + Scroll(ctx context.Context, request ScrollRequestObject) (ScrollResponseObject, error) // Type text on the host computer // (POST /computer/type) TypeText(ctx context.Context, request TypeTextRequestObject) (TypeTextResponseObject, error) @@ -8670,6 +9365,37 @@ func (sh *strictHandler) ClickMouse(w http.ResponseWriter, r *http.Request) { } } +// DragMouse operation middleware +func (sh *strictHandler) DragMouse(w http.ResponseWriter, r *http.Request) { + var request DragMouseRequestObject + + var body DragMouseJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.DragMouse(ctx, request.(DragMouseRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "DragMouse") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(DragMouseResponseObject); ok { + if err := validResponse.VisitDragMouseResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // MoveMouse operation middleware func (sh *strictHandler) MoveMouse(w http.ResponseWriter, r *http.Request) { var request MoveMouseRequestObject @@ -8701,6 +9427,37 @@ func (sh *strictHandler) MoveMouse(w http.ResponseWriter, r *http.Request) { } } +// PressKey operation middleware +func (sh *strictHandler) PressKey(w http.ResponseWriter, r *http.Request) { + var request PressKeyRequestObject + + var body PressKeyJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PressKey(ctx, request.(PressKeyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PressKey") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PressKeyResponseObject); ok { + if err := validResponse.VisitPressKeyResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // TakeScreenshot operation middleware func (sh *strictHandler) TakeScreenshot(w http.ResponseWriter, r *http.Request) { var request TakeScreenshotRequestObject @@ -8732,6 +9489,37 @@ func (sh *strictHandler) TakeScreenshot(w http.ResponseWriter, r *http.Request) } } +// Scroll operation middleware +func (sh *strictHandler) Scroll(w http.ResponseWriter, r *http.Request) { + var request ScrollRequestObject + + var body ScrollJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.Scroll(ctx, request.(ScrollRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "Scroll") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ScrollResponseObject); ok { + if err := validResponse.VisitScrollResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // TypeText operation middleware func (sh *strictHandler) TypeText(w http.ResponseWriter, r *http.Request) { var request TypeTextRequestObject @@ -9578,98 +10366,113 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "efyYjfxfAAAA///E79iFinwAAA==", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 3d599296..66f79366 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -349,6 +349,70 @@ paths: $ref: "#/components/responses/BadRequestError" "500": $ref: "#/components/responses/InternalError" + /computer/press_key: + post: + summary: Press one or more keys on the host computer + description: | + Presses the specified keys for an optional duration. Keys should be key symbols + supported by xdotool. For a comprehensive list of key symbols, see + the X11 keysym definitions at https://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h + The server honors millisecond-level durations using fractional sleeps under the hood. + operationId: pressKey + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PressKeyRequest" + responses: + "200": + description: Keys pressed successfully + "400": + $ref: "#/components/responses/BadRequestError" + "500": + $ref: "#/components/responses/InternalError" + /computer/scroll: + post: + summary: Scroll the mouse wheel at a position on the host computer + description: | + Scroll vertically and/or horizontally at the given coordinates, optionally while holding modifier keys. + The scroll amounts are in logical ticks (not pixels) and application behavior may vary; tuning may be required. + When both horizontal and vertical deltas are provided, the server applies them sequentially (vertical then horizontal) + and batches wheel events using `xdotool click --repeat N --delay 0