diff --git a/server/cmd/api/api/api.go b/server/cmd/api/api/api.go index 68c36092..2ee7ee9d 100644 --- a/server/cmd/api/api/api.go +++ b/server/cmd/api/api/api.go @@ -6,7 +6,6 @@ import ( "fmt" "log/slog" "os" - "os/exec" "sync" "time" @@ -89,14 +88,9 @@ type ApiService struct { // inputMu serializes input-related operations (mouse, keyboard, screenshot) inputMu sync.Mutex - // playwrightMu serializes Playwright code execution (only one execution at a time) - playwrightMu sync.Mutex - - // playwrightDaemonStarting is an atomic flag to prevent concurrent daemon starts - playwrightDaemonStarting int32 - - // playwrightDaemonCmd holds the daemon process for cleanup - playwrightDaemonCmd *exec.Cmd + // playwrightExecutors runs Playwright calls, each executor (including the + // default one) in its own daemon process. + playwrightExecutors *playwrightExecutorManager browserRepl *browserReplManager @@ -179,7 +173,7 @@ func New( _ = mon.SetTelemetry(false) ctx, cancel := context.WithCancel(context.Background()) - return &ApiService{ + s := &ApiService{ recordManager: recordManager, factory: factory, defaultRecorderID: "default", @@ -198,7 +192,9 @@ func New( browserRepl: newBrowserReplManager(), lifecycleCtx: ctx, lifecycleCancel: cancel, - }, nil + } + s.playwrightExecutors = newPlaywrightExecutorManager(cdpPlaywrightExecutorTabs{withCDP: s.withCDPClient}) + return s, nil } func (s *ApiService) StartRecording(ctx context.Context, req oapi.StartRecordingRequestObject) (oapi.StartRecordingResponseObject, error) { @@ -472,6 +468,7 @@ func (s *ApiService) NetworkMetrics() (resets, completed uint64, up bool) { func (s *ApiService) Shutdown(ctx context.Context) error { s.lifecycleCancel() replErr := s.browserRepl.Shutdown(ctx) + s.playwrightExecutors.Shutdown(ctx) _ = s.webmcp.Close() s.monitorMu.Lock() diff --git a/server/cmd/api/api/browser_repl.go b/server/cmd/api/api/browser_repl.go index c3222fcf..3ee1aa53 100644 --- a/server/cmd/api/api/browser_repl.go +++ b/server/cmd/api/api/browser_repl.go @@ -228,7 +228,7 @@ func (m *browserReplManager) ensureLocked(ctx context.Context) error { "repl_id", child.id, "exit_err", err) child.done = closedWaitChannel(err) // The group leader exited, but descendants may still be alive. - _ = signalBrowserReplGroup(child.cmd, killSignal) + _ = signalChildProcessGroup(child.cmd, killSignal) m.clearLocked(ctx, child) default: return nil @@ -282,7 +282,7 @@ func (m *browserReplManager) startLocked(ctx context.Context) error { "BROWSER_REPL_SOCKET="+socketPath, "BROWSER_REPL_ID="+replID, ) - configureBrowserReplCmd(cmd) + configureChildProcessCmd(cmd) log.Info("starting browser REPL", "repl_id", replID, "socket", socketPath) if err := cmd.Start(); err != nil { @@ -307,7 +307,7 @@ func (m *browserReplManager) startLocked(ctx context.Context) error { select { case waitErr := <-child.done: child.done = closedWaitChannel(waitErr) - _ = signalBrowserReplGroup(child.cmd, killSignal) + _ = signalChildProcessGroup(child.cmd, killSignal) m.clearLocked(ctx, child) return fmt.Errorf("browser REPL exited during startup: %w", waitErr) case <-ctx.Done(): @@ -338,21 +338,21 @@ func (m *browserReplManager) terminateLocked(ctx context.Context, reason string) log.Info("terminating browser REPL", "repl_id", child.id, "reason", reason) // SIGTERM the whole process group so any grandchildren go down too. - _ = signalBrowserReplGroup(child.cmd, termSignal) + _ = signalChildProcessGroup(child.cmd, termSignal) select { case err := <-child.done: child.done = closedWaitChannel(err) // The group leader exiting does not imply descendants honored SIGTERM. // Kill the process group before relinquishing ownership. - _ = signalBrowserReplGroup(child.cmd, killSignal) + _ = signalChildProcessGroup(child.cmd, killSignal) m.clearLocked(ctx, child) return err case <-time.After(browserReplShutdownGrace): } log.Warn("browser REPL did not exit on SIGTERM; escalating to SIGKILL", "repl_id", child.id) - _ = signalBrowserReplGroup(child.cmd, killSignal) + _ = signalChildProcessGroup(child.cmd, killSignal) var waitErr error select { @@ -364,7 +364,7 @@ func (m *browserReplManager) terminateLocked(ctx context.Context, reason string) } // Re-signal after the leader is reaped: descendants remain members of the // original process group even if the leader exited first. - _ = signalBrowserReplGroup(child.cmd, killSignal) + _ = signalChildProcessGroup(child.cmd, killSignal) m.clearLocked(ctx, child) return waitErr } @@ -382,7 +382,7 @@ func (m *browserReplManager) killLocked(ctx context.Context, reason string) { } log := logger.FromContext(ctx) log.Info("killing browser REPL", "repl_id", child.id, "reason", reason) - _ = signalBrowserReplGroup(child.cmd, killSignal) + _ = signalChildProcessGroup(child.cmd, killSignal) select { case err := <-child.done: @@ -390,7 +390,7 @@ func (m *browserReplManager) killLocked(ctx context.Context, reason string) { case <-time.After(browserReplShutdownGrace): log.Error("browser REPL did not exit after SIGKILL", "repl_id", child.id) } - _ = signalBrowserReplGroup(child.cmd, killSignal) + _ = signalChildProcessGroup(child.cmd, killSignal) m.clearLocked(ctx, child) } diff --git a/server/cmd/api/api/browser_repl_proc_linux.go b/server/cmd/api/api/browser_repl_proc_linux.go deleted file mode 100644 index a199d138..00000000 --- a/server/cmd/api/api/browser_repl_proc_linux.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build linux - -package api - -import ( - "os/exec" - "syscall" -) - -var ( - termSignal = syscall.SIGTERM - killSignal = syscall.SIGKILL -) - -// configureBrowserReplCmd puts the REPL child in its own process group so a -// reset or timeout also terminates subprocesses. Parent-death signaling stops -// the daemon if the API process exits unexpectedly. -func configureBrowserReplCmd(cmd *exec.Cmd) { - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, - Pdeathsig: syscall.SIGKILL, - } -} - -func signalBrowserReplGroup(cmd *exec.Cmd, sig syscall.Signal) error { - if cmd == nil || cmd.Process == nil { - return nil - } - return syscall.Kill(-cmd.Process.Pid, sig) -} diff --git a/server/cmd/api/api/child_process_linux.go b/server/cmd/api/api/child_process_linux.go new file mode 100644 index 00000000..0ddfb03d --- /dev/null +++ b/server/cmd/api/api/child_process_linux.go @@ -0,0 +1,31 @@ +//go:build linux + +package api + +import ( + "os/exec" + "syscall" +) + +var ( + termSignal = syscall.SIGTERM + killSignal = syscall.SIGKILL +) + +// configureChildProcessCmd puts an API-owned child (Browser REPL or Playwright +// daemon) in its own process group so a reset or timeout also terminates its +// subprocesses. Parent-death signaling stops the child if the API process +// exits unexpectedly. +func configureChildProcessCmd(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + Pdeathsig: syscall.SIGKILL, + } +} + +func signalChildProcessGroup(cmd *exec.Cmd, sig syscall.Signal) error { + if cmd == nil || cmd.Process == nil { + return nil + } + return syscall.Kill(-cmd.Process.Pid, sig) +} diff --git a/server/cmd/api/api/browser_repl_proc_other.go b/server/cmd/api/api/child_process_other.go similarity index 82% rename from server/cmd/api/api/browser_repl_proc_other.go rename to server/cmd/api/api/child_process_other.go index 0dd7e6a2..edb6e7e7 100644 --- a/server/cmd/api/api/browser_repl_proc_other.go +++ b/server/cmd/api/api/child_process_other.go @@ -15,9 +15,9 @@ var ( killSignal = syscall.SIGKILL ) -func configureBrowserReplCmd(cmd *exec.Cmd) {} +func configureChildProcessCmd(cmd *exec.Cmd) {} -func signalBrowserReplGroup(cmd *exec.Cmd, sig syscall.Signal) error { +func signalChildProcessGroup(cmd *exec.Cmd, sig syscall.Signal) error { if cmd == nil || cmd.Process == nil { return nil } diff --git a/server/cmd/api/api/playwright.go b/server/cmd/api/api/playwright.go index 395a45ed..c91f0eb9 100644 --- a/server/cmd/api/api/playwright.go +++ b/server/cmd/api/api/playwright.go @@ -1,187 +1,112 @@ package api import ( - "bufio" "context" - "encoding/json" + "errors" "fmt" - "net" - "os" - "os/exec" - "sync/atomic" "time" - "github.com/google/uuid" "github.com/kernel/kernel-images/server/lib/logger" "github.com/kernel/kernel-images/server/lib/oapi" ) const ( - playwrightDaemonSocket = "/tmp/playwright-daemon.sock" playwrightDaemonScript = "/usr/local/lib/playwright-daemon.js" playwrightDaemonStartup = 5 * time.Second ) type playwrightDaemonRequest struct { - ID string `json:"id"` - Code string `json:"code"` - TimeoutMs int `json:"timeout_ms,omitempty"` + ID string `json:"id"` + Code string `json:"code"` + TimeoutMs int `json:"timeout_ms,omitempty"` + Executor string `json:"executor,omitempty"` + TargetID string `json:"target_id,omitempty"` + TabCreated bool `json:"tab_created,omitempty"` } type playwrightDaemonResponse struct { - ID string `json:"id"` - Success bool `json:"success"` - Result interface{} `json:"result,omitempty"` - Error string `json:"error,omitempty"` - Stack string `json:"stack,omitempty"` + ID string `json:"id"` + Success bool `json:"success"` + Result interface{} `json:"result,omitempty"` + Error string `json:"error,omitempty"` + Stack string `json:"stack,omitempty"` + TargetID string `json:"target_id,omitempty"` + TabCreated bool `json:"tab_created,omitempty"` + TimedOut bool `json:"timed_out,omitempty"` + TabMissing bool `json:"tab_missing,omitempty"` } -func (s *ApiService) ensurePlaywrightDaemon(ctx context.Context) error { - log := logger.FromContext(ctx) - - if conn, err := net.DialTimeout("unix", playwrightDaemonSocket, 100*time.Millisecond); err == nil { - conn.Close() - return nil - } - - if !atomic.CompareAndSwapInt32(&s.playwrightDaemonStarting, 0, 1) { - deadline := time.Now().Add(playwrightDaemonStartup) - for time.Now().Before(deadline) { - if conn, err := net.DialTimeout("unix", playwrightDaemonSocket, 100*time.Millisecond); err == nil { - conn.Close() - return nil - } - time.Sleep(100 * time.Millisecond) - } - return fmt.Errorf("timeout waiting for daemon to start") +func (s *ApiService) ExecutePlaywrightCode(ctx context.Context, request oapi.ExecutePlaywrightCodeRequestObject) (oapi.ExecutePlaywrightCodeResponseObject, error) { + if request.Body == nil || request.Body.Code == "" { + return oapi.ExecutePlaywrightCode400JSONResponse{ + BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "code is required", + }, + }, nil } - defer atomic.StoreInt32(&s.playwrightDaemonStarting, 0) - log.Info("starting playwright daemon") - - cmd := exec.Command("node", playwrightDaemonScript) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Env = os.Environ() + RecordTelemetryCode(ctx, request.Body.Code) - if err := cmd.Start(); err != nil { - return fmt.Errorf("failed to start playwright daemon: %w", err) + timeout := 60 * time.Second + if request.Body.TimeoutSec != nil && *request.Body.TimeoutSec > 0 { + timeout = time.Duration(*request.Body.TimeoutSec) * time.Second } - s.playwrightDaemonCmd = cmd - - deadline := time.Now().Add(playwrightDaemonStartup) - for time.Now().Before(deadline) { - if conn, err := net.DialTimeout("unix", playwrightDaemonSocket, 100*time.Millisecond); err == nil { - conn.Close() - log.Info("playwright daemon started successfully") - return nil - } - time.Sleep(100 * time.Millisecond) + name := defaultPlaywrightExecutor + if request.Body.Executor != nil { + name = *request.Body.Executor } - - cmd.Process.Kill() - return fmt.Errorf("playwright daemon failed to start within %v", playwrightDaemonStartup) + return s.executePlaywrightOnExecutor(ctx, name, request.Body.Code, timeout) } -func (s *ApiService) executeViaUnixSocket(ctx context.Context, code string, timeout time.Duration) (*playwrightDaemonResponse, error) { - conn, err := net.DialTimeout("unix", playwrightDaemonSocket, 2*time.Second) - if err != nil { - return nil, fmt.Errorf("failed to connect to daemon: %w", err) - } - defer conn.Close() - - if err := conn.SetDeadline(time.Now().Add(timeout + 5*time.Second)); err != nil { - return nil, fmt.Errorf("failed to set deadline: %w", err) - } - - reqID := uuid.New().String() - req := playwrightDaemonRequest{ - ID: reqID, - Code: code, - TimeoutMs: int(timeout.Milliseconds()), - } - - reqBytes, err := json.Marshal(req) - if err != nil { - return nil, fmt.Errorf("failed to marshal request: %w", err) - } - reqBytes = append(reqBytes, '\n') - - if _, err := conn.Write(reqBytes); err != nil { - return nil, fmt.Errorf("failed to send request: %w", err) - } - - reader := bufio.NewReader(conn) - respLine, err := reader.ReadBytes('\n') - if err != nil { - return nil, fmt.Errorf("failed to read response: %w", err) +func playwrightResult(resp *playwrightDaemonResponse) oapi.ExecutePlaywrightResult { + result := oapi.ExecutePlaywrightResult{Success: resp.Success} + if resp.TargetID != "" { + result.Tab = &oapi.PlaywrightTab{TargetId: resp.TargetID, Created: resp.TabCreated} } - - var resp playwrightDaemonResponse - if err := json.Unmarshal(respLine, &resp); err != nil { - return nil, fmt.Errorf("failed to parse response: %w", err) - } - - if resp.ID != reqID { - return nil, fmt.Errorf("response ID mismatch: expected %s, got %s", reqID, resp.ID) + if resp.Success { + result.Result = &resp.Result + } else { + result.Error = &resp.Error + result.Stderr = &resp.Stack } - - return &resp, nil + return result } -func (s *ApiService) ExecutePlaywrightCode(ctx context.Context, request oapi.ExecutePlaywrightCodeRequestObject) (oapi.ExecutePlaywrightCodeResponseObject, error) { - s.playwrightMu.Lock() - defer s.playwrightMu.Unlock() - +func (s *ApiService) executePlaywrightOnExecutor(ctx context.Context, name, code string, timeout time.Duration) (oapi.ExecutePlaywrightCodeResponseObject, error) { log := logger.FromContext(ctx) - if request.Body == nil || request.Body.Code == "" { + if !playwrightExecutorNamePattern.MatchString(name) { return oapi.ExecutePlaywrightCode400JSONResponse{ BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ - Message: "code is required", + Message: "executor name must match " + playwrightExecutorNamePattern.String(), }, }, nil } - RecordTelemetryCode(ctx, request.Body.Code) - - timeout := 60 * time.Second - if request.Body.TimeoutSec != nil && *request.Body.TimeoutSec > 0 { - timeout = time.Duration(*request.Body.TimeoutSec) * time.Second + resp, err := s.playwrightExecutors.Execute(ctx, name, code, timeout) + var limitErr *playwrightExecutorLimitError + if errors.As(err, &limitErr) { + return oapi.ExecutePlaywrightCode409JSONResponse{ + Message: limitErr.Error(), + Executors: s.playwrightExecutorsJSON(ctx, limitErr.executors), + }, nil } - - if err := s.ensurePlaywrightDaemon(ctx); err != nil { - log.Error("failed to ensure playwright daemon", "error", err) + if errors.Is(err, errPlaywrightExecutorSetup) { + log.Error("failed to set up playwright executor", "executor", name, "error", err) return oapi.ExecutePlaywrightCode500JSONResponse{ InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ - Message: fmt.Sprintf("failed to start playwright daemon: %v", err), + Message: err.Error(), }, }, nil } - - resp, err := s.executeViaUnixSocket(ctx, request.Body.Code, timeout) if err != nil { - log.Error("playwright execution failed", "error", err) + log.Error("playwright executor execution failed", "executor", name, "error", err) errorMsg := fmt.Sprintf("execution failed: %v", err) return oapi.ExecutePlaywrightCode200JSONResponse{ Success: false, Error: &errorMsg, }, nil } - - if !resp.Success { - errorMsg := resp.Error - stderr := resp.Stack - return oapi.ExecutePlaywrightCode200JSONResponse{ - Success: false, - Error: &errorMsg, - Stderr: &stderr, - }, nil - } - - return oapi.ExecutePlaywrightCode200JSONResponse{ - Success: true, - Result: &resp.Result, - }, nil + return oapi.ExecutePlaywrightCode200JSONResponse(playwrightResult(resp)), nil } diff --git a/server/cmd/api/api/playwright_executors.go b/server/cmd/api/api/playwright_executors.go new file mode 100644 index 00000000..5cd58ba3 --- /dev/null +++ b/server/cmd/api/api/playwright_executors.go @@ -0,0 +1,685 @@ +package api + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "regexp" + "slices" + "strings" + "sync" + "time" + + "github.com/google/uuid" + "github.com/kernel/kernel-images/server/lib/cdpclient" + "github.com/kernel/kernel-images/server/lib/logger" + "github.com/kernel/kernel-images/server/lib/oapi" +) + +const ( + // defaultPlaywrightExecutor runs calls that name no executor. It always + // exists, binds `page` to the active tab instead of owning one, and does + // not count toward maxPlaywrightExecutors. + defaultPlaywrightExecutor = "default" + + maxPlaywrightExecutors = 8 + + // playwrightExecutorResponseGrace is added to the execution timeout when + // setting the socket deadline, giving the daemon time to report its own + // timeout before the API treats the process as unresponsive. + playwrightExecutorResponseGrace = 5 * time.Second + + // playwrightExecutorKillGrace bounds how long a kill waits for the + // process to be reaped. + playwrightExecutorKillGrace = 3 * time.Second +) + +var ( + playwrightExecutorNamePattern = regexp.MustCompile(`^[A-Za-z0-9_-]{1,64}$`) + + errPlaywrightExecutorDeleted = errors.New("executor was deleted") + errPlaywrightExecutorRestarted = errors.New("executor was restarted") + errPlaywrightExecutorsShutDown = errors.New("playwright executors are shutting down") + errPlaywrightExecutorNotFound = errors.New("executor not found") + errPlaywrightExecutorSetup = errors.New("failed to set up playwright executor") +) + +type playwrightExecutorLimitError struct { + executors []playwrightExecutorInfo +} + +func (e *playwrightExecutorLimitError) Error() string { + return fmt.Sprintf("named executor limit (%d) reached; delete one with DELETE /playwright/executors/{name}", maxPlaywrightExecutors) +} + +// playwrightExecutorTabs opens and closes executor tabs. The manager owns +// every executor tab, so a tab's ID is recorded before any daemon process can +// use it and DELETE or shutdown can always find it. +type playwrightExecutorTabs interface { + Open(ctx context.Context) (string, error) + Close(ctx context.Context, targetID string) error +} + +// cdpPlaywrightExecutorTabs manages executor tabs over the browser's DevTools +// connection. +type cdpPlaywrightExecutorTabs struct { + withCDP func(context.Context, func(context.Context, *cdpclient.Client) error) error +} + +// Open creates a background tab in the default browser context, so it does +// not take focus from the live view or change the active tab that calls +// without an executor bind to. +func (t cdpPlaywrightExecutorTabs) Open(ctx context.Context) (string, error) { + var created struct { + TargetID string `json:"targetId"` + } + err := t.withCDP(ctx, func(ctx context.Context, c *cdpclient.Client) error { + raw, err := c.Send(ctx, "Target.createTarget", map[string]any{"url": "about:blank", "background": true}, "") + if err != nil { + return err + } + return json.Unmarshal(raw, &created) + }) + return created.TargetID, err +} + +// Close closes a tab. A tab that is already gone has nothing left to close. +func (t cdpPlaywrightExecutorTabs) Close(ctx context.Context, targetID string) error { + err := t.withCDP(ctx, func(ctx context.Context, c *cdpclient.Client) error { + _, err := c.Send(ctx, "Target.closeTarget", map[string]any{"targetId": targetID}, "") + return err + }) + if err != nil && strings.Contains(err.Error(), "No target with given id") { + return nil + } + return err +} + +// playwrightExecutorChild is one executor's daemon process. +type playwrightExecutorChild struct { + cmd *exec.Cmd + socket string + exited chan struct{} // closed once cmd.Wait returns + killOnce sync.Once +} + +func (c *playwrightExecutorChild) alive() bool { + select { + case <-c.exited: + return false + default: + return true + } +} + +// kill SIGKILLs the child's process group, waits for it to be reaped, and +// removes its socket. The group is signaled once, even if the leader already +// exited, so descendants are not left behind; later calls do nothing, so a +// reused process group ID is never signaled. +func (c *playwrightExecutorChild) kill() { + c.killOnce.Do(func() { + _ = signalChildProcessGroup(c.cmd, killSignal) + select { + case <-c.exited: + case <-time.After(playwrightExecutorKillGrace): + } + _ = os.Remove(c.socket) + }) +} + +// playwrightExecutor is a named execution lane. admission serializes calls on +// it; every other field is guarded by the manager's mu. +type playwrightExecutor struct { + name string + // pinned marks the default executor: it is never removed, owns no tab, + // and binds `page` to the active tab. + pinned bool + createdAt time.Time + admission chan struct{} + lastUsedAt time.Time + busy bool + targetID string + child *playwrightExecutorChild + // restarts counts restarts of the default executor. A call that sees it + // change between admission and its result was interrupted by a restart. + restarts uint64 + // gone is set when the executor is removed (deleted, shut down, or never + // started); calls still holding it fail with this error. + gone error +} + +// playwrightExecutorInfo is a point-in-time copy of an executor's listed +// fields, safe to read without the manager's mu. +type playwrightExecutorInfo struct { + name string + pinned bool + createdAt time.Time + lastUsedAt time.Time + busy bool + targetID string +} + +// playwrightExecutorManager owns the named executors behind +// POST /playwright/execute. Each executor runs in its own daemon process, so +// calls on different executors run concurrently and a timeout or crash only +// takes down that executor's process. The manager opens each executor's tab +// and keeps its target ID, so a replacement process binds `page` to the same +// tab. +type playwrightExecutorManager struct { + script string + socketDir string + responseGrace time.Duration + tabs playwrightExecutorTabs + + mu sync.Mutex + executors map[string]*playwrightExecutor + closed bool +} + +func newPlaywrightExecutorManager(tabs playwrightExecutorTabs) *playwrightExecutorManager { + m := &playwrightExecutorManager{ + tabs: tabs, + script: playwrightDaemonScript, + socketDir: os.TempDir(), + responseGrace: playwrightExecutorResponseGrace, + executors: make(map[string]*playwrightExecutor), + } + e := newPlaywrightExecutor(defaultPlaywrightExecutor) + e.pinned = true + m.executors[e.name] = e + return m +} + +func newPlaywrightExecutor(name string) *playwrightExecutor { + now := time.Now() + e := &playwrightExecutor{ + name: name, + createdAt: now, + lastUsedAt: now, + admission: make(chan struct{}, 1), + } + e.admission <- struct{}{} + return e +} + +// executorFor returns the named executor, creating it if there is room. +func (m *playwrightExecutorManager) executorFor(name string) (*playwrightExecutor, error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.closed { + return nil, errPlaywrightExecutorsShutDown + } + if e, ok := m.executors[name]; ok { + return e, nil + } + if m.namedCountLocked() >= maxPlaywrightExecutors { + return nil, &playwrightExecutorLimitError{executors: m.snapshotLocked()} + } + e := newPlaywrightExecutor(name) + m.executors[name] = e + return e, nil +} + +// namedCountLocked counts the executors that take a slot. The caller must hold +// mu. +func (m *playwrightExecutorManager) namedCountLocked() int { + n := 0 + for _, e := range m.executors { + if !e.pinned { + n++ + } + } + return n +} + +// Execute runs code on the named executor, creating the executor, starting its +// process and opening its tab as needed. A failure after the call was handed to +// the process comes back as an unsuccessful response that still reports the +// executor's tab. +func (m *playwrightExecutorManager) Execute(ctx context.Context, name, code string, timeout time.Duration) (*playwrightDaemonResponse, error) { + e, err := m.executorFor(name) + if err != nil { + return nil, err + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-e.admission: + } + defer func() { e.admission <- struct{}{} }() + + m.mu.Lock() + if e.gone != nil { + m.mu.Unlock() + return nil, e.gone + } + e.busy = true + e.lastUsedAt = time.Now() + targetID := e.targetID + restarts := e.restarts + m.mu.Unlock() + defer func() { + m.mu.Lock() + e.busy = false + m.mu.Unlock() + }() + + child, err := m.ensureChild(ctx, e, restarts) + if err != nil { + return nil, err + } + + created := false + if targetID == "" && !e.pinned { + if targetID, err = m.openTab(ctx, e); err != nil { + return nil, err + } + created = true + } + resp, err := m.send(child, e, code, targetID, created, timeout) + if err == nil && resp.TabMissing && !e.pinned { + // The tab was closed since the last call. The daemon answered without + // running the code, so it is safe to send it again with a new tab. + if targetID, err = m.openTab(ctx, e); err != nil { + return nil, err + } + created = true + resp, err = m.send(child, e, code, targetID, created, timeout) + } + + // An unresponsive or dead process is replaced on the next call. The + // daemon cannot interrupt abandoned user code, so a named executor's + // process is also replaced after a timeout. The default executor keeps its + // process after a timeout, as calls without an executor always have: the + // daemon drops its browser connection, so abandoned code cannot keep + // driving the browser, and the next call does not pay a cold start. + replace := err != nil || (resp.TimedOut && !e.pinned) + m.mu.Lock() + gone := e.gone + restarted := e.restarts != restarts + if replace && e.child == child { + e.child = nil + } + m.mu.Unlock() + if replace { + child.kill() + } + + if gone != nil { + return nil, gone + } + if restarted { + return nil, errPlaywrightExecutorRestarted + } + if err != nil { + logger.FromContext(ctx).Error("playwright executor call failed", "executor", name, "error", err) + return &playwrightDaemonResponse{ + Success: false, + Error: fmt.Sprintf("execution failed: %v", err), + TargetID: targetID, + TabCreated: created, + }, nil + } + return resp, nil +} + +// ensureChild returns the executor's live process, starting a new one if +// needed. restarts is the executor's restart count when the call was +// admitted; a process started across a restart is discarded. The caller must +// hold the executor's admission. +func (m *playwrightExecutorManager) ensureChild(ctx context.Context, e *playwrightExecutor, restarts uint64) (*playwrightExecutorChild, error) { + m.mu.Lock() + child := e.child + m.mu.Unlock() + if child != nil && child.alive() { + return child, nil + } + if child != nil { + child.kill() + } + + child, err := m.start(ctx, e.name) + if err != nil { + m.mu.Lock() + // A named executor whose process never started has no tab yet; drop it + // so it does not take one of the limited slots. + if !e.pinned && e.targetID == "" && m.executors[e.name] == e { + delete(m.executors, e.name) + e.gone = err + } + m.mu.Unlock() + return nil, err + } + + m.mu.Lock() + gone := e.gone + if gone == nil && e.restarts != restarts { + gone = errPlaywrightExecutorRestarted + } + if gone == nil { + e.child = child + } + m.mu.Unlock() + if gone != nil { + child.kill() + return nil, gone + } + return child, nil +} + +// openTab opens a new tab for the executor and records it before any process +// uses it. The caller must hold the executor's admission. +func (m *playwrightExecutorManager) openTab(ctx context.Context, e *playwrightExecutor) (string, error) { + targetID, err := m.tabs.Open(ctx) + if err != nil { + return "", fmt.Errorf("%w: open tab: %w", errPlaywrightExecutorSetup, err) + } + m.mu.Lock() + e.targetID = targetID + m.mu.Unlock() + return targetID, nil +} + +func (m *playwrightExecutorManager) start(ctx context.Context, name string) (*playwrightExecutorChild, error) { + log := logger.FromContext(ctx) + socket := filepath.Join(m.socketDir, "playwright-executor-"+uuid.NewString()+".sock") + + cmd := exec.Command("node", m.script) + cmd.Stdout = os.Stderr + cmd.Stderr = os.Stderr + cmd.Env = append(os.Environ(), "PLAYWRIGHT_DAEMON_SOCKET="+socket) + configureChildProcessCmd(cmd) + + log.Info("starting playwright executor", "executor", name) + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("%w: %w", errPlaywrightExecutorSetup, err) + } + child := &playwrightExecutorChild{cmd: cmd, socket: socket, exited: make(chan struct{})} + go func() { + _ = cmd.Wait() + close(child.exited) + }() + + deadline := time.Now().Add(playwrightDaemonStartup) + for time.Now().Before(deadline) { + if conn, err := net.DialTimeout("unix", socket, 100*time.Millisecond); err == nil { + conn.Close() + return child, nil + } + if !child.alive() { + child.kill() + return nil, fmt.Errorf("%w: process exited during startup", errPlaywrightExecutorSetup) + } + time.Sleep(50 * time.Millisecond) + } + child.kill() + return nil, fmt.Errorf("%w: not ready within %v", errPlaywrightExecutorSetup, playwrightDaemonStartup) +} + +// send runs one call on the child, bound to the given tab. created tells the +// daemon the tab was just opened, so it waits longer for it to appear. Calls +// on the default executor carry no executor name, so the daemon binds the +// active tab. +func (m *playwrightExecutorManager) send(child *playwrightExecutorChild, e *playwrightExecutor, code, targetID string, created bool, timeout time.Duration) (*playwrightDaemonResponse, error) { + conn, err := net.DialTimeout("unix", child.socket, 2*time.Second) + if err != nil { + return nil, fmt.Errorf("failed to connect to executor: %w", err) + } + defer conn.Close() + + if err := conn.SetDeadline(time.Now().Add(timeout + m.responseGrace)); err != nil { + return nil, fmt.Errorf("failed to set deadline: %w", err) + } + + reqID := uuid.NewString() + req := playwrightDaemonRequest{ + ID: reqID, + Code: code, + TimeoutMs: int(timeout.Milliseconds()), + TargetID: targetID, + TabCreated: created, + } + if !e.pinned { + req.Executor = e.name + } + reqBytes, err := json.Marshal(req) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + if _, err := conn.Write(append(reqBytes, '\n')); err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + + respLine, err := bufio.NewReader(conn).ReadBytes('\n') + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + var resp playwrightDaemonResponse + if err := json.Unmarshal(respLine, &resp); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + if resp.ID != reqID { + return nil, fmt.Errorf("response ID mismatch: expected %s, got %s", reqID, resp.ID) + } + return &resp, nil +} + +// Delete removes the named executor and kills its process. It returns the +// executor's tab target ID, if it had one, so the caller can close the tab. +// The default executor is restarted instead: its process is killed without +// waiting for queued calls, the call it interrupts fails, and the next call +// starts a new process. +func (m *playwrightExecutorManager) Delete(ctx context.Context, name string) (string, error) { + m.mu.Lock() + e, ok := m.executors[name] + if !ok { + m.mu.Unlock() + return "", errPlaywrightExecutorNotFound + } + if e.pinned { + e.restarts++ + child := e.child + e.child = nil + m.mu.Unlock() + if child != nil { + child.kill() + } + return "", nil + } + delete(m.executors, name) + e.gone = errPlaywrightExecutorDeleted + m.mu.Unlock() + + return m.retire(ctx, e), nil +} + +// retire kills a removed executor's process and returns its tab target ID, if +// any. It waits for an in-flight call to unwind first, so a tab that call +// opened is included: killing the process makes the call return promptly, +// and a call records a tab before handing it to the process. The caller must +// already have removed the executor and set gone. +func (m *playwrightExecutorManager) retire(ctx context.Context, e *playwrightExecutor) string { + m.mu.Lock() + child := e.child + e.child = nil + m.mu.Unlock() + + if child != nil { + child.kill() + } + + select { + case <-e.admission: + e.admission <- struct{}{} + case <-ctx.Done(): + } + + m.mu.Lock() + defer m.mu.Unlock() + return e.targetID +} + +// List returns the executors, the default executor first and the rest oldest +// first. +func (m *playwrightExecutorManager) List() []playwrightExecutorInfo { + m.mu.Lock() + defer m.mu.Unlock() + return m.snapshotLocked() +} + +// snapshotLocked copies the executors so callers can read them without mu. +// The caller must hold mu. +func (m *playwrightExecutorManager) snapshotLocked() []playwrightExecutorInfo { + executors := make([]playwrightExecutorInfo, 0, len(m.executors)) + for _, e := range m.executors { + executors = append(executors, playwrightExecutorInfo{ + name: e.name, + pinned: e.pinned, + createdAt: e.createdAt, + lastUsedAt: e.lastUsedAt, + busy: e.busy, + targetID: e.targetID, + }) + } + slices.SortFunc(executors, func(a, b playwrightExecutorInfo) int { + if a.pinned != b.pinned { + if a.pinned { + return -1 + } + return 1 + } + return a.createdAt.Compare(b.createdAt) + }) + return executors +} + +// Shutdown kills every executor process and closes the executors' tabs, which +// a later API process would have no way to reach. +func (m *playwrightExecutorManager) Shutdown(ctx context.Context) { + m.mu.Lock() + m.closed = true + executors := make([]*playwrightExecutor, 0, len(m.executors)) + for _, e := range m.executors { + e.gone = errPlaywrightExecutorsShutDown + executors = append(executors, e) + } + m.executors = make(map[string]*playwrightExecutor) + m.mu.Unlock() + + // Retiring and closing get separate budgets, so slow process exits cannot + // use up the time needed to close the tabs they leave behind. + retireCtx, cancelRetire := context.WithTimeout(ctx, playwrightExecutorKillGrace) + defer cancelRetire() + targetIDs := make([]string, 0, len(executors)) + for _, e := range executors { + if targetID := m.retire(retireCtx, e); targetID != "" { + targetIDs = append(targetIDs, targetID) + } + } + + closeCtx, cancelClose := context.WithTimeout(ctx, playwrightExecutorKillGrace) + defer cancelClose() + for _, targetID := range targetIDs { + if err := m.tabs.Close(closeCtx, targetID); err != nil { + logger.FromContext(ctx).Warn("failed to close playwright executor tab", "target_id", targetID, "error", err) + } + } +} + +// playwrightExecutorsJSON converts executors to API objects, adding the +// current URL of each executor's tab when the browser can report it. +func (s *ApiService) playwrightExecutorsJSON(ctx context.Context, executors []playwrightExecutorInfo) []oapi.PlaywrightExecutor { + urls := make(map[string]string) + if slices.ContainsFunc(executors, func(e playwrightExecutorInfo) bool { return e.targetID != "" }) { + err := s.withCDPClient(ctx, func(ctx context.Context, c *cdpclient.Client) error { + raw, err := c.Send(ctx, "Target.getTargets", nil, "") + if err != nil { + return err + } + var targets struct { + TargetInfos []struct { + TargetID string `json:"targetId"` + URL string `json:"url"` + } `json:"targetInfos"` + } + if err := json.Unmarshal(raw, &targets); err != nil { + return err + } + for _, t := range targets.TargetInfos { + urls[t.TargetID] = t.URL + } + return nil + }) + if err != nil { + logger.FromContext(ctx).Warn("failed to read playwright executor tab URLs", "error", err) + } + } + + out := make([]oapi.PlaywrightExecutor, 0, len(executors)) + for _, e := range executors { + item := oapi.PlaywrightExecutor{ + Name: e.name, + Busy: e.busy, + CreatedAt: e.createdAt, + LastUsedAt: e.lastUsedAt, + } + if e.targetID != "" { + targetID := e.targetID + item.TargetId = &targetID + if url, ok := urls[e.targetID]; ok { + item.Url = &url + } + } + out = append(out, item) + } + return out +} + +func (s *ApiService) ListPlaywrightExecutors(ctx context.Context, _ oapi.ListPlaywrightExecutorsRequestObject) (oapi.ListPlaywrightExecutorsResponseObject, error) { + return oapi.ListPlaywrightExecutors200JSONResponse{ + Executors: s.playwrightExecutorsJSON(ctx, s.playwrightExecutors.List()), + }, nil +} + +func (s *ApiService) DeletePlaywrightExecutor(ctx context.Context, request oapi.DeletePlaywrightExecutorRequestObject) (oapi.DeletePlaywrightExecutorResponseObject, error) { + log := logger.FromContext(ctx) + + if !playwrightExecutorNamePattern.MatchString(request.Name) { + return oapi.DeletePlaywrightExecutor400JSONResponse{ + BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "executor name must match " + playwrightExecutorNamePattern.String(), + }, + }, nil + } + + // The default executor owns no tab, so close_tab has nothing to act on. + targetID, err := s.playwrightExecutors.Delete(ctx, request.Name) + if errors.Is(err, errPlaywrightExecutorNotFound) { + return oapi.DeletePlaywrightExecutor404JSONResponse{ + NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{ + Message: fmt.Sprintf("executor %q not found", request.Name), + }, + }, nil + } + + closeTab := request.Params.CloseTab == nil || *request.Params.CloseTab + if closeTab && targetID != "" { + if err := s.playwrightExecutors.tabs.Close(ctx, targetID); err != nil { + log.Error("failed to close playwright executor tab", "executor", request.Name, "error", err) + return oapi.DeletePlaywrightExecutor500JSONResponse{ + InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: fmt.Sprintf("executor deleted, but closing its tab %s failed: %v", targetID, err), + }, + }, nil + } + } + + return oapi.DeletePlaywrightExecutor204Response{}, nil +} diff --git a/server/cmd/api/api/playwright_executors_test.go b/server/cmd/api/api/playwright_executors_test.go new file mode 100644 index 00000000..6d1cee75 --- /dev/null +++ b/server/cmd/api/api/playwright_executors_test.go @@ -0,0 +1,683 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "slices" + "sync" + "testing" + "time" + + "github.com/kernel/kernel-images/server/lib/oapi" + "github.com/kernel/kernel-images/server/lib/recorder" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fakePlaywrightDaemon speaks the daemon's socket protocol. Code is a command: +// "sleep:" resolves after ms (reporting timed_out when it exceeds +// timeout_ms, like the real daemon), "hang" blocks the event loop and "exit" +// kills the process. FAKE_START_DELAY_MS delays listening on the socket. A tab +// exists while its file exists in FAKE_TABS_DIR (see +// fakeExecutorTabs); a missing tab is answered with tab_missing. Every result +// carries the process ID so tests can tell when a process is replaced. +const fakePlaywrightDaemon = ` +const fs = require('fs'); +const net = require('net'); +const path = require('path'); +const socketPath = process.env.PLAYWRIGHT_DAEMON_SOCKET; +const server = net.createServer(socket => { + let buffer = ''; + socket.on('data', data => { + buffer += data; + let i; + while ((i = buffer.indexOf('\n')) !== -1) { + const req = JSON.parse(buffer.slice(0, i)); + buffer = buffer.slice(i + 1); + if (!req.executor) { + req.target_id = 'active-tab'; + } else if (!fs.existsSync(path.join(process.env.FAKE_TABS_DIR, req.target_id))) { + socket.write(JSON.stringify({ id: req.id, success: false, error: 'tab closed', tab_missing: true }) + '\n'); + continue; + } + const respond = extra => socket.write(JSON.stringify({ + id: req.id, target_id: req.target_id, tab_created: !!req.tab_created, ...extra, + }) + '\n'); + if (req.code === 'hang') { for (;;) {} } + if (req.code === 'exit') { process.exit(1); } + const ms = Number(req.code.split(':')[1] || 0); + if (ms > req.timeout_ms) { + setTimeout(() => respond({ success: false, error: 'timed out', timed_out: true }), req.timeout_ms); + } else { + setTimeout(() => respond({ success: true, result: { pid: process.pid } }), ms); + } + } + }); +}); +setTimeout(() => server.listen(socketPath), Number(process.env.FAKE_START_DELAY_MS || 0)); +` + +// fakeExecutorTabs records each open tab as a file the fake daemon can see. +// When gate is set, Open signals opening and blocks until gate is closed. +type fakeExecutorTabs struct { + dir string + mu sync.Mutex + n int + gate chan struct{} + opening chan struct{} +} + +func (f *fakeExecutorTabs) Open(context.Context) (string, error) { + if f.gate != nil { + f.opening <- struct{}{} + <-f.gate + } + f.mu.Lock() + f.n++ + targetID := fmt.Sprintf("tab-%d", f.n) + f.mu.Unlock() + return targetID, os.WriteFile(filepath.Join(f.dir, targetID), nil, 0o644) +} + +func (f *fakeExecutorTabs) Close(_ context.Context, targetID string) error { + if err := os.Remove(filepath.Join(f.dir, targetID)); err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +func (f *fakeExecutorTabs) isOpen(targetID string) bool { + _, err := os.Stat(filepath.Join(f.dir, targetID)) + return err == nil +} + +func newTestPlaywrightExecutorManager(t *testing.T) (*playwrightExecutorManager, *fakeExecutorTabs) { + t.Helper() + if _, err := exec.LookPath("node"); err != nil { + t.Skipf("node not available: %v", err) + } + // Unix socket paths are limited to ~108 bytes, so avoid t.TempDir's long paths. + dir, err := os.MkdirTemp("", "pwx") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(dir) }) + script := filepath.Join(dir, "daemon.js") + require.NoError(t, os.WriteFile(script, []byte(fakePlaywrightDaemon), 0o644)) + tabs := &fakeExecutorTabs{dir: t.TempDir()} + t.Setenv("FAKE_TABS_DIR", tabs.dir) + + m := newPlaywrightExecutorManager(tabs) + m.script = script + m.socketDir = dir + m.responseGrace = 500 * time.Millisecond + t.Cleanup(func() { m.Shutdown(context.Background()) }) + return m, tabs +} + +// namedExecutors drops the default executor, which always exists. +func namedExecutors(executors []playwrightExecutorInfo) []playwrightExecutorInfo { + return slices.DeleteFunc(executors, func(e playwrightExecutorInfo) bool { return e.pinned }) +} + +func executorPID(t *testing.T, resp *playwrightDaemonResponse) int { + t.Helper() + require.True(t, resp.Success, "execution failed: %s", resp.Error) + raw, err := json.Marshal(resp.Result) + require.NoError(t, err) + var result struct { + PID int `json:"pid"` + } + require.NoError(t, json.Unmarshal(raw, &result)) + return result.PID +} + +func TestPlaywrightExecutorsRunConcurrently(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + + // Warm both processes so the measurement covers only execution. + for _, name := range []string{"a", "b"} { + _, err := m.Execute(ctx, name, "sleep:0", 10*time.Second) + require.NoError(t, err) + } + + start := time.Now() + var wg sync.WaitGroup + for _, name := range []string{"a", "b"} { + wg.Add(1) + go func() { + defer wg.Done() + resp, err := m.Execute(ctx, name, "sleep:2000", 10*time.Second) + assert.NoError(t, err) + assert.True(t, resp.Success) + }() + } + wg.Wait() + assert.Less(t, time.Since(start), 3500*time.Millisecond, "different executors should overlap") +} + +func TestPlaywrightExecutorSerializesCallsOnOneName(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + _, err := m.Execute(ctx, "a", "sleep:0", 10*time.Second) + require.NoError(t, err) + + start := time.Now() + var wg sync.WaitGroup + for range 2 { + wg.Add(1) + go func() { + defer wg.Done() + _, err := m.Execute(ctx, "a", "sleep:500", 10*time.Second) + assert.NoError(t, err) + }() + } + wg.Wait() + assert.GreaterOrEqual(t, time.Since(start), time.Second, "calls on one executor should not overlap") +} + +func TestPlaywrightExecutorKeepsItsTab(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + + first, err := m.Execute(ctx, "a", "sleep:0", 10*time.Second) + require.NoError(t, err) + require.NotEmpty(t, first.TargetID) + assert.True(t, first.TabCreated) + + second, err := m.Execute(ctx, "a", "sleep:0", 10*time.Second) + require.NoError(t, err) + assert.Equal(t, first.TargetID, second.TargetID) + assert.False(t, second.TabCreated) +} + +func TestPlaywrightExecutorTimeoutReplacesOnlyThatProcess(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + + first, err := m.Execute(ctx, "a", "sleep:0", 10*time.Second) + require.NoError(t, err) + firstPID := executorPID(t, first) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + resp, err := m.Execute(ctx, "b", "sleep:1500", 10*time.Second) + assert.NoError(t, err) + assert.True(t, resp.Success, "executor b should be unaffected by a's timeout") + }() + + timedOut, err := m.Execute(ctx, "a", "sleep:5000", time.Second) + require.NoError(t, err) + assert.True(t, timedOut.TimedOut) + wg.Wait() + + next, err := m.Execute(ctx, "a", "sleep:0", 10*time.Second) + require.NoError(t, err) + assert.NotEqual(t, firstPID, executorPID(t, next), "a timed-out executor should get a fresh process") + assert.Equal(t, first.TargetID, next.TargetID, "the fresh process should be handed the executor's tab") + assert.False(t, next.TabCreated) +} + +func TestPlaywrightExecutorUnresponsiveProcessIsKilled(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + + first, err := m.Execute(ctx, "a", "sleep:0", 10*time.Second) + require.NoError(t, err) + firstPID := executorPID(t, first) + + hung, err := m.Execute(ctx, "a", "hang", time.Second) + require.NoError(t, err) + assert.False(t, hung.Success) + assert.Equal(t, first.TargetID, hung.TargetID, "a call whose process stops answering still reports its tab") + + next, err := m.Execute(ctx, "a", "sleep:0", 10*time.Second) + require.NoError(t, err) + assert.NotEqual(t, firstPID, executorPID(t, next)) + assert.Equal(t, first.TargetID, next.TargetID) +} + +func TestPlaywrightExecutorProcessExitReportsTab(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + + first, err := m.Execute(ctx, "a", "sleep:0", 10*time.Second) + require.NoError(t, err) + + exited, err := m.Execute(ctx, "a", "exit", 10*time.Second) + require.NoError(t, err) + assert.False(t, exited.Success) + assert.Equal(t, first.TargetID, exited.TargetID) + assert.False(t, exited.TabCreated) + + next, err := m.Execute(ctx, "a", "sleep:0", 10*time.Second) + require.NoError(t, err) + assert.True(t, next.Success) + assert.Equal(t, first.TargetID, next.TargetID) +} + +func TestPlaywrightExecutorReopensClosedTab(t *testing.T) { + m, tabs := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + + first, err := m.Execute(ctx, "a", "sleep:0", 10*time.Second) + require.NoError(t, err) + require.NoError(t, tabs.Close(ctx, first.TargetID)) + + next, err := m.Execute(ctx, "a", "sleep:0", 10*time.Second) + require.NoError(t, err) + assert.True(t, next.Success) + assert.True(t, next.TabCreated) + assert.NotEqual(t, first.TargetID, next.TargetID) + assert.True(t, tabs.isOpen(next.TargetID)) +} + +func TestPlaywrightExecutorShutdownClosesTabs(t *testing.T) { + m, tabs := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + + a, err := m.Execute(ctx, "a", "sleep:0", 10*time.Second) + require.NoError(t, err) + b, err := m.Execute(ctx, "b", "sleep:0", 10*time.Second) + require.NoError(t, err) + + m.Shutdown(ctx) + assert.False(t, tabs.isOpen(a.TargetID)) + assert.False(t, tabs.isOpen(b.TargetID)) + assert.Empty(t, m.List()) +} + +func TestPlaywrightExecutorShutdownClosesTabOpenedDuringShutdown(t *testing.T) { + m, tabs := newTestPlaywrightExecutorManager(t) + tabs.gate = make(chan struct{}) + tabs.opening = make(chan struct{}, 1) + ctx := context.Background() + + done := make(chan error, 1) + go func() { + _, err := m.Execute(ctx, "a", "sleep:0", 10*time.Second) + done <- err + }() + <-tabs.opening + + shutdown := make(chan struct{}) + go func() { + m.Shutdown(ctx) + close(shutdown) + }() + select { + case <-shutdown: + t.Fatal("Shutdown returned before the in-flight call finished opening its tab") + case <-time.After(200 * time.Millisecond): + } + + close(tabs.gate) + <-shutdown + assert.ErrorIs(t, <-done, errPlaywrightExecutorsShutDown) + entries, err := os.ReadDir(tabs.dir) + require.NoError(t, err) + assert.Empty(t, entries, "the tab opened during shutdown should be closed") +} + +func TestPlaywrightExecutorLimit(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + + for i := range maxPlaywrightExecutors { + _, err := m.Execute(ctx, fmt.Sprintf("e%d", i), "sleep:0", 10*time.Second) + require.NoError(t, err) + } + + _, err := m.Execute(ctx, "one-too-many", "sleep:0", 10*time.Second) + var limitErr *playwrightExecutorLimitError + require.ErrorAs(t, err, &limitErr) + assert.Len(t, namedExecutors(limitErr.executors), maxPlaywrightExecutors) + assert.Contains(t, limitErr.Error(), "DELETE /playwright/executors/{name}") + + // Existing executors keep working at the limit, and deleting one frees a slot. + _, err = m.Execute(ctx, "e0", "sleep:0", 10*time.Second) + require.NoError(t, err) + _, err = m.Delete(ctx, "e0") + require.NoError(t, err) + _, err = m.Execute(ctx, "one-too-many", "sleep:0", 10*time.Second) + require.NoError(t, err) +} + +func TestPlaywrightExecutorDelete(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + + first, err := m.Execute(ctx, "a", "sleep:0", 10*time.Second) + require.NoError(t, err) + + errCh := make(chan error, 1) + go func() { + _, err := m.Execute(ctx, "a", "sleep:5000", 10*time.Second) + errCh <- err + }() + require.Eventually(t, func() bool { + executors := namedExecutors(m.List()) + return len(executors) == 1 && executors[0].busy + }, 2*time.Second, 10*time.Millisecond) + + targetID, err := m.Delete(ctx, "a") + require.NoError(t, err) + assert.Equal(t, first.TargetID, targetID) + + select { + case err := <-errCh: + assert.ErrorIs(t, err, errPlaywrightExecutorDeleted) + case <-time.After(2 * time.Second): + t.Fatal("in-flight call did not fail after delete") + } + assert.Empty(t, namedExecutors(m.List())) + + _, err = m.Delete(ctx, "a") + assert.ErrorIs(t, err, errPlaywrightExecutorNotFound) + + // The name is reusable and gets a fresh tab. + reused, err := m.Execute(ctx, "a", "sleep:0", 10*time.Second) + require.NoError(t, err) + assert.True(t, reused.TabCreated) + assert.NotEqual(t, first.TargetID, reused.TargetID) +} + +func TestPlaywrightExecutorDeleteReportsTabOpenedByInFlightCall(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + + done := make(chan *playwrightDaemonResponse, 1) + go func() { + resp, _ := m.Execute(ctx, "a", "sleep:5000", 10*time.Second) + done <- resp + }() + require.Eventually(t, func() bool { + executors := namedExecutors(m.List()) + return len(executors) == 1 && executors[0].targetID != "" + }, 3*time.Second, 10*time.Millisecond, "the tab should be known before the call finishes") + openedTab := namedExecutors(m.List())[0].targetID + + targetID, err := m.Delete(ctx, "a") + require.NoError(t, err) + assert.Equal(t, openedTab, targetID) + assert.Nil(t, <-done) +} + +func TestPlaywrightExecutorStartFailure(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + m.script = filepath.Join(m.socketDir, "missing.js") + svc, err := newSvc(t, recorder.NewFFmpegManager()) + require.NoError(t, err) + svc.playwrightExecutors = m + + name := "a" + resp, err := svc.ExecutePlaywrightCode(context.Background(), oapi.ExecutePlaywrightCodeRequestObject{ + Body: &oapi.ExecutePlaywrightCodeJSONRequestBody{Code: "sleep:0", Executor: &name}, + }) + require.NoError(t, err) + assert.IsType(t, oapi.ExecutePlaywrightCode500JSONResponse{}, resp) + assert.Empty(t, namedExecutors(m.List()), "an executor that never started should not take a slot") + + resp, err = svc.ExecutePlaywrightCode(context.Background(), oapi.ExecutePlaywrightCodeRequestObject{ + Body: &oapi.ExecutePlaywrightCodeJSONRequestBody{Code: "sleep:0"}, + }) + require.NoError(t, err) + assert.IsType(t, oapi.ExecutePlaywrightCode500JSONResponse{}, resp) + executors := m.List() + require.Len(t, executors, 1, "a failed start must not remove the default executor") + assert.Equal(t, defaultPlaywrightExecutor, executors[0].name) +} + +func TestPlaywrightExecutorHandlers(t *testing.T) { + m, tabs := newTestPlaywrightExecutorManager(t) + svc, err := newSvc(t, recorder.NewFFmpegManager()) + require.NoError(t, err) + svc.playwrightExecutors = m + ctx := context.Background() + + execute := func(name string) oapi.ExecutePlaywrightCodeResponseObject { + t.Helper() + resp, err := svc.ExecutePlaywrightCode(ctx, oapi.ExecutePlaywrightCodeRequestObject{ + Body: &oapi.ExecutePlaywrightCodeJSONRequestBody{Code: "sleep:0", Executor: &name}, + }) + require.NoError(t, err) + return resp + } + + ok, isOK := execute("a").(oapi.ExecutePlaywrightCode200JSONResponse) + require.True(t, isOK) + assert.True(t, ok.Success) + require.NotNil(t, ok.Tab) + assert.NotEmpty(t, ok.Tab.TargetId) + assert.True(t, ok.Tab.Created) + + _, isBadRequest := execute("not a valid name").(oapi.ExecutePlaywrightCode400JSONResponse) + assert.True(t, isBadRequest) + + for i := 1; i < maxPlaywrightExecutors; i++ { + execute(fmt.Sprintf("e%d", i)) + } + limit, isLimit := execute("one-too-many").(oapi.ExecutePlaywrightCode409JSONResponse) + require.True(t, isLimit) + assert.Len(t, limit.Executors, maxPlaywrightExecutors+1, "the default executor is listed but not counted") + assert.Equal(t, defaultPlaywrightExecutor, limit.Executors[0].Name) + assert.Equal(t, "a", limit.Executors[1].Name) + + list, err := svc.ListPlaywrightExecutors(ctx, oapi.ListPlaywrightExecutorsRequestObject{}) + require.NoError(t, err) + assert.Len(t, list.(oapi.ListPlaywrightExecutors200JSONResponse).Executors, maxPlaywrightExecutors+1) + + keepTab := false + deleted, err := svc.DeletePlaywrightExecutor(ctx, oapi.DeletePlaywrightExecutorRequestObject{ + Name: "a", + Params: oapi.DeletePlaywrightExecutorParams{CloseTab: &keepTab}, + }) + require.NoError(t, err) + assert.IsType(t, oapi.DeletePlaywrightExecutor204Response{}, deleted) + assert.True(t, tabs.isOpen(ok.Tab.TargetId), "close_tab=false should keep the tab") + + missing, err := svc.DeletePlaywrightExecutor(ctx, oapi.DeletePlaywrightExecutorRequestObject{Name: "a"}) + require.NoError(t, err) + assert.IsType(t, oapi.DeletePlaywrightExecutor404JSONResponse{}, missing) + + e1, ok1 := execute("e1").(oapi.ExecutePlaywrightCode200JSONResponse) + require.True(t, ok1) + deleted, err = svc.DeletePlaywrightExecutor(ctx, oapi.DeletePlaywrightExecutorRequestObject{Name: "e1"}) + require.NoError(t, err) + assert.IsType(t, oapi.DeletePlaywrightExecutor204Response{}, deleted) + assert.False(t, tabs.isOpen(e1.Tab.TargetId), "delete should close the tab by default") +} + +func TestPlaywrightExecutorListIncludesDefault(t *testing.T) { + svc, err := newSvc(t, recorder.NewFFmpegManager()) + require.NoError(t, err) + list, err := svc.ListPlaywrightExecutors(context.Background(), oapi.ListPlaywrightExecutorsRequestObject{}) + require.NoError(t, err) + executors := list.(oapi.ListPlaywrightExecutors200JSONResponse).Executors + require.Len(t, executors, 1) + assert.Equal(t, defaultPlaywrightExecutor, executors[0].Name) + assert.Nil(t, executors[0].TargetId, "the default executor owns no tab") +} + +func TestPlaywrightExecutorDefaultLane(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + svc, err := newSvc(t, recorder.NewFFmpegManager()) + require.NoError(t, err) + svc.playwrightExecutors = m + ctx := context.Background() + + execute := func(executor *string) oapi.ExecutePlaywrightCodeResponseObject { + t.Helper() + resp, err := svc.ExecutePlaywrightCode(ctx, oapi.ExecutePlaywrightCodeRequestObject{ + Body: &oapi.ExecutePlaywrightCodeJSONRequestBody{Code: "sleep:0", Executor: executor}, + }) + require.NoError(t, err) + return resp + } + + unnamed, ok := execute(nil).(oapi.ExecutePlaywrightCode200JSONResponse) + require.True(t, ok) + require.NotNil(t, unnamed.Tab) + assert.Equal(t, "active-tab", unnamed.Tab.TargetId, "the default executor binds the active tab") + assert.False(t, unnamed.Tab.Created) + + name := defaultPlaywrightExecutor + explicit, ok := execute(&name).(oapi.ExecutePlaywrightCode200JSONResponse) + require.True(t, ok) + assert.Equal(t, executorPID(t, &playwrightDaemonResponse{Success: true, Result: unnamed.Result}), + executorPID(t, &playwrightDaemonResponse{Success: true, Result: explicit.Result}), + `executor "default" should run on the same process as calls without an executor`) + + for i := range maxPlaywrightExecutors { + n := fmt.Sprintf("e%d", i) + _, ok := execute(&n).(oapi.ExecutePlaywrightCode200JSONResponse) + require.True(t, ok, "the default executor should not take a named slot") + } + _, ok = execute(nil).(oapi.ExecutePlaywrightCode200JSONResponse) + assert.True(t, ok, "the default executor works at the named executor limit") +} + +func TestPlaywrightExecutorDefaultRecoversFromBlockedProcess(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + + first, err := m.Execute(ctx, defaultPlaywrightExecutor, "sleep:0", 10*time.Second) + require.NoError(t, err) + + hung, err := m.Execute(ctx, defaultPlaywrightExecutor, "hang", time.Second) + require.NoError(t, err) + assert.False(t, hung.Success) + + for range 3 { + start := time.Now() + next, err := m.Execute(ctx, defaultPlaywrightExecutor, "sleep:0", 10*time.Second) + require.NoError(t, err) + assert.NotEqual(t, executorPID(t, first), executorPID(t, next)) + assert.Less(t, time.Since(start), 3*time.Second, "calls after a blocked process should not wait on it") + } +} + +func TestPlaywrightExecutorDeleteRestartsDefault(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + + first, err := m.Execute(ctx, defaultPlaywrightExecutor, "sleep:0", 10*time.Second) + require.NoError(t, err) + + errCh := make(chan error, 1) + go func() { + _, err := m.Execute(ctx, defaultPlaywrightExecutor, "sleep:5000", 10*time.Second) + errCh <- err + }() + require.Eventually(t, func() bool { return m.List()[0].busy }, 2*time.Second, 10*time.Millisecond) + + targetID, err := m.Delete(ctx, defaultPlaywrightExecutor) + require.NoError(t, err) + assert.Empty(t, targetID, "the default executor owns no tab to close") + select { + case err := <-errCh: + assert.ErrorIs(t, err, errPlaywrightExecutorRestarted) + case <-time.After(2 * time.Second): + t.Fatal("in-flight call did not fail after restart") + } + + executors := m.List() + require.Len(t, executors, 1, "restarting the default executor must not remove it") + assert.Equal(t, defaultPlaywrightExecutor, executors[0].name) + + next, err := m.Execute(ctx, defaultPlaywrightExecutor, "sleep:0", 10*time.Second) + require.NoError(t, err) + assert.NotEqual(t, executorPID(t, first), executorPID(t, next)) +} + +func TestPlaywrightExecutorRestartDefaultDuringStartup(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + t.Setenv("FAKE_START_DELAY_MS", "1000") + ctx := context.Background() + + errCh := make(chan error, 1) + go func() { + _, err := m.Execute(ctx, defaultPlaywrightExecutor, "sleep:0", 10*time.Second) + errCh <- err + }() + require.Eventually(t, func() bool { return m.List()[0].busy }, 2*time.Second, 10*time.Millisecond) + + start := time.Now() + _, err := m.Delete(ctx, defaultPlaywrightExecutor) + require.NoError(t, err) + assert.Less(t, time.Since(start), 500*time.Millisecond, "restart should not wait for the starting call") + assert.ErrorIs(t, <-errCh, errPlaywrightExecutorRestarted, "a call starting its process during a restart should fail") + + next, err := m.Execute(ctx, defaultPlaywrightExecutor, "sleep:0", 10*time.Second) + require.NoError(t, err) + assert.True(t, next.Success) +} + +func TestPlaywrightExecutorRestartDefaultDoesNotWaitForQueuedCalls(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + _, err := m.Execute(ctx, defaultPlaywrightExecutor, "sleep:0", 10*time.Second) + require.NoError(t, err) + + inFlight := make(chan error, 1) + go func() { + _, err := m.Execute(ctx, defaultPlaywrightExecutor, "sleep:3000", 10*time.Second) + inFlight <- err + }() + require.Eventually(t, func() bool { return m.List()[0].busy }, 2*time.Second, 10*time.Millisecond) + queued := make(chan *playwrightDaemonResponse, 1) + go func() { + resp, err := m.Execute(ctx, defaultPlaywrightExecutor, "sleep:1000", 10*time.Second) + assert.NoError(t, err) + queued <- resp + }() + time.Sleep(100 * time.Millisecond) + + start := time.Now() + _, err = m.Delete(ctx, defaultPlaywrightExecutor) + require.NoError(t, err) + assert.Less(t, time.Since(start), 500*time.Millisecond, "restart should not wait for queued calls") + assert.ErrorIs(t, <-inFlight, errPlaywrightExecutorRestarted) + + resp := <-queued + require.NotNil(t, resp) + queuedPID := executorPID(t, resp) + after, err := m.Execute(ctx, defaultPlaywrightExecutor, "sleep:0", 10*time.Second) + require.NoError(t, err) + assert.Equal(t, queuedPID, executorPID(t, after), "the process started for the queued call should be kept") +} + +func TestPlaywrightExecutorDefaultKeepsProcessAfterTimeout(t *testing.T) { + m, _ := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + + first, err := m.Execute(ctx, defaultPlaywrightExecutor, "sleep:0", 10*time.Second) + require.NoError(t, err) + timedOut, err := m.Execute(ctx, defaultPlaywrightExecutor, "sleep:5000", time.Second) + require.NoError(t, err) + require.True(t, timedOut.TimedOut) + + next, err := m.Execute(ctx, defaultPlaywrightExecutor, "sleep:0", 10*time.Second) + require.NoError(t, err) + assert.Equal(t, executorPID(t, first), executorPID(t, next), "the default executor keeps its process after a reported timeout") +} + +func TestPlaywrightExecutorRejectsCallsAfterShutdown(t *testing.T) { + m, tabs := newTestPlaywrightExecutorManager(t) + ctx := context.Background() + m.Shutdown(ctx) + + for _, name := range []string{defaultPlaywrightExecutor, "a"} { + _, err := m.Execute(ctx, name, "sleep:0", 10*time.Second) + assert.ErrorIs(t, err, errPlaywrightExecutorsShutDown) + } + entries, err := os.ReadDir(tabs.dir) + require.NoError(t, err) + assert.Empty(t, entries, "no tab should be opened after shutdown") + assert.Empty(t, m.List()) +} diff --git a/server/e2e/e2e_playwright_test.go b/server/e2e/e2e_playwright_test.go index a3f2b53e..edd0418a 100644 --- a/server/e2e/e2e_playwright_test.go +++ b/server/e2e/e2e_playwright_test.go @@ -7,10 +7,12 @@ import ( "net/http" "os/exec" "strings" + "sync" "testing" "time" instanceoapi "github.com/kernel/kernel-images/server/lib/oapi" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -278,6 +280,9 @@ func TestPlaywrightExecuteAPI(t *testing.T) { t.Run("WebMCPScratchDocument", func(t *testing.T) { testWebMCPScratchDocument(t, ctx, c, client) }) + t.Run("Executors", func(t *testing.T) { + testPlaywrightExecutors(t, ctx, client) + }) } func TestPlaywrightExecuteTimeoutReturnsPromptlyAndRecovers(t *testing.T) { @@ -363,6 +368,31 @@ await page.evaluate(() => document.body.dataset.timeoutMutation = "late"); require.NotNil(t, lateRsp.JSON200.Result) require.Equal(t, "initial", lateRsp.JSON200.Result, "timed-out execution mutated the page after returning") + // A script that blocks the daemon's event loop cannot answer its own + // timeout. Later calls must still work instead of waiting on the blocked + // process until their socket deadlines. + t.Log("executing playwright code that blocks the event loop") + blockedReq := instanceoapi.ExecutePlaywrightCodeJSONRequestBody{ + Code: `while (true) {}`, + TimeoutSec: &timeoutSec, + } + blockedRsp, err := client.ExecutePlaywrightCodeWithResponse(ctx, blockedReq) + require.NoError(t, err) + require.Equal(t, http.StatusOK, blockedRsp.StatusCode()) + require.NotNil(t, blockedRsp.JSON200) + require.False(t, blockedRsp.JSON200.Success, "expected success=false for a blocked event loop") + + for i := range 3 { + start := time.Now() + rsp, err := client.ExecutePlaywrightCodeWithResponse(ctx, recoveryReq) + require.NoError(t, err) + require.Equal(t, http.StatusOK, rsp.StatusCode()) + require.NotNil(t, rsp.JSON200) + require.True(t, rsp.JSON200.Success, "call %d after a blocked event loop failed: %s", i+1, rsp.Body) + require.Equal(t, "initial", rsp.JSON200.Result) + require.Less(t, time.Since(start), 10*time.Second, "call %d after a blocked event loop should not wait on the blocked process", i+1) + } + t.Log("playwright timeout regression test passed") } @@ -483,5 +513,126 @@ func TestPlaywrightDaemonRecovery(t *testing.T) { // Step 4: Execute playwright code again - daemon should recover waitForExecution("execution after chromium restart (daemon should recover)", 30*time.Second) + // Step 5: Restart the API. The daemon must exit with it rather than keep + // serving the socket, so the first call to the new API succeeds. + t.Log("restarting the API via supervisorctl") + code, output, err := c.Exec(ctx, []string{"supervisorctl", "-c", "/etc/supervisor/supervisord.conf", "restart", "kernel-images-api"}) + require.NoError(t, err) + require.Equal(t, 0, code, "supervisorctl restart kernel-images-api: %s", output) + require.NoError(t, c.WaitReady(ctx), "api not ready after restart") + executeAndVerify("first execution after API restart") + + code, output, err = c.Exec(ctx, []string{"pgrep", "-fc", "/usr/local/lib/playwright-daemon.js"}) + require.NoError(t, err) + require.Equal(t, 0, code, "pgrep: %s", output) + require.Equal(t, "1", strings.TrimSpace(output), "the previous API's daemon should have exited") + t.Log("playwright daemon recovery test passed") } + +func testPlaywrightExecutors(t *testing.T, ctx context.Context, client *instanceoapi.ClientWithResponses) { + execute := func(executor, code string) *instanceoapi.ExecutePlaywrightResult { + t.Helper() + body := instanceoapi.ExecutePlaywrightCodeJSONRequestBody{Code: code} + if executor != "" { + body.Executor = &executor + } + rsp, err := client.ExecutePlaywrightCodeWithResponse(ctx, body) + require.NoError(t, err) + require.Equal(t, http.StatusOK, rsp.StatusCode(), "body=%s", string(rsp.Body)) + require.NotNil(t, rsp.JSON200) + return rsp.JSON200 + } + + // Executor tabs open in the default context. Start from a single window + // whose active tab is in that context, since earlier subtests leave other + // contexts and windows behind. + activeBefore := execute("", ` + await Promise.all(browser.contexts().slice(1).map(context => context.close())); + const active = await browser.contexts()[0].newPage(); + await active.goto('data:text/html,active-tab'); + await Promise.all(browser.contexts()[0].pages().filter(p => p !== active).map(p => p.close())); + await active.bringToFront(); + return active.url(); + `) + require.True(t, activeBefore.Success, "error=%v", activeBefore.Error) + activeTab := execute("", `return page.url();`) + require.Equal(t, "data:text/html,active-tab", activeTab.Result) + require.NotNil(t, activeTab.Tab, "calls without an executor should report the tab they bound") + require.False(t, activeTab.Tab.Created) + + t.Log("verifying the first call opens the executor's tab and later calls reuse it") + first := execute("a", `await page.goto('data:text/html,executor-a'); return page.url();`) + require.True(t, first.Success, "error=%v", first.Error) + require.NotNil(t, first.Tab) + require.True(t, first.Tab.Created) + execute("b", `await page.goto('data:text/html,executor-b'); return page.url();`) + + again := execute("a", `return page.url();`) + require.True(t, again.Success) + require.Equal(t, "data:text/html,executor-a", again.Result) + require.Equal(t, first.Tab.TargetId, again.Tab.TargetId) + require.False(t, again.Tab.Created) + + t.Log("verifying different executors run concurrently") + start := time.Now() + var wg sync.WaitGroup + for _, name := range []string{"a", "b"} { + wg.Add(1) + go func() { + defer wg.Done() + result := execute(name, `await page.waitForTimeout(3000); return page.url();`) + assert.True(t, result.Success, "executor %s error=%v", name, result.Error) + assert.Equal(t, "data:text/html,executor-"+name, result.Result) + }() + } + wg.Wait() + require.Less(t, time.Since(start), 5*time.Second, "two 3s calls on different executors should overlap") + + t.Log("verifying calls without an executor still bind the active tab") + unnamed := execute("", `return page.url();`) + require.True(t, unnamed.Success) + require.Equal(t, "data:text/html,active-tab", unnamed.Result, "opening executor tabs should not change the active tab") + require.NotNil(t, unnamed.Tab) + require.Equal(t, activeTab.Tab.TargetId, unnamed.Tab.TargetId) + + t.Log("verifying a closed executor tab is reopened and reported") + execute("a", `await page.close();`) + reopened := execute("a", `return page.url();`) + require.True(t, reopened.Success) + require.True(t, reopened.Tab.Created) + require.NotEqual(t, first.Tab.TargetId, reopened.Tab.TargetId) + + t.Log("verifying list and delete") + list, err := client.ListPlaywrightExecutorsWithResponse(ctx) + require.NoError(t, err) + require.Equal(t, http.StatusOK, list.StatusCode()) + require.Len(t, list.JSON200.Executors, 3) + require.Equal(t, "default", list.JSON200.Executors[0].Name, "the default executor is listed first") + require.Nil(t, list.JSON200.Executors[0].TargetId, "the default executor owns no tab") + + del, err := client.DeletePlaywrightExecutorWithResponse(ctx, "b", nil) + require.NoError(t, err) + require.Equal(t, http.StatusNoContent, del.StatusCode(), "body=%s", string(del.Body)) + urls := execute("", `return browser.contexts().flatMap(c => c.pages()).map(p => p.url());`) + require.NotContains(t, urls.Result, "data:text/html,executor-b", "deleting an executor should close its tab") + + missing, err := client.DeletePlaywrightExecutorWithResponse(ctx, "b", nil) + require.NoError(t, err) + require.Equal(t, http.StatusNotFound, missing.StatusCode()) + + del, err = client.DeletePlaywrightExecutorWithResponse(ctx, "a", nil) + require.NoError(t, err) + require.Equal(t, http.StatusNoContent, del.StatusCode(), "body=%s", string(del.Body)) + + t.Log("verifying deleting the default executor restarts it and keeps the active tab") + del, err = client.DeletePlaywrightExecutorWithResponse(ctx, "default", nil) + require.NoError(t, err) + require.Equal(t, http.StatusNoContent, del.StatusCode(), "body=%s", string(del.Body)) + restarted := execute("", `return page.url();`) + require.True(t, restarted.Success, "error=%v", restarted.Error) + require.Equal(t, "data:text/html,active-tab", restarted.Result) + require.Equal(t, activeTab.Tab.TargetId, restarted.Tab.TargetId) + explicitDefault := execute("default", `return page.url();`) + require.Equal(t, "data:text/html,active-tab", explicitDefault.Result, `executor "default" should bind the active tab`) +} diff --git a/server/lib/events/category_gen.go b/server/lib/events/category_gen.go index 5e276ca1..ba24d23b 100644 --- a/server/lib/events/category_gen.go +++ b/server/lib/events/category_gen.go @@ -58,6 +58,7 @@ var categoryByOperationID = map[string]oapi.TelemetryEventCategory{ "CreateDirectory": oapi.TelemetryEventCategory("platform"), "DeleteDirectory": oapi.TelemetryEventCategory("platform"), "DeleteFile": oapi.TelemetryEventCategory("platform"), + "DeletePlaywrightExecutor": oapi.TelemetryEventCategory("control"), "DeleteRecording": oapi.TelemetryEventCategory("platform"), "DisableScaleToZero": oapi.TelemetryEventCategory("platform"), "DownloadDirZip": oapi.TelemetryEventCategory("platform"), @@ -74,6 +75,7 @@ var categoryByOperationID = map[string]oapi.TelemetryEventCategory{ "InvokeWebMCPTool": oapi.TelemetryEventCategory("control"), "ListCustomWebMCPTools": oapi.TelemetryEventCategory("control"), "ListFiles": oapi.TelemetryEventCategory("platform"), + "ListPlaywrightExecutors": oapi.TelemetryEventCategory("control"), "ListRecorders": oapi.TelemetryEventCategory("platform"), "LogsStream": oapi.TelemetryEventCategory("platform"), "MarkRecording": oapi.TelemetryEventCategory("platform"), diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index e71c32e9..8dc71146 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -6365,6 +6365,11 @@ type ExecutePlaywrightRequest struct { // Example: "await page.goto('https://example.com'); return await page.title();" Code string `json:"code"` + // Executor Name of a Playwright executor. Calls with the same name run in the same executor; the + // first call with a new name creates it. 'default' names the executor that runs calls + // without a name. + Executor *PlaywrightExecutorName `json:"executor,omitempty"` + // TimeoutSec Maximum execution time in seconds. Default is 60. TimeoutSec *int `json:"timeout_sec,omitempty"` } @@ -6385,6 +6390,9 @@ type ExecutePlaywrightResult struct { // Success Whether the code executed successfully Success bool `json:"success"` + + // Tab The tab 'page' was bound to for this call. Absent if the call failed before binding a tab. + Tab *PlaywrightTab `json:"tab,omitempty"` } // ExtensionUploadMultipart defines model for ExtensionUploadMultipart. @@ -6536,6 +6544,55 @@ type PatchDisplayRequest struct { // PatchDisplayRequestRefreshRate Display refresh rate in Hz. If omitted, uses the highest available rate for the resolution. type PatchDisplayRequestRefreshRate int +// PlaywrightExecutor A named Playwright executor +type PlaywrightExecutor struct { + // Busy Whether a call is running on the executor + Busy bool `json:"busy"` + CreatedAt time.Time `json:"created_at"` + + // LastUsedAt When the most recent call on the executor started + LastUsedAt time.Time `json:"last_used_at"` + + // Name Name of a Playwright executor. Calls with the same name run in the same executor; the + // first call with a new name creates it. 'default' names the executor that runs calls + // without a name. + Name PlaywrightExecutorName `json:"name"` + + // TargetId CDP page target ID of the executor's tab, once it has one + TargetId *string `json:"target_id,omitempty"` + + // Url Current URL of the executor's tab, when it is open + Url *string `json:"url,omitempty"` +} + +// PlaywrightExecutorLimitError defines model for PlaywrightExecutorLimitError. +type PlaywrightExecutorLimitError struct { + // Executors The default executor and the named executors + Executors []PlaywrightExecutor `json:"executors"` + Message string `json:"message"` +} + +// PlaywrightExecutorList defines model for PlaywrightExecutorList. +type PlaywrightExecutorList struct { + Executors []PlaywrightExecutor `json:"executors"` +} + +// PlaywrightExecutorName Name of a Playwright executor. Calls with the same name run in the same executor; the +// first call with a new name creates it. 'default' names the executor that runs calls +// without a name. +type PlaywrightExecutorName = string + +// PlaywrightTab The tab 'page' was bound to for this call. Absent if the call failed before binding a tab. +type PlaywrightTab struct { + // Created Whether this call opened the tab. For an executor this happens on its first call and + // after its previous tab was closed. Without an executor it happens only when the browser + // had no open page. + Created bool `json:"created"` + + // TargetId CDP page target ID of the tab + TargetId string `json:"target_id"` +} + // PressKeyRequest defines model for PressKeyRequest. type PressKeyRequest struct { // Duration Duration to hold the keys down in milliseconds. If omitted or 0, keys are tapped. @@ -7176,6 +7233,12 @@ type LogsStreamParams struct { // LogsStreamParamsSource defines parameters for LogsStream. type LogsStreamParamsSource string +// DeletePlaywrightExecutorParams defines parameters for DeletePlaywrightExecutor. +type DeletePlaywrightExecutorParams struct { + // CloseTab Close the executor's tab. Defaults to true. + CloseTab *bool `form:"close_tab,omitempty" json:"close_tab,omitempty"` +} + // DownloadRecordingParams defines parameters for DownloadRecording. type DownloadRecordingParams struct { // Id Identifier of the recording session to download, as passed to /recording/start. When omitted, the default recording session is downloaded. @@ -9914,6 +9977,12 @@ type ClientInterface interface { ExecutePlaywrightCode(ctx context.Context, body ExecutePlaywrightCodeJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ListPlaywrightExecutors request + ListPlaywrightExecutors(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeletePlaywrightExecutor request + DeletePlaywrightExecutor(ctx context.Context, name PlaywrightExecutorName, params *DeletePlaywrightExecutorParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // ProcessExecWithBody request with any body ProcessExecWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -10707,6 +10776,30 @@ func (c *Client) ExecutePlaywrightCode(ctx context.Context, body ExecutePlaywrig return c.Client.Do(req) } +func (c *Client) ListPlaywrightExecutors(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListPlaywrightExecutorsRequest(c.Server) + 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) DeletePlaywrightExecutor(ctx context.Context, name PlaywrightExecutorName, params *DeletePlaywrightExecutorParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeletePlaywrightExecutorRequest(c.Server, name, params) + 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) ProcessExecWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewProcessExecRequestWithBody(c.Server, contentType, body) if err != nil { @@ -12714,6 +12807,89 @@ func NewExecutePlaywrightCodeRequestWithBody(server string, contentType string, return req, nil } +// NewListPlaywrightExecutorsRequest generates requests for ListPlaywrightExecutors +func NewListPlaywrightExecutorsRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/playwright/executors") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewDeletePlaywrightExecutorRequest generates requests for DeletePlaywrightExecutor +func NewDeletePlaywrightExecutorRequest(server string, name PlaywrightExecutorName, params *DeletePlaywrightExecutorParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "name", name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/playwright/executors/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.CloseTab != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "close_tab", *params.CloseTab, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewProcessExecRequest calls the generic ProcessExec builder with application/json body func NewProcessExecRequest(server string, body ProcessExecJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -13928,6 +14104,12 @@ type ClientWithResponsesInterface interface { ExecutePlaywrightCodeWithResponse(ctx context.Context, body ExecutePlaywrightCodeJSONRequestBody, reqEditors ...RequestEditorFn) (*ExecutePlaywrightCodeResponse, error) + // ListPlaywrightExecutorsWithResponse request + ListPlaywrightExecutorsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListPlaywrightExecutorsResponse, error) + + // DeletePlaywrightExecutorWithResponse request + DeletePlaywrightExecutorWithResponse(ctx context.Context, name PlaywrightExecutorName, params *DeletePlaywrightExecutorParams, reqEditors ...RequestEditorFn) (*DeletePlaywrightExecutorResponse, error) + // ProcessExecWithBodyWithResponse request with any body ProcessExecWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ProcessExecResponse, error) @@ -14895,6 +15077,7 @@ type ExecutePlaywrightCodeResponse struct { HTTPResponse *http.Response JSON200 *ExecutePlaywrightResult JSON400 *BadRequestError + JSON409 *PlaywrightExecutorLimitError JSON500 *InternalError } @@ -14914,6 +15097,53 @@ func (r ExecutePlaywrightCodeResponse) StatusCode() int { return 0 } +type ListPlaywrightExecutorsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *PlaywrightExecutorList + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r ListPlaywrightExecutorsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ListPlaywrightExecutorsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DeletePlaywrightExecutorResponse struct { + Body []byte + HTTPResponse *http.Response + JSON400 *BadRequestError + JSON404 *NotFoundError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r DeletePlaywrightExecutorResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeletePlaywrightExecutorResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type ProcessExecResponse struct { Body []byte HTTPResponse *http.Response @@ -16033,6 +16263,24 @@ func (c *ClientWithResponses) ExecutePlaywrightCodeWithResponse(ctx context.Cont return ParseExecutePlaywrightCodeResponse(rsp) } +// ListPlaywrightExecutorsWithResponse request returning *ListPlaywrightExecutorsResponse +func (c *ClientWithResponses) ListPlaywrightExecutorsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListPlaywrightExecutorsResponse, error) { + rsp, err := c.ListPlaywrightExecutors(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseListPlaywrightExecutorsResponse(rsp) +} + +// DeletePlaywrightExecutorWithResponse request returning *DeletePlaywrightExecutorResponse +func (c *ClientWithResponses) DeletePlaywrightExecutorWithResponse(ctx context.Context, name PlaywrightExecutorName, params *DeletePlaywrightExecutorParams, reqEditors ...RequestEditorFn) (*DeletePlaywrightExecutorResponse, error) { + rsp, err := c.DeletePlaywrightExecutor(ctx, name, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeletePlaywrightExecutorResponse(rsp) +} + // ProcessExecWithBodyWithResponse request with arbitrary body returning *ProcessExecResponse func (c *ClientWithResponses) ProcessExecWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ProcessExecResponse, error) { rsp, err := c.ProcessExecWithBody(ctx, contentType, body, reqEditors...) @@ -17757,6 +18005,86 @@ func ParseExecutePlaywrightCodeResponse(rsp *http.Response) (*ExecutePlaywrightC } response.JSON400 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest PlaywrightExecutorLimitError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &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 +} + +// ParseListPlaywrightExecutorsResponse parses an HTTP response from a ListPlaywrightExecutorsWithResponse call +func ParseListPlaywrightExecutorsResponse(rsp *http.Response) (*ListPlaywrightExecutorsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ListPlaywrightExecutorsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest PlaywrightExecutorList + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &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 +} + +// ParseDeletePlaywrightExecutorResponse parses an HTTP response from a DeletePlaywrightExecutorWithResponse call +func ParseDeletePlaywrightExecutorResponse(rsp *http.Response) (*DeletePlaywrightExecutorResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeletePlaywrightExecutorResponse{ + 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 == 404: + var dest NotFoundError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -18933,6 +19261,12 @@ type ServerInterface interface { // Execute Playwright/TypeScript code against the browser // (POST /playwright/execute) ExecutePlaywrightCode(w http.ResponseWriter, r *http.Request) + // List Playwright executors + // (GET /playwright/executors) + ListPlaywrightExecutors(w http.ResponseWriter, r *http.Request) + // Delete a Playwright executor + // (DELETE /playwright/executors/{name}) + DeletePlaywrightExecutor(w http.ResponseWriter, r *http.Request, name PlaywrightExecutorName, params DeletePlaywrightExecutorParams) // Execute a command synchronously // (POST /process/exec) ProcessExec(w http.ResponseWriter, r *http.Request) @@ -19239,6 +19573,18 @@ func (_ Unimplemented) ExecutePlaywrightCode(w http.ResponseWriter, r *http.Requ w.WriteHeader(http.StatusNotImplemented) } +// List Playwright executors +// (GET /playwright/executors) +func (_ Unimplemented) ListPlaywrightExecutors(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Delete a Playwright executor +// (DELETE /playwright/executors/{name}) +func (_ Unimplemented) DeletePlaywrightExecutor(w http.ResponseWriter, r *http.Request, name PlaywrightExecutorName, params DeletePlaywrightExecutorParams) { + w.WriteHeader(http.StatusNotImplemented) +} + // Execute a command synchronously // (POST /process/exec) func (_ Unimplemented) ProcessExec(w http.ResponseWriter, r *http.Request) { @@ -20137,6 +20483,56 @@ func (siw *ServerInterfaceWrapper) ExecutePlaywrightCode(w http.ResponseWriter, handler.ServeHTTP(w, r) } +// ListPlaywrightExecutors operation middleware +func (siw *ServerInterfaceWrapper) ListPlaywrightExecutors(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListPlaywrightExecutors(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// DeletePlaywrightExecutor operation middleware +func (siw *ServerInterfaceWrapper) DeletePlaywrightExecutor(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "name" ------------- + var name PlaywrightExecutorName + + err = runtime.BindStyledParameterWithOptions("simple", "name", chi.URLParam(r, "name"), &name, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "name", Err: err}) + return + } + + // Parameter object where we will unmarshal all parameters from the context + var params DeletePlaywrightExecutorParams + + // ------------- Optional query parameter "close_tab" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "close_tab", r.URL.Query(), ¶ms.CloseTab, runtime.BindQueryParameterOptions{Type: "boolean", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "close_tab", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeletePlaywrightExecutor(w, r, name, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ProcessExec operation middleware func (siw *ServerInterfaceWrapper) ProcessExec(w http.ResponseWriter, r *http.Request) { @@ -20851,6 +21247,12 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/playwright/execute", wrapper.ExecutePlaywrightCode) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/playwright/executors", wrapper.ListPlaywrightExecutors) + }) + r.Group(func(r chi.Router) { + r.Delete(options.BaseURL+"/playwright/executors/{name}", wrapper.DeletePlaywrightExecutor) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/process/exec", wrapper.ProcessExec) }) @@ -22454,6 +22856,15 @@ func (response ExecutePlaywrightCode400JSONResponse) VisitExecutePlaywrightCodeR return json.NewEncoder(w).Encode(response) } +type ExecutePlaywrightCode409JSONResponse PlaywrightExecutorLimitError + +func (response ExecutePlaywrightCode409JSONResponse) VisitExecutePlaywrightCodeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + type ExecutePlaywrightCode500JSONResponse struct{ InternalErrorJSONResponse } func (response ExecutePlaywrightCode500JSONResponse) VisitExecutePlaywrightCodeResponse(w http.ResponseWriter) error { @@ -22463,6 +22874,75 @@ func (response ExecutePlaywrightCode500JSONResponse) VisitExecutePlaywrightCodeR return json.NewEncoder(w).Encode(response) } +type ListPlaywrightExecutorsRequestObject struct { +} + +type ListPlaywrightExecutorsResponseObject interface { + VisitListPlaywrightExecutorsResponse(w http.ResponseWriter) error +} + +type ListPlaywrightExecutors200JSONResponse PlaywrightExecutorList + +func (response ListPlaywrightExecutors200JSONResponse) VisitListPlaywrightExecutorsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ListPlaywrightExecutors500JSONResponse struct{ InternalErrorJSONResponse } + +func (response ListPlaywrightExecutors500JSONResponse) VisitListPlaywrightExecutorsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type DeletePlaywrightExecutorRequestObject struct { + Name PlaywrightExecutorName `json:"name"` + Params DeletePlaywrightExecutorParams +} + +type DeletePlaywrightExecutorResponseObject interface { + VisitDeletePlaywrightExecutorResponse(w http.ResponseWriter) error +} + +type DeletePlaywrightExecutor204Response struct { +} + +func (response DeletePlaywrightExecutor204Response) VisitDeletePlaywrightExecutorResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type DeletePlaywrightExecutor400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response DeletePlaywrightExecutor400JSONResponse) VisitDeletePlaywrightExecutorResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type DeletePlaywrightExecutor404JSONResponse struct{ NotFoundErrorJSONResponse } + +func (response DeletePlaywrightExecutor404JSONResponse) VisitDeletePlaywrightExecutorResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type DeletePlaywrightExecutor500JSONResponse struct{ InternalErrorJSONResponse } + +func (response DeletePlaywrightExecutor500JSONResponse) VisitDeletePlaywrightExecutorResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type ProcessExecRequestObject struct { Body *ProcessExecJSONRequestBody } @@ -23697,6 +24177,12 @@ type StrictServerInterface interface { // Execute Playwright/TypeScript code against the browser // (POST /playwright/execute) ExecutePlaywrightCode(ctx context.Context, request ExecutePlaywrightCodeRequestObject) (ExecutePlaywrightCodeResponseObject, error) + // List Playwright executors + // (GET /playwright/executors) + ListPlaywrightExecutors(ctx context.Context, request ListPlaywrightExecutorsRequestObject) (ListPlaywrightExecutorsResponseObject, error) + // Delete a Playwright executor + // (DELETE /playwright/executors/{name}) + DeletePlaywrightExecutor(ctx context.Context, request DeletePlaywrightExecutorRequestObject) (DeletePlaywrightExecutorResponseObject, error) // Execute a command synchronously // (POST /process/exec) ProcessExec(ctx context.Context, request ProcessExecRequestObject) (ProcessExecResponseObject, error) @@ -24901,6 +25387,57 @@ func (sh *strictHandler) ExecutePlaywrightCode(w http.ResponseWriter, r *http.Re } } +// ListPlaywrightExecutors operation middleware +func (sh *strictHandler) ListPlaywrightExecutors(w http.ResponseWriter, r *http.Request) { + var request ListPlaywrightExecutorsRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ListPlaywrightExecutors(ctx, request.(ListPlaywrightExecutorsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ListPlaywrightExecutors") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ListPlaywrightExecutorsResponseObject); ok { + if err := validResponse.VisitListPlaywrightExecutorsResponse(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)) + } +} + +// DeletePlaywrightExecutor operation middleware +func (sh *strictHandler) DeletePlaywrightExecutor(w http.ResponseWriter, r *http.Request, name PlaywrightExecutorName, params DeletePlaywrightExecutorParams) { + var request DeletePlaywrightExecutorRequestObject + + request.Name = name + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.DeletePlaywrightExecutor(ctx, request.(DeletePlaywrightExecutorRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "DeletePlaywrightExecutor") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(DeletePlaywrightExecutorResponseObject); ok { + if err := validResponse.VisitDeletePlaywrightExecutorResponse(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)) + } +} + // ProcessExec operation middleware func (sh *strictHandler) ProcessExec(w http.ResponseWriter, r *http.Request) { var request ProcessExecRequestObject @@ -25663,679 +26200,697 @@ func (sh *strictHandler) GetWebMCPTools(w http.ResponseWriter, r *http.Request, // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "l5N2rEPy8eP/fwDJP5bXuskDAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 7cc3e8e7..f57e6df7 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1548,11 +1548,32 @@ paths: statusCode, code, invocationId, and body fields. The result of the code execution is returned in the response. - 'page' is bound to an active tab reported by Chrome. In single-window sessions, this is - the foreground tab. When multiple browser windows are open, Chrome reports one active tab - per window and the selected window is unspecified. 'context' is the BrowserContext that owns - the selected page. If active-tab resolution races with a target change, execution falls back - to an existing page. Use 'browser.contexts()' to select a context or page explicitly. + Every call runs in an executor: a dedicated Node.js process with its own browser + connection. Calls on different executors run concurrently; calls on the same executor run + one at a time. A timeout, crash, or blocked event loop in one executor does not affect + other executors. After a crash or a blocked event loop, the next call on that executor + starts a fresh process; named executors also start a fresh process after a timeout. + + Calls without 'executor' run in the executor named 'default', which always exists and is + the same as passing 'executor: "default"'. In the default executor, 'page' is bound to an + active tab reported by Chrome. In single-window sessions, this is the foreground tab. When + multiple browser windows are open, Chrome reports one active tab per window and the + selected window is unspecified. 'context' is the BrowserContext that owns the selected + page. If active-tab resolution races with a target change, execution falls back to an + existing page. Use 'browser.contexts()' to select a context or page explicitly. + + Pass any other name to run the call in a named executor. The first call with a new name + creates it. Each named executor owns a tab. Its first call opens a new background tab in the default browser + context, and 'page' is bound to that tab on every later call while it stays open. Opening it + does not change the active tab of an existing window; if the default context has no window, + Chrome opens one. If the tab is closed, the next call opens a new + one and reports 'tab.created: true'. Executor code can still reach other tabs through + 'context' and 'browser'; ownership only decides what 'page' is bound to. + + A browser can have at most 8 named executors; the default executor does not count. A call + that would create another returns 409 with the executors; delete one with + DELETE /playwright/executors/{name}. Named executors are not removed automatically while + the API runs; when it shuts down, they are removed and their tabs closed. operationId: executePlaywrightCode x-telemetry-category: control requestBody: @@ -1570,6 +1591,62 @@ paths: $ref: "#/components/schemas/ExecutePlaywrightResult" "400": $ref: "#/components/responses/BadRequestError" + "409": + description: The call would create an executor beyond the per-browser limit + content: + application/json: + schema: + $ref: "#/components/schemas/PlaywrightExecutorLimitError" + "500": + $ref: "#/components/responses/InternalError" + /playwright/executors: + get: + summary: List Playwright executors + description: Lists the default executor first, then the named executors created by POST /playwright/execute. + operationId: listPlaywrightExecutors + x-telemetry-category: control + responses: + "200": + description: Open executors + content: + application/json: + schema: + $ref: "#/components/schemas/PlaywrightExecutorList" + "500": + $ref: "#/components/responses/InternalError" + /playwright/executors/{name}: + delete: + summary: Delete a Playwright executor + description: | + Stops the executor's process. A call running on it fails with an error saying the + executor was deleted. By default the executor's tab is closed too. The name can be + reused; the next call with it creates a new executor and tab. + + The default executor is restarted instead of removed: its process is stopped, a call + running on it fails with an error saying the executor was restarted, and queued and later + calls run on a new process. It owns no tab, so 'close_tab' has no effect on it. + operationId: deletePlaywrightExecutor + x-telemetry-category: control + parameters: + - name: name + in: path + required: true + schema: + $ref: "#/components/schemas/PlaywrightExecutorName" + - name: close_tab + in: query + required: false + description: Close the executor's tab. Defaults to true. + schema: + type: boolean + default: true + responses: + "204": + description: Executor deleted + "400": + $ref: "#/components/responses/BadRequestError" + "404": + $ref: "#/components/responses/NotFoundError" "500": $ref: "#/components/responses/InternalError" /repl: @@ -7423,6 +7500,60 @@ components: default: 60 minimum: 1 maximum: 300 + executor: + $ref: "#/components/schemas/PlaywrightExecutorName" + additionalProperties: false + PlaywrightExecutorName: + type: string + description: | + Name of a Playwright executor. Calls with the same name run in the same executor; the + first call with a new name creates it. 'default' names the executor that runs calls + without a name. + pattern: "^[A-Za-z0-9_-]{1,64}$" + PlaywrightExecutor: + type: object + description: A named Playwright executor + required: [name, busy, created_at, last_used_at] + properties: + name: + $ref: "#/components/schemas/PlaywrightExecutorName" + busy: + type: boolean + description: Whether a call is running on the executor + created_at: + type: string + format: date-time + last_used_at: + type: string + format: date-time + description: When the most recent call on the executor started + target_id: + type: string + description: CDP page target ID of the executor's tab, once it has one + url: + type: string + description: Current URL of the executor's tab, when it is open + additionalProperties: false + PlaywrightExecutorList: + type: object + required: [executors] + properties: + executors: + type: array + items: + $ref: "#/components/schemas/PlaywrightExecutor" + additionalProperties: false + PlaywrightExecutorLimitError: + type: object + required: [message, executors] + properties: + message: + type: string + executors: + type: array + description: The default executor and the named executors + items: + $ref: "#/components/schemas/PlaywrightExecutor" additionalProperties: false ExecutePlaywrightResult: type: object @@ -7443,6 +7574,24 @@ components: stderr: type: string description: Standard error from the execution + tab: + $ref: "#/components/schemas/PlaywrightTab" + additionalProperties: false + PlaywrightTab: + type: object + description: | + The tab 'page' was bound to for this call. Absent if the call failed before binding a tab. + required: [target_id, created] + properties: + target_id: + type: string + description: CDP page target ID of the tab + created: + type: boolean + description: | + Whether this call opened the tab. For an executor this happens on its first call and + after its previous tab was closed. Without an executor it happens only when the browser + had no open page. additionalProperties: false BrowserReplRequest: type: object diff --git a/server/runtime/playwright-daemon.ts b/server/runtime/playwright-daemon.ts index 9dfca843..ee0f4082 100644 --- a/server/runtime/playwright-daemon.ts +++ b/server/runtime/playwright-daemon.ts @@ -5,8 +5,19 @@ * connection to the browser, and uses esbuild for TypeScript transformation. * * Protocol (newline-delimited JSON): - * Request: { "id": string, "code": string, "timeout_ms"?: number } - * Response: { "id": string, "success": boolean, "result"?: any, "error"?: string, "stack"?: string } + * Request: { "id": string, "code": string, "timeout_ms"?: number, + * "executor"?: string, "target_id"?: string, "tab_created"?: boolean } + * Response: { "id": string, "success": boolean, "result"?: any, "error"?: string, "stack"?: string, + * "target_id"?: string, "tab_created"?: boolean, "timed_out"?: boolean, "tab_missing"?: boolean } + * + * "target_id" and "tab_created" in the response describe the tab `page` was + * bound to; they are omitted if the call failed before binding one. + * + * When "executor" is set, the API owns the executor's tab: it opens the tab + * and passes its "target_id" (with "tab_created" when it just opened it), and + * `page` is bound to that tab. If the tab no longer exists the daemon answers + * with "tab_missing" without running the code, so the API can open a new tab + * and resend the request. */ import { createServer, Socket } from 'net'; @@ -42,6 +53,9 @@ interface ExecuteRequest { id: string; code: string; timeout_ms?: number; + executor?: string; + target_id?: string; + tab_created?: boolean; } interface ExecuteResponse { @@ -50,6 +64,17 @@ interface ExecuteResponse { result?: unknown; error?: string; stack?: string; + target_id?: string; + tab_created?: boolean; + timed_out?: boolean; + tab_missing?: boolean; +} + +// The tab a call bound `page` to. It is recorded as soon as it is known so a +// timed-out call still reports it. +interface BoundTab { + targetId?: string; + created?: boolean; } function withTimeout(promise: Promise, signal: AbortSignal): Promise { @@ -258,7 +283,65 @@ async function resolveActivePage(browser: Browser): Promise { return null; } -async function executeCode(request: ExecuteRequest, signal: AbortSignal): Promise { +const OWNED_PAGE_RESOLUTION_ATTEMPTS = 3; +const OWNED_PAGE_RESOLUTION_RETRY_DELAY_MS = 150; + +async function findPage(browser: Browser, targetId: string, refresh: boolean): Promise { + const pages = browser + .contexts() + .flatMap(context => context.pages()) + .filter(page => !page.isClosed()); + return (await pageTargetIdCache.buildPageByTargetId(pages, { refresh })).get(targetId); +} + +async function targetExists(browser: Browser, targetId: string): Promise { + const root = await browser.newBrowserCDPSession(); + try { + await root.send('Target.getTargetInfo', { targetId }); + return true; + } catch { + return false; + } finally { + await root.detach().catch(() => {}); + } +} + +const NEW_TAB_ATTACH_ATTEMPTS = 50; +const NEW_TAB_ATTACH_RETRY_DELAY_MS = 100; + +class TabMissingError extends Error {} + +// Binds an executor's `page` to the tab the API gave it. A tab the API just +// opened gets longer to appear in this connection. A tab that exists but never +// gets a Playwright page is an error; the connection is dropped first so the +// next call reconnects and re-attaches to every tab instead of failing on the +// same stale connection. +async function resolveExecutorPage(browser: Browser, tab: BoundTab, targetId: string, created: boolean): Promise { + const attempts = created ? NEW_TAB_ATTACH_ATTEMPTS : OWNED_PAGE_RESOLUTION_ATTEMPTS; + const delayMs = created ? NEW_TAB_ATTACH_RETRY_DELAY_MS : OWNED_PAGE_RESOLUTION_RETRY_DELAY_MS; + for (let attempt = 0; attempt < attempts; attempt++) { + if (attempt > 0) { + await new Promise(resolve => setTimeout(resolve, delayMs)); + } + const page = await findPage(browser, targetId, attempt > 0); + if (page) { + tab.targetId = targetId; + tab.created = created; + return page; + } + if (!(await targetExists(browser, targetId))) { + throw new TabMissingError(`executor tab ${targetId} is closed`); + } + } + await disconnectBrowser(); + throw new Error(`executor tab ${targetId} is open but not available to Playwright yet; retry the call`); +} + +async function executeCode( + request: ExecuteRequest, + signal: AbortSignal, + tab: BoundTab, +): Promise { const { id, code } = request; try { @@ -296,17 +379,32 @@ async function executeCode(request: ExecuteRequest, signal: AbortSignal): Promis }; } } - const contexts = browserInstance.contexts(); - const defaultContext = contexts.length > 0 ? contexts[0] : await browserInstance.newContext(); - const pages = contexts.flatMap(context => context.pages()); - // Bind `page` to the actual foreground tab (see resolveActivePage). Using - // pages[0] bound `page` to the oldest tab regardless of which was active, so - // calls like page.pdf() operated on the wrong tab whenever more than one was - // open. - const page = - (pages.length > 0 ? await resolveActivePage(browserInstance) : null) ?? - pages.findLast(candidate => !candidate.isClosed()) ?? - (await defaultContext.newPage()); + let page: Page; + if (request.executor) { + if (!request.target_id) { + throw new Error('executor call has no target_id'); + } + page = await resolveExecutorPage(browserInstance, tab, request.target_id, request.tab_created === true); + } else { + const contexts = browserInstance.contexts(); + const defaultContext = contexts.length > 0 ? contexts[0] : await browserInstance.newContext(); + const pages = contexts.flatMap(context => context.pages()); + // Bind `page` to the actual foreground tab (see resolveActivePage). Using + // pages[0] bound `page` to the oldest tab regardless of which was active, so + // calls like page.pdf() operated on the wrong tab whenever more than one was + // open. + const existing = + (pages.length > 0 ? await resolveActivePage(browserInstance) : null) ?? + pages.findLast(candidate => !candidate.isClosed()); + page = existing ?? (await defaultContext.newPage()); + try { + tab.targetId = await pageTargetIdCache.get(page); + tab.created = !existing; + } catch { + // A page that is crashing or closing can fail target discovery; the + // call still runs, it just reports no tab. + } + } const context = page.context(); const webmcp = createWebMCPClient({apiBaseUrl: KERNEL_API_ENDPOINT, signal}); @@ -331,10 +429,27 @@ async function executeCode(request: ExecuteRequest, signal: AbortSignal): Promis success: false, error: error.message, stack: error.stack, + ...(error instanceof TabMissingError && { tab_missing: true }), }; } } +// Serializes a response line. A result JSON cannot represent (a cycle, a +// BigInt) becomes an error response instead of an exception that would leave +// the caller without an answer. +function responseLine(response: ExecuteResponse): string { + try { + return JSON.stringify(response) + '\n'; + } catch (error: any) { + const { result: _result, ...rest } = response; + return JSON.stringify({ + ...rest, + success: false, + error: `result is not JSON-serializable: ${error.message}`, + }) + '\n'; + } +} + function handleConnection(socket: Socket): void { let buffer = ''; @@ -362,9 +477,10 @@ function handleConnection(socket: Socket): void { } const signal = AbortSignal.timeout(request.timeout_ms ?? 60000); + const tab: BoundTab = {}; let response: ExecuteResponse; try { - response = await withTimeout(executeCode(request, signal), signal); + response = await withTimeout(executeCode(request, signal, tab), signal); } catch (error: any) { if (signal.aborted) { await disconnectBrowser(); @@ -374,9 +490,14 @@ function handleConnection(socket: Socket): void { success: false, error: error.message, stack: error.stack, + ...(signal.aborted && { timed_out: true }), }; } - socket.write(JSON.stringify(response) + '\n'); + if (tab.targetId) { + response.target_id = tab.targetId; + response.tab_created = tab.created; + } + socket.write(responseLine(response)); } });