From 9e3a80e5b916383b53570d8c26c845a19afcb06b Mon Sep 17 00:00:00 2001 From: hiroTamada Date: Tue, 19 May 2026 09:27:41 -0400 Subject: [PATCH 01/12] Add batched POST /chromium/configure with optional start_url navigation. Single multipart request can apply display, policies, flags, extensions, and profile changes with one Chromium restart, then navigate via CDP after DevTools is ready. Co-authored-by: Cursor --- AGENTS.md | 2 + images/chromium-headless/run-docker.sh | 19 +- .../run-local-chromium-configure-powerset.sh | 43 + server/Makefile | 2 +- server/cmd/api/api/chromium.go | 156 ++-- server/cmd/api/api/chromium_configure.go | 623 +++++++++++++ server/cmd/api/api/chromium_configure_test.go | 76 ++ .../e2e_chromium_configure_powerset_test.go | 199 ++++ server/e2e/e2e_chromium_configure_test.go | 51 ++ server/lib/cdpclient/cdpclient.go | 126 +++ server/lib/oapi/oapi.go | 858 ++++++++++++------ server/openapi.yaml | 87 ++ 12 files changed, 1907 insertions(+), 335 deletions(-) create mode 100755 scripts/run-local-chromium-configure-powerset.sh create mode 100644 server/cmd/api/api/chromium_configure.go create mode 100644 server/cmd/api/api/chromium_configure_test.go create mode 100644 server/e2e/e2e_chromium_configure_powerset_test.go create mode 100644 server/e2e/e2e_chromium_configure_test.go diff --git a/AGENTS.md b/AGENTS.md index cdeccf59..8b19b6df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,8 @@ Kernel Images is a sandboxed cloud browser infrastructure platform. The Go serve - **Build server**: `cd server && make build` - **Build headless image**: `cd /workspace && DOCKER_BUILDKIT=1 docker build -f images/chromium-headless/image/Dockerfile -t kernel-headless-test .` - **Run headless container**: `docker run -d --name kernel-headless -p 10001:10001 -p 9222:9222 --shm-size=2g kernel-headless-test` +- **Run headless (repo scripts)** — foreground: `./images/chromium-headless/run-docker.sh` maps API to host **`:444`**; background: `DETACHED=1 ./images/chromium-headless/run-docker.sh` +- **Chromium configure multipart permutation e2e** (31 part combinations — rebuild image first): `./scripts/run-local-chromium-configure-powerset.sh` - See `server/README.md` and `server/Makefile` for additional commands and configuration. ### Docker in Cloud VM diff --git a/images/chromium-headless/run-docker.sh b/images/chromium-headless/run-docker.sh index 4a670748..a31f0425 100755 --- a/images/chromium-headless/run-docker.sh +++ b/images/chromium-headless/run-docker.sh @@ -10,6 +10,12 @@ source ../../shared/ensure-common-build-run-vars.sh chromium-headless HOST_RECORDINGS_DIR="$SCRIPT_DIR/recordings" mkdir -p "$HOST_RECORDINGS_DIR" +# DETACHED=1 runs in background (--rm cleans up container on stop/failure teardown without -it) +DOCKER_RUN_FRONT=(-it --rm) +if [[ "${DETACHED:-}" == "1" ]]; then + DOCKER_RUN_FRONT=(-d --rm) +fi + RUN_ARGS=( --name "$NAME" --privileged @@ -42,4 +48,15 @@ if [[ $# -ge 1 && -n "$1" ]]; then fi docker rm -f "$NAME" 2>/dev/null || true -docker run -it --rm "${ENTRYPOINT_ARG[@]}" "${RUN_ARGS[@]}" "$IMAGE" + +if [[ "${DETACHED:-}" == "1" ]]; then + echo "Detached mode: Kernel HTTP API mapped to http://127.0.0.1:444 (container port 10001)" + echo "CDP WS proxy http://127.0.0.1:9222 ChromeDriver proxy http://127.0.0.1:9224" +fi + +docker run "${DOCKER_RUN_FRONT[@]}" "${ENTRYPOINT_ARG[@]}" "${RUN_ARGS[@]}" "$IMAGE" + +if [[ "${DETACHED:-}" == "1" ]]; then + echo "Container id/name: $NAME" + echo "Stop with: docker stop $NAME" +fi diff --git a/scripts/run-local-chromium-configure-powerset.sh b/scripts/run-local-chromium-configure-powerset.sh new file mode 100755 index 00000000..3eab1283 --- /dev/null +++ b/scripts/run-local-chromium-configure-powerset.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# 1) Build the headless chromium image via images/chromium-headless/build-docker.sh +# 2) Run chromium/configure multipart powerset e2e (31 part combinations + JSON start_url + legacy bare-start_url test). +# +# Prereqs: Docker, Go, network for pulls. +# +# Tests use testcontainers-go (dynamic host ports). +# For manual experiments: DETACHED=1 ./images/chromium-headless/run-docker.sh → API on http://127.0.0.1:444 +# +# Usage: +# ./scripts/run-local-chromium-configure-powerset.sh +# IMAGE=onkernel/chromium-headless-test:mytag ./scripts/run-local-chromium-configure-powerset.sh +# +# Skip image rebuild: +# ./scripts/run-local-chromium-configure-powerset.sh --skip-build + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +SKIP_BUILD=0 +for arg in "$@"; do + case "$arg" in + --skip-build) SKIP_BUILD=1 ;; + *) + echo "unknown arg: $arg" >&2 + exit 1 + ;; + esac +done + +IMAGE="${IMAGE:-onkernel/chromium-headless-test:latest}" +export E2E_CHROMIUM_HEADLESS_IMAGE="$IMAGE" + +if [[ "$SKIP_BUILD" == "0" ]]; then + (cd "$ROOT/images/chromium-headless" && IMAGE="$IMAGE" ./build-docker.sh) +fi + +echo "Running chromium/configure permutation tests against image $E2E_CHROMIUM_HEADLESS_IMAGE ..." +(cd "$ROOT/server" && + go test ./e2e -count=1 -timeout 120m -v \ + -run 'TestChromiumConfigure(StartURLBare|MultipartPowerset|StartURLJSONObject)$') diff --git a/server/Makefile b/server/Makefile index 303ea419..6f98d35b 100644 --- a/server/Makefile +++ b/server/Makefile @@ -36,7 +36,7 @@ test: @echo "" @echo "=== Running e2e tests (testcontainers — this may take a few minutes) ===" @echo "" - go test -v -race ./e2e/ + go test -v -race -timeout 120m ./e2e/ clean: @rm -rf $(BIN_DIR) diff --git a/server/cmd/api/api/chromium.go b/server/cmd/api/api/chromium.go index 0fbda144..a8040905 100644 --- a/server/cmd/api/api/chromium.go +++ b/server/cmd/api/api/chromium.go @@ -21,6 +21,12 @@ import ( var nameRegex = regexp.MustCompile(`^[A-Za-z0-9._-]{1,255}$`) +// extensionZipItem is a finalized name + temp zip path (caller removes temps). +type extensionZipItem struct { + zipTemp string + name string +} + // chromiumFlagsPath is the runtime flags file read by the chromium-launcher at startup. const chromiumFlagsPath = "/chromium/flags" @@ -130,52 +136,73 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "no extensions provided"}}, nil } - // Materialize uploads + extItems := make([]extensionZipItem, 0, len(items)) + for _, p := range items { + if !p.zipReceived || p.name == "" { + return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "each item must include zip_file and name"}}, nil + } + extItems = append(extItems, extensionZipItem{zipTemp: p.zipTemp, name: p.name}) + } + + reqMsg, err := s.applyExtensionZipItems(ctx, extItems) + if reqMsg != "" { + return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: reqMsg}}, nil + } + if err != nil { + return oapi.UploadExtensionsAndRestart500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: err.Error()}}, nil + } + + // Restart Chromium and wait for DevTools to be ready + if err := s.restartChromiumAndWait(ctx, "extension upload"); err != nil { + return oapi.UploadExtensionsAndRestart500JSONResponse{ + InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: err.Error()}, + }, nil + } + + log.Info("devtools ready", "elapsed", time.Since(start).String()) + return oapi.UploadExtensionsAndRestart201Response{}, nil +} + +// applyExtensionZipItems applies name+zipTemp extension pairs (merge flags for --load-extension). +// On validation errors returns (reqMsg, nil); on internal errors returns ("", err). +func (s *ApiService) applyExtensionZipItems(ctx context.Context, items []extensionZipItem) (reqMsg string, err error) { + log := logger.FromContext(ctx) extBase := "/home/kernel/extensions" - // Fail early if any destination already exists for _, p := range items { dest := filepath.Join(extBase, p.name) if _, err := os.Stat(dest); err == nil { - return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: fmt.Sprintf("extension name already exists: %s", p.name)}}, nil + return fmt.Sprintf("extension name already exists: %s", p.name), nil } else if !os.IsNotExist(err) { log.Error("failed to check extension dir", "error", err) - return oapi.UploadExtensionsAndRestart500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to check extension dir"}}, nil + return "", fmt.Errorf("failed to check extension dir: %w", err) } } for _, p := range items { - if !p.zipReceived || p.name == "" { - return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "each item must include zip_file and name"}}, nil - } dest := filepath.Join(extBase, p.name) if err := os.MkdirAll(dest, 0o755); err != nil { log.Error("failed to create extension dir", "error", err) - return oapi.UploadExtensionsAndRestart500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to create extension dir"}}, nil + return "", fmt.Errorf("failed to create extension dir: %w", err) } if err := ziputil.Unzip(p.zipTemp, dest); err != nil { log.Error("failed to unzip zip file", "error", err) - return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "invalid zip file"}}, nil + return "invalid zip file", nil } - // Rewrite update.xml URLs to match the extension name (directory name) - // This ensures URLs like /extensions/web-bot-auth/ become /extensions// updateXMLPath := filepath.Join(dest, "update.xml") if err := policy.RewriteUpdateXMLUrls(updateXMLPath, p.name); err != nil { log.Warn("failed to rewrite update.xml URLs", "error", err, "extension", p.name) - // continue since not all extensions require update.xml } if err := exec.Command("chown", "-R", "kernel:kernel", dest).Run(); err != nil { log.Error("failed to chown extension dir", "error", err) - return oapi.UploadExtensionsAndRestart500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to chown extension dir"}}, nil + return "", fmt.Errorf("failed to chown extension dir: %w", err) } log.Info("installed extension", "name", p.name) } - // Update enterprise policy for extensions that require it - // Track which extensions need --load-extension flags (those NOT using policy installation) var pathsNeedingFlags []string for _, p := range items { @@ -184,14 +211,11 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap manifestPath := filepath.Join(extensionPath, "manifest.json") updateXMLPath := filepath.Join(extensionPath, "update.xml") - // Check if this extension requires enterprise policy requiresEntPolicy, err := s.policy.RequiresEnterprisePolicy(manifestPath) if err != nil { log.Warn("failed to read manifest for policy check", "error", err, "extension", extensionName) - // Continue with requiresEntPolicy = false } - // Try to extract Chrome extension ID from update.xml chromeExtensionID := extensionName var extractionErr error if extractedID, err := policy.ExtractExtensionIDFromUpdateXML(updateXMLPath); err == nil { @@ -205,25 +229,17 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap if requiresEntPolicy { log.Info("extension requires enterprise policy", "name", extensionName) - // Validate that update.xml and .crx files are present for policy-installed extensions - // These files are required for ExtensionInstallForcelist to work hasUpdateXML := false hasCRX := false if _, err := os.Stat(updateXMLPath); err == nil { - // For policy extensions, update.xml must exist AND be parseable if extractionErr != nil { - return oapi.UploadExtensionsAndRestart400JSONResponse{ - BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ - Message: fmt.Sprintf("extension %s requires enterprise policy but update.xml is invalid: %v", extensionName, extractionErr), - }, - }, nil + return fmt.Sprintf("extension %s requires enterprise policy but update.xml is invalid: %v", extensionName, extractionErr), nil } hasUpdateXML = true log.Info("found update.xml in extension zip", "name", extensionName) } - // Look for any .crx file in the directory entries, err := os.ReadDir(extensionPath) if err == nil { for _, entry := range entries { @@ -235,7 +251,6 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap } } - // If missing required files for ExtensionInstallForcelist, fall back to --load-extension if !hasUpdateXML || !hasCRX { log.Info("extension missing policy files, falling back to --load-extension", "name", extensionName, "hasUpdateXML", hasUpdateXML, "hasCRX", hasCRX) @@ -243,31 +258,17 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap pathsNeedingFlags = append(pathsNeedingFlags, extensionPath) } } else { - // Only add --load-extension flags for non-policy extensions pathsNeedingFlags = append(pathsNeedingFlags, extensionPath) } - // Add to enterprise policy - // Pass both extensionName (for URL paths) and chromeExtensionID (for policy entries) if err := s.policy.AddExtension(extensionName, chromeExtensionID, extensionPath, requiresEntPolicy); err != nil { log.Error("failed to update enterprise policy", "error", err, "extension", extensionName) - return oapi.UploadExtensionsAndRestart500JSONResponse{ - InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ - Message: fmt.Sprintf("failed to update enterprise policy for %s: %v", extensionName, err), - }, - }, nil + return "", fmt.Errorf("failed to update enterprise policy for %s: %w", extensionName, err) } log.Info("updated enterprise policy", "extension", extensionName, "chromeExtensionID", chromeExtensionID, "requiresEnterprisePolicy", requiresEntPolicy) } - // Build flags overlay file in /chromium/flags, merging with existing flags - // Only add --load-extension flags for extensions that don't use policy installation - // NOTE: We intentionally do NOT use --disable-extensions-except here because it causes - // Chrome to disable external providers (including the policy loader), which prevents - // enterprise policy extensions (ExtensionInstallForcelist) from being fetched and installed. - // See Chromium source: extension_service.cc - external providers are only created when - // extensions_enabled() returns true, which is false when --disable-extensions-except is used. var newTokens []string if len(pathsNeedingFlags) > 0 { newTokens = []string{ @@ -275,22 +276,11 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap } } - // Merge and write flags if _, err := s.mergeAndWriteChromiumFlags(ctx, newTokens); err != nil { - return oapi.UploadExtensionsAndRestart500JSONResponse{ - InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: err.Error()}, - }, nil - } - - // Restart Chromium and wait for DevTools to be ready - if err := s.restartChromiumAndWait(ctx, "extension upload"); err != nil { - return oapi.UploadExtensionsAndRestart500JSONResponse{ - InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: err.Error()}, - }, nil + return "", err } - log.Info("devtools ready", "elapsed", time.Since(start).String()) - return oapi.UploadExtensionsAndRestart201Response{}, nil + return "", nil } // mergeAndWriteChromiumFlags reads existing flags, merges them with new flags, @@ -370,6 +360,60 @@ func (s *ApiService) restartChromiumAndWait(ctx context.Context, operation strin } } +const supervisorCtlConf = "/etc/supervisor/supervisord.conf" + +func supervisorctlArgv(verb string, prog string) []string { + return []string{"-c", supervisorCtlConf, verb, prog} +} + +// stopChromium runs supervisorctl stop chromium and waits for the command to complete. +func (s *ApiService) stopChromium(ctx context.Context) error { + log := logger.FromContext(ctx) + cmdCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Minute) + defer cancel() + log.Info("stopping chromium via supervisorctl") + out, err := exec.CommandContext(cmdCtx, "supervisorctl", supervisorctlArgv("stop", "chromium")...).CombinedOutput() + if err != nil { + log.Error("failed to stop chromium", "error", err, "out", string(out)) + return fmt.Errorf("supervisorctl stop chromium failed: %w", err) + } + return nil +} + +// startChromiumAndWait launches chromium via supervisorctl start and waits for DevTools readiness. +func (s *ApiService) startChromiumAndWait(ctx context.Context, operation string) error { + log := logger.FromContext(ctx) + start := time.Now() + + updates, cancelSub := s.upstreamMgr.Subscribe() + defer cancelSub() + + errCh := make(chan error, 1) + log.Info("starting chromium via supervisorctl", "operation", operation) + go func() { + cmdCtx, cancelCmd := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Minute) + defer cancelCmd() + out, err := exec.CommandContext(cmdCtx, "supervisorctl", supervisorctlArgv("start", "chromium")...).CombinedOutput() + if err != nil { + log.Error("failed to start chromium", "error", err, "out", string(out)) + errCh <- fmt.Errorf("supervisorctl start chromium failed: %w", err) + } + }() + + timeout := time.NewTimer(15 * time.Second) + defer timeout.Stop() + select { + case <-updates: + log.Info("devtools ready", "operation", operation, "elapsed", time.Since(start).String()) + return nil + case err := <-errCh: + return err + case <-timeout.C: + log.Info("devtools not ready in time", "operation", operation, "elapsed", time.Since(start).String()) + return fmt.Errorf("devtools not ready in time") + } +} + // PatchChromiumPolicies applies user-provided Chromium enterprise policy overrides // to policy.json, restarts Chromium, and waits for DevTools to be ready. func (s *ApiService) PatchChromiumPolicies(ctx context.Context, request oapi.PatchChromiumPoliciesRequestObject) (oapi.PatchChromiumPoliciesResponseObject, error) { diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go new file mode 100644 index 00000000..ad6a68a0 --- /dev/null +++ b/server/cmd/api/api/chromium_configure.go @@ -0,0 +1,623 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/url" + "os" + "os/exec" + "strconv" + "strings" + "time" + + "github.com/kernel/kernel-images/server/lib/cdpclient" + "github.com/kernel/kernel-images/server/lib/logger" + oapi "github.com/kernel/kernel-images/server/lib/oapi" + "github.com/kernel/kernel-images/server/lib/policy" + "github.com/kernel/kernel-images/server/lib/zstdutil" +) + +const userDataProfileDir = "/home/kernel/user-data" + +type chromiumConfigureState struct { + displayJSON *string + chromiumFlagsJSON *string + chromePoliciesJSON *string + + stripComponents int + + profileTemp string // temp archive path + hasProfile bool + + startURLRaw *string + + extItems []extensionZipItem // zipTemp paths; merged with chromiumCfgParseExtensions + + allTemps []string +} + +func (st *chromiumConfigureState) cleanup() { + for _, p := range st.allTemps { + _ = os.Remove(p) + } +} + +// ChromiumConfigure batched Chromium/session configuration plus optional navigation. +func (s *ApiService) ChromiumConfigure(ctx context.Context, request oapi.ChromiumConfigureRequestObject) (oapi.ChromiumConfigureResponseObject, error) { + start := time.Now() + + if request.Body == nil { + return cfg400("request body required"), nil + } + + st := &chromiumConfigureState{} + if msg := chromiumCfgParseMultipart(request.Body, st); msg != "" { + st.cleanup() + return cfg400(msg), nil + } + defer st.cleanup() + + if cfgActionables(st)+cfgHasStartURL(st.startURLRaw) == 0 { + return cfg400("no configuration fields provided"), nil + } + + needsStop := chromiumNeedsStopCycle(st) + + if needsStop { + if chromiumDisplayHasSizedRequest(st.displayJSON) { + stopped, stopErr := s.stopActiveRecordings(ctx) + if stopErr != nil { + return cfg500Configure(fmt.Sprintf("failed to stop recordings: %v", stopErr)), nil + } + if len(stopped) > 0 { + defer func() { + go s.startNewRecordingSegments(context.WithoutCancel(ctx), stopped) + }() + } + } + + logger.FromContext(ctx).Info("chromium configure (stop/start path)") + if err := s.stopChromium(ctx); err != nil { + return cfg500Configure(err.Error()), nil + } + + if st.hasProfile { + if err := chromiumApplyProfileArchive(st.profileTemp, st.stripComponents); err != nil { + return cfg500Configure(err.Error()), nil + } + } + + if chromiumDisplayHasSizedRequest(st.displayJSON) { + b, msgs := chromiumParseDisplayParts(st.displayJSON) + if msgs != "" { + return cfg400(msgs), nil + } + if b != nil { + if rr := chromiumDisplayApplyWhileStopped(ctx, s, b); rr != nil { + return rr, nil + } + } + } + + if msgs := chromiumApplyPolicies(ctx, s, st.chromePoliciesJSON); msgs != "" { + return policyDisposition(msgs), nil + } + + if reqMsgs, ierr := chromiumApplyExtensions(ctx, s, st.extItems); reqMsgs != "" { + return cfg400(reqMsgs), nil + } else if ierr != nil { + return cfg500Configure(ierr.Error()), nil + } + + if msgs := chromiumMergeFlagsRaw(ctx, s, st.chromiumFlagsJSON); msgs != "" { + if strings.HasPrefix(msgs, "bad:") { + return cfg400(strings.TrimPrefix(msgs, "bad:")), nil + } + return cfg500Configure(strings.TrimPrefix(msgs, "int:")), nil + } + + if err := s.startChromiumAndWait(ctx, "batched chromium configure"); err != nil { + return cfg500Configure(err.Error()), nil + } + } else { + if st.displayJSON != nil && strings.TrimSpace(*st.displayJSON) != "" { + body, msgs := chromiumParseDisplayParts(st.displayJSON) + if msgs != "" { + return cfg400(msgs), nil + } + if rr := chromiumRunPatchDisplay(ctx, s, body); rr != nil { + return rr, nil + } + } + } + + spec, msgs := chromiumStartURLSpec(st.startURLRaw) + if msgs != "" { + return cfg400(msgs), nil + } + if spec.needsNav { + if err := chromiumDoNavigate(ctx, s, spec); err != nil { + return cfg500Navigate(err.Error()), nil + } + } + + logger.FromContext(ctx).Info("chromium configure finished", "elapsed", time.Since(start).String()) + return oapi.ChromiumConfigure200JSONResponse{Ok: true}, nil +} + +type startURLParsed struct { + needsNav bool + url string + wait cdpclient.NavigateWaitUntil + timeout time.Duration +} + +func chromiumStartURLSpec(raw *string) (startURLParsed, string) { + var out startURLParsed + out.timeout = 45 * time.Second + out.wait = cdpclient.NavigateWaitLoad + if raw == nil || strings.TrimSpace(*raw) == "" { + return out, "" + } + s := strings.TrimSpace(*raw) + if strings.HasPrefix(s, "{") { + var v struct { + URL string `json:"url"` + WaitUntil string `json:"wait_until"` + Timeout *int `json:"timeout_sec,omitempty"` + } + if err := json.Unmarshal([]byte(s), &v); err != nil { + return out, "invalid start_url JSON" + } + if strings.TrimSpace(v.URL) == "" { + return out, "start_url JSON requires url" + } + switch strings.TrimSpace(strings.ToLower(v.WaitUntil)) { + case "", "load": + out.wait = cdpclient.NavigateWaitLoad + case "domcontentloaded": + out.wait = cdpclient.NavigateWaitDOMContentLoaded + default: + return out, "wait_until must be load or domcontentloaded" + } + out.url = strings.TrimSpace(v.URL) + if v.Timeout != nil && *v.Timeout > 0 { + out.timeout = time.Duration(*v.Timeout) * time.Second + } + } else { + out.url = s + } + if errMsgs := chromiumValidateNavigateURL(out.url); errMsgs != "" { + return out, errMsgs + } + out.needsNav = true + return out, "" +} + +func chromiumValidateNavigateURL(u string) string { + parsed, err := url.Parse(u) + if err != nil { + return "invalid start URL" + } + switch strings.ToLower(parsed.Scheme) { + case "https", "http", "about", "data", "chrome", "devtools": + default: + return fmt.Sprintf("unsupported URL scheme %q", parsed.Scheme) + } + return "" +} + +func chromiumDoNavigate(ctx context.Context, s *ApiService, spec startURLParsed) error { + upstream := s.upstreamMgr.Current() + if upstream == "" { + return fmt.Errorf("devtools upstream not available") + } + navCtx, cancel := context.WithTimeout(ctx, spec.timeout) + defer cancel() + return cdpclient.NavigateFirstPage(navCtx, upstream, spec.url, spec.wait) +} + +func chromiumNeedsStopCycle(st *chromiumConfigureState) bool { + return st.hasProfile || + len(st.extItems) > 0 || + policiesContentNonEmpty(st.chromePoliciesJSON) || + flagsContentNonEmpty(st.chromiumFlagsJSON) +} + +func policiesContentNonEmpty(s *string) bool { + if !policiesNonEmpty(s) { + return false + } + var m map[string]interface{} + if err := json.Unmarshal([]byte(strings.TrimSpace(*s)), &m); err != nil { + return true + } + return len(m) > 0 +} + +func flagsContentNonEmpty(s *string) bool { + if !flagsNonEmpty(s) { + return false + } + var raw struct { + Flags []string `json:"flags"` + } + if err := json.Unmarshal([]byte(strings.TrimSpace(*s)), &raw); err != nil { + return true + } + return len(raw.Flags) > 0 +} + +func policiesNonEmpty(s *string) bool { + return s != nil && strings.TrimSpace(*s) != "" +} + +func flagsNonEmpty(s *string) bool { + return s != nil && strings.TrimSpace(*s) != "" +} + +func chromiumDisplayHasSizedRequest(displayJSON *string) bool { + if displayJSON == nil { + return false + } + var raw map[string]interface{} + if err := json.Unmarshal([]byte(*displayJSON), &raw); err != nil { + return false + } + w, ow := raw["width"] + h, oh := raw["height"] + if !ow || !oh { + return false + } + fw, wok := w.(float64) + fh, hok := h.(float64) + if wok && hok && fw > 0 && fh > 0 { + return true + } + iw, wok := w.(int) + ih, hok := h.(int) + return wok && hok && iw > 0 && ih > 0 +} + +func cfg400(msg string) oapi.ChromiumConfigure400JSONResponse { + return oapi.ChromiumConfigure400JSONResponse{ + BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: msg}, + } +} + +func cfg500Configure(msg string) oapi.ChromiumConfigure500JSONResponse { + return oapi.ChromiumConfigure500JSONResponse(oapi.ChromiumConfigureError{ + Phase: oapi.ConfigurePhase, + Message: msg, + }) +} + +func cfg500Navigate(msg string) oapi.ChromiumConfigure500JSONResponse { + return oapi.ChromiumConfigure500JSONResponse(oapi.ChromiumConfigureError{ + Phase: oapi.NavigatePhase, + Message: msg, + }) +} + +func cfgActionables(st *chromiumConfigureState) int { + n := 0 + if policiesContentNonEmpty(st.chromePoliciesJSON) { + n++ + } + if flagsContentNonEmpty(st.chromiumFlagsJSON) { + n++ + } + if len(st.extItems) > 0 { + n++ + } + if st.hasProfile { + n++ + } + if chromiumDisplayHasSizedRequest(st.displayJSON) { + n++ + } + return n +} + +func cfgHasStartURL(s *string) int { + if s == nil || strings.TrimSpace(*s) == "" { + return 0 + } + return 1 +} + +func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) string { + mr, ok := any(body).(interface { + NextPart() (*multipart.Part, error) + }) + if !ok { + return "multipart reader not available" + } + + type pend struct { + zipTmp string + name string + gotZip bool + } + var cur *pend + + for { + part, err := mr.NextPart() + if err == io.EOF { + break + } + if err != nil { + return "failed reading multipart" + } + switch name := part.FormName(); name { + case "display": + b, err := io.ReadAll(part) + if err != nil { + return "read display field" + } + v := strings.TrimSpace(string(b)) + st.displayJSON = &v + case "chromium_flags": + b, err := io.ReadAll(part) + if err != nil { + return "read chromium_flags field" + } + v := string(b) + st.chromiumFlagsJSON = &v + case "chrome_policies": + b, err := io.ReadAll(part) + if err != nil { + return "read chrome_policies field" + } + v := string(b) + st.chromePoliciesJSON = &v + case "strip_components": + b, err := io.ReadAll(part) + if err != nil { + return "read strip_components" + } + if n, err := strconv.Atoi(strings.TrimSpace(string(b))); err == nil && n >= 0 { + st.stripComponents = n + } + case "profile_archive": + tmp, err := os.CreateTemp("", "bcc-prof-*.tar.zst") + if err != nil { + return "temp profile_archive" + } + st.allTemps = append(st.allTemps, tmp.Name()) + if _, err := io.Copy(tmp, part); err != nil { + tmp.Close() + return "read profile_archive" + } + if err := tmp.Close(); err != nil { + return "finalize profile_archive" + } + st.profileTemp = tmp.Name() + st.hasProfile = true + case "start_url": + b, err := io.ReadAll(part) + if err != nil { + return "read start_url" + } + v := string(b) + st.startURLRaw = &v + case "extensions.zip_file": + if cur == nil { + cur = &pend{} + } + tmp, err := os.CreateTemp("", "bcc-ext-*.zip") + if err != nil { + return "temp extensions.zip_file" + } + st.allTemps = append(st.allTemps, tmp.Name()) + if _, err := io.Copy(tmp, part); err != nil { + tmp.Close() + return "read extensions.zip_file" + } + if err := tmp.Close(); err != nil { + return "close extensions.zip_file" + } + if cur.gotZip { + return "duplicate extensions.zip_file pair" + } + cur.zipTmp = tmp.Name() + cur.gotZip = true + case "extensions.name": + if cur == nil { + cur = &pend{} + } + b, err := io.ReadAll(part) + if err != nil { + return "read extensions.name" + } + nm := strings.TrimSpace(string(b)) + if nm == "" || !nameRegex.MatchString(nm) { + return "invalid extensions.name" + } + if cur.name != "" { + return "duplicate extensions.name in pair" + } + cur.name = nm + default: + return fmt.Sprintf("unknown form field %q", name) + } + if cur != nil && cur.gotZip && cur.name != "" { + st.extItems = append(st.extItems, extensionZipItem{zipTemp: cur.zipTmp, name: cur.name}) + cur = nil + } + } + if cur != nil && (!cur.gotZip || cur.name == "") { + return "each extension pair needs extensions.zip_file plus extensions.name" + } + return "" +} + +func chromiumApplyProfileArchive(profilePath string, strip int) error { + if err := os.RemoveAll(userDataProfileDir); err != nil { + return fmt.Errorf("clear user-data: %w", err) + } + if err := os.MkdirAll(userDataProfileDir, 0o755); err != nil { + return fmt.Errorf("mkdir user-data: %w", err) + } + f, err := os.Open(profilePath) + if err != nil { + return err + } + defer f.Close() + if err := zstdutil.UntarZstd(f, userDataProfileDir, strip); err != nil { + return fmt.Errorf("extract profile archive: %w", err) + } + out, err := exec.Command("chown", "-R", "kernel:kernel", userDataProfileDir).CombinedOutput() + if err != nil { + return fmt.Errorf("chown user-data: %w (%s)", err, string(out)) + } + return nil +} + +func chromiumParseDisplayParts(displayJSON *string) (*oapi.PatchDisplayJSONRequestBody, string) { + if displayJSON == nil { + return nil, "" + } + var raw map[string]interface{} + if err := json.Unmarshal([]byte(*displayJSON), &raw); err != nil { + return nil, "invalid display JSON" + } + if len(raw) == 0 { + return nil, "display payload empty" + } + blob, err := json.Marshal(raw) + if err != nil { + return nil, "invalid display marshal" + } + var body oapi.PatchDisplayJSONRequestBody + if err := json.Unmarshal(blob, &body); err != nil { + return nil, fmt.Sprintf("invalid display payload: %v", err) + } + return &body, "" +} + +func chromiumDisplayApplyWhileStopped(ctx context.Context, s *ApiService, body *oapi.PatchDisplayRequest) oapi.ChromiumConfigureResponseObject { + if body.Width == nil || body.Height == nil { + return nil + } + w, h := *body.Width, *body.Height + if w <= 0 || h <= 0 { + return cfg400("display width and height must be positive") + } + mode := s.detectDisplayMode(ctx) + rr := 60 + if body.RefreshRate != nil { + rr = int(*body.RefreshRate) + } + if mode == "xvfb" { + s.xvfbResizeMu.Lock() + err := s.resizeXvfb(ctx, w, h) + s.xvfbResizeMu.Unlock() + if err != nil { + return cfg500Configure(err.Error()) + } + s.clearViewportOverride() + return nil + } + var err error + if s.isNekoEnabled() { + err = s.setResolutionViaNeko(ctx, w, h, rr) + } else { + err = s.setResolutionXorgViaXrandr(ctx, w, h, rr, false) + } + if err != nil { + return cfg500Configure(err.Error()) + } + return nil +} + +func chromiumRunPatchDisplay(ctx context.Context, s *ApiService, body *oapi.PatchDisplayJSONRequestBody) oapi.ChromiumConfigureResponseObject { + resp, err := s.PatchDisplay(ctx, oapi.PatchDisplayRequestObject{Body: body}) + if err != nil { + return cfg500Configure(err.Error()) + } + switch r := resp.(type) { + case oapi.PatchDisplay200JSONResponse: + return nil + case oapi.PatchDisplay400JSONResponse: + return cfg400(r.Message) + case oapi.PatchDisplay409JSONResponse: + return oapi.ChromiumConfigure409JSONResponse{ConflictErrorJSONResponse: r.ConflictErrorJSONResponse} + case oapi.PatchDisplay500JSONResponse: + return cfg500Configure(r.Message) + default: + return cfg500Configure("unexpected PatchDisplay response") + } +} + +func chromiumApplyPolicies(ctx context.Context, s *ApiService, raw *string) string { + if raw == nil || strings.TrimSpace(*raw) == "" { + return "" + } + var m map[string]interface{} + if err := json.Unmarshal([]byte(*raw), &m); err != nil { + return "bad:invalid chrome_policies JSON" + } + if len(m) == 0 { + return "" + } + overrides, err := policy.NewChromiumPolicyOverrides(m) + if err != nil { + if strings.Contains(err.Error(), "cannot be overridden") || strings.Contains(err.Error(), "invalid chromium policy overrides") { + return "bad:" + err.Error() + } + return "int:" + err.Error() + } + if err := s.policy.ApplyOverrides(overrides); err != nil { + if strings.Contains(err.Error(), "cannot be overridden") || strings.Contains(err.Error(), "invalid chromium policy overrides") { + return "bad:" + err.Error() + } + return "int:" + err.Error() + } + return "" +} + +func policyDisposition(msgs string) oapi.ChromiumConfigureResponseObject { + if strings.HasPrefix(msgs, "bad:") { + return cfg400(strings.TrimPrefix(msgs, "bad:")) + } + return cfg500Configure(strings.TrimPrefix(msgs, "int:")) +} + +func chromiumApplyExtensions(ctx context.Context, s *ApiService, items []extensionZipItem) (string, error) { + if len(items) == 0 { + return "", nil + } + return s.applyExtensionZipItems(ctx, items) +} + +func chromiumMergeFlagsRaw(ctx context.Context, s *ApiService, raw *string) string { + if raw == nil || strings.TrimSpace(*raw) == "" { + return "" + } + var body struct { + Flags []string `json:"flags"` + } + if err := json.Unmarshal([]byte(*raw), &body); err != nil { + return "bad:invalid chromium_flags JSON" + } + if len(body.Flags) == 0 { + return "bad:chromium_flags requires at least one flag" + } + for _, flag := range body.Flags { + t := strings.TrimSpace(flag) + if t == "" { + return "bad:empty flag in chromium_flags" + } + if !strings.HasPrefix(t, "--") { + return fmt.Sprintf("bad:invalid flag format: %s (must start with --)", flag) + } + } + if _, err := s.mergeAndWriteChromiumFlags(ctx, body.Flags); err != nil { + return "int:" + err.Error() + } + return "" +} diff --git a/server/cmd/api/api/chromium_configure_test.go b/server/cmd/api/api/chromium_configure_test.go new file mode 100644 index 00000000..f02fd8c7 --- /dev/null +++ b/server/cmd/api/api/chromium_configure_test.go @@ -0,0 +1,76 @@ +package api + +import ( + "bytes" + "mime/multipart" + "strings" + "testing" + "time" + + "github.com/kernel/kernel-images/server/lib/cdpclient" + "github.com/stretchr/testify/require" +) + +func TestFlagsContentNonEmpty(t *testing.T) { + emptyArr := `{}` + fl := `{"flags":[]}` + real := `{"flags":["--kiosk"]}` + require.False(t, flagsContentNonEmpty(&emptyArr)) + require.False(t, flagsContentNonEmpty(&fl)) + require.True(t, flagsContentNonEmpty(&real)) +} + +func TestPoliciesContentNonEmpty(t *testing.T) { + emptyObj := `{}` + real := `{"DefaultCookiesSetting": 1}` + require.False(t, policiesContentNonEmpty(&emptyObj)) + require.True(t, policiesContentNonEmpty(&real)) +} + +func TestChromiumStartURLSpec_plainAndJSON(t *testing.T) { + plain := "https://example.com/" + out, errs := chromiumStartURLSpec(&plain) + require.Empty(t, errs) + require.True(t, out.needsNav) + require.Equal(t, plain, out.url) + require.Equal(t, 45*time.Second, out.timeout) + require.Equal(t, cdpclient.NavigateWaitLoad, out.wait) + + raw := `{"url":"https://a.test/x","wait_until":"domcontentloaded","timeout_sec":12}` + out, errs = chromiumStartURLSpec(&raw) + require.Empty(t, errs) + require.True(t, out.needsNav) + require.Equal(t, "https://a.test/x", out.url) + require.Equal(t, 12*time.Second, out.timeout) + require.Equal(t, cdpclient.NavigateWaitDOMContentLoaded, out.wait) + + badScheme := "file:///etc/passwd" + _, errs = chromiumStartURLSpec(&badScheme) + require.NotEmpty(t, errs) + + badWait := `{"url":"https://x.example","wait_until":"networkidle"}` + _, errs = chromiumStartURLSpec(&badWait) + require.NotEmpty(t, errs) +} + +func TestChromiumCfgParseMultipart(t *testing.T) { + buf := bytes.NewBuffer(nil) + w := multipart.NewWriter(buf) + + require.NoError(t, w.WriteField("chrome_policies", `{"HttpsUpgradesEnabled":false}`)) + require.NoError(t, w.WriteField("strip_components", "2")) + require.NoError(t, w.WriteField("start_url", "https://kernel.example/route")) + + require.NoError(t, w.Close()) + + br := multipart.NewReader(buf, w.Boundary()) + st := &chromiumConfigureState{} + msg := chromiumCfgParseMultipart(br, st) + defer st.cleanup() + require.Empty(t, msg) + + require.True(t, policiesContentNonEmpty(st.chromePoliciesJSON)) + require.Equal(t, 2, st.stripComponents) + require.NotNil(t, st.startURLRaw) + require.Equal(t, "https://kernel.example/route", strings.TrimSpace(*st.startURLRaw)) +} diff --git a/server/e2e/e2e_chromium_configure_powerset_test.go b/server/e2e/e2e_chromium_configure_powerset_test.go new file mode 100644 index 00000000..c58352c9 --- /dev/null +++ b/server/e2e/e2e_chromium_configure_powerset_test.go @@ -0,0 +1,199 @@ +package e2e + +import ( + "bytes" + "context" + "encoding/json" + "io" + "mime/multipart" + "net/http" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + instanceoapi "github.com/kernel/kernel-images/server/lib/oapi" + "github.com/stretchr/testify/require" +) + +const ( + matDisplay = 1 << iota + matPolicy + matKioskFlags + matExtension + matStartURL + + matMaxBitmask = matDisplay | matPolicy | matKioskFlags | matExtension | matStartURL // 31 +) + +// TestChromiumConfigureMultipartPowerset runs sequential subtests covering every non-empty combination +// of multipart parts (display, chrome_policies, chromium_flags kiosk, extensions, start_url). +// Run after: images/chromium-headless/build-docker.sh (default image onkernel/chromium-headless-test:latest). +func TestChromiumConfigureMultipartPowerset(t *testing.T) { + + if _, err := exec.LookPath("docker"); err != nil { + t.Skipf("docker not available: %v", err) + } + + extDir, err := filepath.Abs("test-extension") + require.NoError(t, err) + extZip, err := zipDirToBytes(extDir) + require.NoError(t, err) + + for bits := 1; bits <= matMaxBitmask; bits++ { + bits := bits + t.Run(chromiumConfigurePowersetLabel(bits), func(t *testing.T) { + + ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute) + defer cancel() + + c := NewTestContainer(t, headlessImage) + require.NoError(t, c.Start(ctx, ContainerConfig{ + Env: map[string]string{ + "WIDTH": "1024", + "HEIGHT": "768", + }, + }), "failed to start container") + defer func() { _ = c.Stop(context.WithoutCancel(ctx)) }() + + require.NoError(t, c.WaitReady(ctx)) + + var body bytes.Buffer + w := multipart.NewWriter(&body) + require.NoError(t, chromiumConfigurePowersetPopulate(t, w, bits, extZip)) + require.NoError(t, w.Close()) + + client, err := c.APIClient() + require.NoError(t, err) + + rsp, err := client.ChromiumConfigureWithBodyWithResponse(ctx, w.FormDataContentType(), io.NopCloser(bytes.NewReader(body.Bytes()))) + require.NoError(t, err) + + require.Equal(t, http.StatusOK, rsp.StatusCode(), + "bits=%02x unexpected status=%s body=%s", bits, rsp.Status(), string(rsp.Body)) + require.NotNil(t, rsp.JSON200, "want ok JSON") + require.True(t, rsp.JSON200.Ok) + }) + } +} + +func chromiumConfigurePowersetLabel(bits int) string { + var p []string + if bits&matDisplay != 0 { + p = append(p, "display") + } + if bits&matPolicy != 0 { + p = append(p, "policy") + } + if bits&matKioskFlags != 0 { + p = append(p, "kiosk") + } + if bits&matExtension != 0 { + p = append(p, "ext") + } + if bits&matStartURL != 0 { + p = append(p, "nav") + } + return strings.Join(p, "+") +} + +func chromiumConfigurePowersetPopulate(t *testing.T, w *multipart.Writer, bits int, extZip []byte) error { + t.Helper() + + if bits&matDisplay != 0 { + restart := true + requireIdle := true + disp := instanceoapi.PatchDisplayJSONRequestBody{ + Width: intPtr(1280), + Height: intPtr(720), + RestartChromium: &restart, + RequireIdle: &requireIdle, + } + blob, err := json.Marshal(disp) + require.NoError(t, err) + if err := w.WriteField("display", string(blob)); err != nil { + return err + } + } + + if bits&matPolicy != 0 { + // QuicAllowed false is benign and allowed by server policy registry / overrides validation. + pol := map[string]interface{}{"QuicAllowed": false} + blob, err := json.Marshal(pol) + require.NoError(t, err) + if err := w.WriteField("chrome_policies", string(blob)); err != nil { + return err + } + } + + if bits&matKioskFlags != 0 { + fl := instanceoapi.PatchChromiumFlagsJSONBody{Flags: []string{"--kiosk"}} + blob, err := json.Marshal(fl) + require.NoError(t, err) + if err := w.WriteField("chromium_flags", string(blob)); err != nil { + return err + } + } + + if bits&matExtension != 0 { + part, err := w.CreateFormFile("extensions.zip_file", "powerset-ext.zip") + if err != nil { + return err + } + if _, err := io.Copy(part, bytes.NewReader(extZip)); err != nil { + return err + } + if err := w.WriteField("extensions.name", "powerset"); err != nil { + return err + } + } + + if bits&matStartURL != 0 { + if err := w.WriteField("start_url", `https://example.com/`); err != nil { + return err + } + } + return nil +} + +func intPtr(i int) *int { return &i } + +// TestChromiumConfigureMultistartJSONObject exercises JSON start_url variant (wait_until JSON). +func TestChromiumConfigureStartURLJSONObject(t *testing.T) { + t.Parallel() + + if _, err := exec.LookPath("docker"); err != nil { + t.Skipf("docker not available: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute) + defer cancel() + + c := NewTestContainer(t, headlessImage) + require.NoError(t, c.Start(ctx, ContainerConfig{ + Env: map[string]string{"WIDTH": "1024", "HEIGHT": "768"}, + })) + defer c.Stop(ctx) + require.NoError(t, c.WaitReady(ctx)) + + payload := map[string]string{ + "url": "https://example.com/", + "wait_until": "domcontentloaded", + } + raw, err := json.Marshal(payload) + require.NoError(t, err) + + var buf bytes.Buffer + mw := multipart.NewWriter(&buf) + require.NoError(t, mw.WriteField("start_url", string(raw))) + require.NoError(t, mw.Close()) + + client, err := c.APIClient() + require.NoError(t, err) + + rsp, err := client.ChromiumConfigureWithBodyWithResponse(ctx, mw.FormDataContentType(), io.NopCloser(bytes.NewReader(buf.Bytes()))) + require.NoError(t, err) + require.Equal(t, http.StatusOK, rsp.StatusCode(), "%s", string(rsp.Body)) + require.True(t, rsp.JSON200.Ok) +} diff --git a/server/e2e/e2e_chromium_configure_test.go b/server/e2e/e2e_chromium_configure_test.go new file mode 100644 index 00000000..6d4811f2 --- /dev/null +++ b/server/e2e/e2e_chromium_configure_test.go @@ -0,0 +1,51 @@ +package e2e + +import ( + "bytes" + "context" + "io" + "mime/multipart" + "net/http" + "os/exec" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestChromiumConfigureStartURLBare(t *testing.T) { + t.Parallel() + + if _, err := exec.LookPath("docker"); err != nil { + t.Skipf("docker not available: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + c := NewTestContainer(t, headlessImage) + require.NoError(t, c.Start(ctx, ContainerConfig{ + Env: map[string]string{ + "WIDTH": "1024", + "HEIGHT": "768", + }, + }), "failed to start container") + defer c.Stop(ctx) + + require.NoError(t, c.WaitReady(ctx)) + + client, err := c.APIClient() + require.NoError(t, err) + + var buf bytes.Buffer + w := multipart.NewWriter(&buf) + require.NoError(t, w.WriteField("start_url", `https://example.com/`)) + require.NoError(t, w.Close()) + + rsp, err := client.ChromiumConfigureWithBodyWithResponse(ctx, w.FormDataContentType(), io.NopCloser(&buf)) + require.NoError(t, err) + + require.Equal(t, http.StatusOK, rsp.StatusCode(), "unexpected status=%s body=%s", rsp.Status(), string(rsp.Body)) + require.NotNil(t, rsp.JSON200, "want ok json") + require.True(t, rsp.JSON200.Ok) +} diff --git a/server/lib/cdpclient/cdpclient.go b/server/lib/cdpclient/cdpclient.go index eff95a00..6692db88 100644 --- a/server/lib/cdpclient/cdpclient.go +++ b/server/lib/cdpclient/cdpclient.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "sync/atomic" + "time" "github.com/coder/websocket" ) @@ -98,6 +99,131 @@ func (c *Client) send(ctx context.Context, method string, params any, sessionID } } +// NavigateWaitUntil controls how long NavigateFirstPage waits after Page.navigate. +type NavigateWaitUntil string + +const ( + NavigateWaitLoad NavigateWaitUntil = "load" + NavigateWaitDOMContentLoaded NavigateWaitUntil = "domcontentloaded" +) + +// NavigateFirstPage attaches to the first page target, navigates to url, and optionally +// waits for load or DOMContentLoaded. Uses a browser-level WebSocket (flattened session). +func NavigateFirstPage(ctx context.Context, devtoolsURL, url string, waitUntil NavigateWaitUntil) error { + c, err := Dial(ctx, devtoolsURL) + if err != nil { + return fmt.Errorf("dial devtools: %w", err) + } + defer c.Close() + + targetsResult, err := c.send(ctx, "Target.getTargets", nil, "") + if err != nil { + return fmt.Errorf("Target.getTargets: %w", err) + } + + var targets struct { + TargetInfos []struct { + TargetID string `json:"targetId"` + Type string `json:"type"` + } `json:"targetInfos"` + } + if err := json.Unmarshal(targetsResult, &targets); err != nil { + return fmt.Errorf("unmarshal targets: %w", err) + } + + var pageTargetID string + for _, t := range targets.TargetInfos { + if t.Type == "page" { + pageTargetID = t.TargetID + break + } + } + if pageTargetID == "" { + return fmt.Errorf("no page target found") + } + + attachResult, err := c.send(ctx, "Target.attachToTarget", map[string]any{ + "targetId": pageTargetID, + "flatten": true, + }, "") + if err != nil { + return fmt.Errorf("Target.attachToTarget: %w", err) + } + + var attach struct { + SessionID string `json:"sessionId"` + } + if err := json.Unmarshal(attachResult, &attach); err != nil { + return fmt.Errorf("unmarshal attach: %w", err) + } + sess := attach.SessionID + defer func() { + detachCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _, _ = c.send(detachCtx, "Target.detachFromTarget", map[string]any{ + "sessionId": sess, + }, "") + }() + + if _, err := c.send(ctx, "Page.enable", map[string]any{}, sess); err != nil { + return fmt.Errorf("Page.enable: %w", err) + } + + wantEvents := map[string]struct{}{} + switch waitUntil { + case NavigateWaitDOMContentLoaded: + wantEvents["Page.domContentEventFired"] = struct{}{} + case NavigateWaitLoad, "": + wantEvents["Page.loadEventFired"] = struct{}{} + default: + return fmt.Errorf("unsupported wait_until: %q", waitUntil) + } + + // Only one goroutine may read from the WebSocket — never overlap send()'s Read + // loop with waitForPageEvents. + if _, err := c.send(ctx, "Page.navigate", map[string]any{"url": url}, sess); err != nil { + return fmt.Errorf("Page.navigate: %w", err) + } + if err := c.waitForPageEvents(ctx, sess, wantEvents); err != nil { + return err + } + + return nil +} + +func (c *Client) waitForPageEvents(ctx context.Context, sessionID string, want map[string]struct{}) error { + for { + select { + case <-ctx.Done(): + return fmt.Errorf("wait for navigation event: %w", ctx.Err()) + default: + } + + _, msg, err := c.conn.Read(ctx) + if err != nil { + return fmt.Errorf("read cdp: %w", err) + } + + var envelope struct { + Method string `json:"method"` + SessionID string `json:"sessionId"` + Params json.RawMessage `json:"params"` + } + if err := json.Unmarshal(msg, &envelope); err != nil { + continue + } + if envelope.Method == "" { + continue + } + if envelope.SessionID != sessionID { + continue + } + if _, ok := want[envelope.Method]; ok { + return nil + } + } +} + // SetDeviceMetricsOverride sets the viewport dimensions on the first page // target found in the browser. It attaches to the target with a flattened // session, sends Emulation.setDeviceMetricsOverride, then detaches. diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 2269d704..4afa5326 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -437,6 +437,24 @@ func (e BrowserTargetType) Valid() bool { } } +// Defines values for ChromiumConfigureErrorPhase. +const ( + ConfigurePhase ChromiumConfigureErrorPhase = "configure_phase" + NavigatePhase ChromiumConfigureErrorPhase = "navigate_phase" +) + +// Valid indicates whether the value is a known member of the ChromiumConfigureErrorPhase enum. +func (e ChromiumConfigureErrorPhase) Valid() bool { + switch e { + case ConfigurePhase: + return true + case NavigatePhase: + return true + default: + return false + } +} + // Defines values for ClickMouseRequestButton. const ( ClickMouseRequestButtonBack ClickMouseRequestButton = "back" @@ -1817,6 +1835,17 @@ type BrowserTelemetryConfig struct { Browser *BrowserTelemetryCategoriesConfig `json:"browser,omitempty"` } +// ChromiumConfigureError Failure from batched chromium configure — includes which phase failed. +type ChromiumConfigureError struct { + Message string `json:"message"` + + // Phase configure_phase maps to restart/filesystem/policy/extension/profile/display work; navigate_phase maps to Page.navigate after readiness. + Phase ChromiumConfigureErrorPhase `json:"phase"` +} + +// ChromiumConfigureErrorPhase configure_phase maps to restart/filesystem/policy/extension/profile/display work; navigate_phase maps to Page.navigate after readiness. +type ChromiumConfigureErrorPhase string + // ClickMouseRequest defines model for ClickMouseRequest. type ClickMouseRequest struct { // Button Mouse button to interact with @@ -2465,6 +2494,33 @@ type InternalError = Error // NotFoundError defines model for NotFoundError. type NotFoundError = Error +// ChromiumConfigureMultipartBody defines parameters for ChromiumConfigure. +type ChromiumConfigureMultipartBody struct { + // ChromePolicies UTF-8 JSON policy override map — same semantics as PATCH /chromium/policies. + ChromePolicies *string `json:"chrome_policies,omitempty"` + + // ChromiumFlags UTF-8 JSON object `{"flags":["--kiosk"]}` — same semantics as PATCH /chromium/flags. + ChromiumFlags *string `json:"chromium_flags,omitempty"` + + // Display UTF-8 JSON object matching `#/components/schemas/PatchDisplayRequest` (width/height/etc.). + Display *string `json:"display,omitempty"` + + // Extensions Extension zips paired with consecutive extensions.name fields (same as upload-extensions-and-restart). + Extensions *[]struct { + Name string `json:"name"` + ZipFile openapi_types.File `json:"zip_file"` + } `json:"extensions,omitempty"` + + // ProfileArchive tar.zst of `/home/kernel/user-data` (V2 profiles). Stripped paths use strip_components optional part. + ProfileArchive *openapi_types.File `json:"profile_archive,omitempty"` + + // StartUrl Bare https? URL text, OR UTF-8 JSON `{"url":"...", "wait_until":"load"|"domcontentloaded"}`. + StartUrl *string `json:"start_url,omitempty"` + + // StripComponents Leading path components to strip when extracting profile_archive (non-negative integer as text). + StripComponents *string `json:"strip_components,omitempty"` +} + // PatchChromiumFlagsJSONBody defines parameters for PatchChromiumFlags. type PatchChromiumFlagsJSONBody struct { // Flags Chromium flags to merge (e.g., ["--kiosk", "--disable-gpu"]) @@ -2590,6 +2646,9 @@ type StreamTelemetryEventsParams struct { LastEventID *string `json:"Last-Event-ID,omitempty"` } +// ChromiumConfigureMultipartRequestBody defines body for ChromiumConfigure for multipart/form-data ContentType. +type ChromiumConfigureMultipartRequestBody ChromiumConfigureMultipartBody + // PatchChromiumFlagsJSONRequestBody defines body for PatchChromiumFlags for application/json ContentType. type PatchChromiumFlagsJSONRequestBody PatchChromiumFlagsJSONBody @@ -3457,6 +3516,9 @@ func WithRequestEditorFn(fn RequestEditorFn) ClientOption { // The interface specification for the client above. type ClientInterface interface { + // ChromiumConfigureWithBody request with any body + ChromiumConfigureWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + // PatchChromiumFlagsWithBody request with any body PatchChromiumFlagsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -3682,6 +3744,18 @@ type ClientInterface interface { StreamTelemetryEvents(ctx context.Context, params *StreamTelemetryEventsParams, reqEditors ...RequestEditorFn) (*http.Response, error) } +func (c *Client) ChromiumConfigureWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewChromiumConfigureRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) PatchChromiumFlagsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewPatchChromiumFlagsRequestWithBody(c.Server, contentType, body) if err != nil { @@ -4702,6 +4776,35 @@ func (c *Client) StreamTelemetryEvents(ctx context.Context, params *StreamTeleme return c.Client.Do(req) } +// NewChromiumConfigureRequestWithBody generates requests for ChromiumConfigure with any type of body +func NewChromiumConfigureRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/chromium/configure") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewPatchChromiumFlagsRequest calls the generic PatchChromiumFlags builder with application/json body func NewPatchChromiumFlagsRequest(server string, body PatchChromiumFlagsJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -6908,6 +7011,9 @@ func WithBaseURL(baseURL string) ClientOption { // ClientWithResponsesInterface is the interface specification for the client with responses above. type ClientWithResponsesInterface interface { + // ChromiumConfigureWithBodyWithResponse request with any body + ChromiumConfigureWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ChromiumConfigureResponse, error) + // PatchChromiumFlagsWithBodyWithResponse request with any body PatchChromiumFlagsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchChromiumFlagsResponse, error) @@ -7133,6 +7239,31 @@ type ClientWithResponsesInterface interface { StreamTelemetryEventsWithResponse(ctx context.Context, params *StreamTelemetryEventsParams, reqEditors ...RequestEditorFn) (*StreamTelemetryEventsResponse, error) } +type ChromiumConfigureResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *OkResponse + JSON400 *BadRequestError + JSON409 *ConflictError + JSON500 *ChromiumConfigureError +} + +// Status returns HTTPResponse.Status +func (r ChromiumConfigureResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ChromiumConfigureResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type PatchChromiumFlagsResponse struct { Body []byte HTTPResponse *http.Response @@ -8413,6 +8544,15 @@ func (r StreamTelemetryEventsResponse) StatusCode() int { return 0 } +// ChromiumConfigureWithBodyWithResponse request with arbitrary body returning *ChromiumConfigureResponse +func (c *ClientWithResponses) ChromiumConfigureWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ChromiumConfigureResponse, error) { + rsp, err := c.ChromiumConfigureWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseChromiumConfigureResponse(rsp) +} + // PatchChromiumFlagsWithBodyWithResponse request with arbitrary body returning *PatchChromiumFlagsResponse func (c *ClientWithResponses) PatchChromiumFlagsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchChromiumFlagsResponse, error) { rsp, err := c.PatchChromiumFlagsWithBody(ctx, contentType, body, reqEditors...) @@ -9147,6 +9287,53 @@ func (c *ClientWithResponses) StreamTelemetryEventsWithResponse(ctx context.Cont return ParseStreamTelemetryEventsResponse(rsp) } +// ParseChromiumConfigureResponse parses an HTTP response from a ChromiumConfigureWithResponse call +func ParseChromiumConfigureResponse(rsp *http.Response) (*ChromiumConfigureResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ChromiumConfigureResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest OkResponse + 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 == 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 == 409: + var dest ConflictError + 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 ChromiumConfigureError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParsePatchChromiumFlagsResponse parses an HTTP response from a PatchChromiumFlagsWithResponse call func ParsePatchChromiumFlagsResponse(rsp *http.Response) (*PatchChromiumFlagsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -11173,6 +11360,9 @@ func ParseStreamTelemetryEventsResponse(rsp *http.Response) (*StreamTelemetryEve // ServerInterface represents all server handlers. type ServerInterface interface { + // Apply batched Chromium filesystem and launch configuration plus optional navigation + // (POST /chromium/configure) + ChromiumConfigure(w http.ResponseWriter, r *http.Request) // Update Chromium launch flags and restart // (PATCH /chromium/flags) PatchChromiumFlags(w http.ResponseWriter, r *http.Request) @@ -11341,6 +11531,12 @@ type ServerInterface interface { type Unimplemented struct{} +// Apply batched Chromium filesystem and launch configuration plus optional navigation +// (POST /chromium/configure) +func (_ Unimplemented) ChromiumConfigure(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Update Chromium launch flags and restart // (PATCH /chromium/flags) func (_ Unimplemented) PatchChromiumFlags(w http.ResponseWriter, r *http.Request) { @@ -11674,6 +11870,20 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(http.Handler) http.Handler +// ChromiumConfigure operation middleware +func (siw *ServerInterfaceWrapper) ChromiumConfigure(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ChromiumConfigure(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // PatchChromiumFlags operation middleware func (siw *ServerInterfaceWrapper) PatchChromiumFlags(w http.ResponseWriter, r *http.Request) { @@ -12839,6 +13049,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl ErrorHandlerFunc: options.ErrorHandlerFunc, } + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/chromium/configure", wrapper.ChromiumConfigure) + }) r.Group(func(r chi.Router) { r.Patch(options.BaseURL+"/chromium/flags", wrapper.PatchChromiumFlags) }) @@ -13013,6 +13226,50 @@ type InternalErrorJSONResponse Error type NotFoundErrorJSONResponse Error +type ChromiumConfigureRequestObject struct { + Body *multipart.Reader +} + +type ChromiumConfigureResponseObject interface { + VisitChromiumConfigureResponse(w http.ResponseWriter) error +} + +type ChromiumConfigure200JSONResponse OkResponse + +func (response ChromiumConfigure200JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ChromiumConfigure400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response ChromiumConfigure400JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type ChromiumConfigure409JSONResponse struct{ ConflictErrorJSONResponse } + +func (response ChromiumConfigure409JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + +type ChromiumConfigure500JSONResponse ChromiumConfigureError + +func (response ChromiumConfigure500JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type PatchChromiumFlagsRequestObject struct { Body *PatchChromiumFlagsJSONRequestBody } @@ -15256,6 +15513,9 @@ func (response StreamTelemetryEvents200TexteventStreamResponse) VisitStreamTelem // StrictServerInterface represents all server handlers. type StrictServerInterface interface { + // Apply batched Chromium filesystem and launch configuration plus optional navigation + // (POST /chromium/configure) + ChromiumConfigure(ctx context.Context, request ChromiumConfigureRequestObject) (ChromiumConfigureResponseObject, error) // Update Chromium launch flags and restart // (PATCH /chromium/flags) PatchChromiumFlags(ctx context.Context, request PatchChromiumFlagsRequestObject) (PatchChromiumFlagsResponseObject, error) @@ -15449,6 +15709,37 @@ type strictHandler struct { options StrictHTTPServerOptions } +// ChromiumConfigure operation middleware +func (sh *strictHandler) ChromiumConfigure(w http.ResponseWriter, r *http.Request) { + var request ChromiumConfigureRequestObject + + if reader, err := r.MultipartReader(); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode multipart body: %w", err)) + return + } else { + request.Body = reader + } + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ChromiumConfigure(ctx, request.(ChromiumConfigureRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ChromiumConfigure") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ChromiumConfigureResponseObject); ok { + if err := validResponse.VisitChromiumConfigureResponse(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)) + } +} + // PatchChromiumFlags operation middleware func (sh *strictHandler) PatchChromiumFlags(w http.ResponseWriter, r *http.Request) { var request PatchChromiumFlagsRequestObject @@ -17042,283 +17333,296 @@ func (sh *strictHandler) StreamTelemetryEvents(w http.ResponseWriter, r *http.Re // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "/wEAAP//c4aJWK9pAQA=", + "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", + "AsYNO8cHwcG5YxLPlq71/x8AAP//rDom6iV1AQA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 1c913ed1..ab2d75b7 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1169,6 +1169,79 @@ paths: $ref: "#/components/responses/BadRequestError" "500": $ref: "#/components/responses/InternalError" + + /chromium/configure: + post: + summary: Apply batched Chromium filesystem and launch configuration plus optional navigation + description: | + Optional multipart parts apply configuration while Chromium stays stopped once (policy, + flags, extensions, profile archive, optional display sizing), then Chromium is started exactly + once and DevTools readiness is awaited. Optional `start_url` applies a Page.navigate via CDP + after readiness. Omit any part you do not need. At least one actionable part must be present. + Prefer this over separate `/chromium/*` and `/display` calls when multiple restart-triggering + steps apply in one session configure. + operationId: chromiumConfigure + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + display: + description: >- + UTF-8 JSON object matching `#/components/schemas/PatchDisplayRequest` (width/height/etc.). + type: string + chromium_flags: + description: >- + UTF-8 JSON object `{"flags":["--kiosk"]}` — same semantics as PATCH /chromium/flags. + type: string + chrome_policies: + description: UTF-8 JSON policy override map — same semantics as PATCH /chromium/policies. + type: string + profile_archive: + description: >- + tar.zst of `/home/kernel/user-data` (V2 profiles). Stripped paths use strip_components optional part. + type: string + format: binary + strip_components: + description: Leading path components to strip when extracting profile_archive (non-negative integer as text). + type: string + start_url: + description: >- + Bare https? URL text, OR UTF-8 JSON `{"url":"...", "wait_until":"load"|"domcontentloaded"}`. + type: string + extensions: + type: array + description: Extension zips paired with consecutive extensions.name fields (same as upload-extensions-and-restart). + items: + type: object + properties: + zip_file: + type: string + format: binary + name: + type: string + pattern: "^[A-Za-z0-9._-]{1,255}$" + required: [zip_file, name] + responses: + "200": + description: Configuration applied; optional navigate completed successfully. + content: + application/json: + schema: + $ref: "#/components/schemas/OkResponse" + "400": + $ref: "#/components/responses/BadRequestError" + "409": + $ref: "#/components/responses/ConflictError" + "500": + description: Configure or navigate failure with structured phase + content: + application/json: + schema: + $ref: "#/components/schemas/ChromiumConfigureError" + /playwright/execute: post: summary: Execute Playwright/TypeScript code against the browser @@ -2491,6 +2564,20 @@ components: properties: message: type: string + ChromiumConfigureError: + type: object + description: Failure from batched chromium configure — includes which phase failed. + required: [phase, message] + additionalProperties: false + properties: + phase: + type: string + enum: + - configure_phase + - navigate_phase + description: configure_phase maps to restart/filesystem/policy/extension/profile/display work; navigate_phase maps to Page.navigate after readiness. + message: + type: string RecorderInfo: type: object required: [id, isRecording] From 46410dbe4c34172194b2fe187e5e939352456f74 Mon Sep 17 00:00:00 2001 From: hiroTamada Date: Tue, 19 May 2026 13:41:22 -0400 Subject: [PATCH 02/12] Remove local-dev helpers not required for chromium/configure. Drop DETACHED run-docker mode, the powerset runner script, and related AGENTS.md notes. Co-authored-by: Cursor --- AGENTS.md | 2 - images/chromium-headless/run-docker.sh | 19 +------- .../run-local-chromium-configure-powerset.sh | 43 ------------------- 3 files changed, 1 insertion(+), 63 deletions(-) delete mode 100755 scripts/run-local-chromium-configure-powerset.sh diff --git a/AGENTS.md b/AGENTS.md index 8b19b6df..cdeccf59 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,8 +14,6 @@ Kernel Images is a sandboxed cloud browser infrastructure platform. The Go serve - **Build server**: `cd server && make build` - **Build headless image**: `cd /workspace && DOCKER_BUILDKIT=1 docker build -f images/chromium-headless/image/Dockerfile -t kernel-headless-test .` - **Run headless container**: `docker run -d --name kernel-headless -p 10001:10001 -p 9222:9222 --shm-size=2g kernel-headless-test` -- **Run headless (repo scripts)** — foreground: `./images/chromium-headless/run-docker.sh` maps API to host **`:444`**; background: `DETACHED=1 ./images/chromium-headless/run-docker.sh` -- **Chromium configure multipart permutation e2e** (31 part combinations — rebuild image first): `./scripts/run-local-chromium-configure-powerset.sh` - See `server/README.md` and `server/Makefile` for additional commands and configuration. ### Docker in Cloud VM diff --git a/images/chromium-headless/run-docker.sh b/images/chromium-headless/run-docker.sh index a31f0425..4a670748 100755 --- a/images/chromium-headless/run-docker.sh +++ b/images/chromium-headless/run-docker.sh @@ -10,12 +10,6 @@ source ../../shared/ensure-common-build-run-vars.sh chromium-headless HOST_RECORDINGS_DIR="$SCRIPT_DIR/recordings" mkdir -p "$HOST_RECORDINGS_DIR" -# DETACHED=1 runs in background (--rm cleans up container on stop/failure teardown without -it) -DOCKER_RUN_FRONT=(-it --rm) -if [[ "${DETACHED:-}" == "1" ]]; then - DOCKER_RUN_FRONT=(-d --rm) -fi - RUN_ARGS=( --name "$NAME" --privileged @@ -48,15 +42,4 @@ if [[ $# -ge 1 && -n "$1" ]]; then fi docker rm -f "$NAME" 2>/dev/null || true - -if [[ "${DETACHED:-}" == "1" ]]; then - echo "Detached mode: Kernel HTTP API mapped to http://127.0.0.1:444 (container port 10001)" - echo "CDP WS proxy http://127.0.0.1:9222 ChromeDriver proxy http://127.0.0.1:9224" -fi - -docker run "${DOCKER_RUN_FRONT[@]}" "${ENTRYPOINT_ARG[@]}" "${RUN_ARGS[@]}" "$IMAGE" - -if [[ "${DETACHED:-}" == "1" ]]; then - echo "Container id/name: $NAME" - echo "Stop with: docker stop $NAME" -fi +docker run -it --rm "${ENTRYPOINT_ARG[@]}" "${RUN_ARGS[@]}" "$IMAGE" diff --git a/scripts/run-local-chromium-configure-powerset.sh b/scripts/run-local-chromium-configure-powerset.sh deleted file mode 100755 index 3eab1283..00000000 --- a/scripts/run-local-chromium-configure-powerset.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash -# 1) Build the headless chromium image via images/chromium-headless/build-docker.sh -# 2) Run chromium/configure multipart powerset e2e (31 part combinations + JSON start_url + legacy bare-start_url test). -# -# Prereqs: Docker, Go, network for pulls. -# -# Tests use testcontainers-go (dynamic host ports). -# For manual experiments: DETACHED=1 ./images/chromium-headless/run-docker.sh → API on http://127.0.0.1:444 -# -# Usage: -# ./scripts/run-local-chromium-configure-powerset.sh -# IMAGE=onkernel/chromium-headless-test:mytag ./scripts/run-local-chromium-configure-powerset.sh -# -# Skip image rebuild: -# ./scripts/run-local-chromium-configure-powerset.sh --skip-build - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -SKIP_BUILD=0 -for arg in "$@"; do - case "$arg" in - --skip-build) SKIP_BUILD=1 ;; - *) - echo "unknown arg: $arg" >&2 - exit 1 - ;; - esac -done - -IMAGE="${IMAGE:-onkernel/chromium-headless-test:latest}" -export E2E_CHROMIUM_HEADLESS_IMAGE="$IMAGE" - -if [[ "$SKIP_BUILD" == "0" ]]; then - (cd "$ROOT/images/chromium-headless" && IMAGE="$IMAGE" ./build-docker.sh) -fi - -echo "Running chromium/configure permutation tests against image $E2E_CHROMIUM_HEADLESS_IMAGE ..." -(cd "$ROOT/server" && - go test ./e2e -count=1 -timeout 120m -v \ - -run 'TestChromiumConfigure(StartURLBare|MultipartPowerset|StartURLJSONObject)$') From 2f7e5d6638b65ae286efa7fc9063914ee2daf77d Mon Sep 17 00:00:00 2001 From: hiroTamada Date: Wed, 20 May 2026 22:53:44 -0400 Subject: [PATCH 03/12] Align chromium configure with control-plane semantics. Batching keeps a single Chromium restart while preserving Kernel's configure order, propagating required-step failures, and keeping start_url best-effort. Co-authored-by: Cursor --- server/cmd/api/api/chromium.go | 20 +- server/cmd/api/api/chromium_configure.go | 477 ++++++++++++------ server/cmd/api/api/chromium_configure_test.go | 134 ++++- .../e2e_chromium_configure_powerset_test.go | 36 +- server/e2e/e2e_chromium_configure_test.go | 17 +- server/lib/cdpclient/cdpclient.go | 90 +++- server/lib/oapi/oapi.go | 5 +- server/openapi.yaml | 29 +- 8 files changed, 613 insertions(+), 195 deletions(-) diff --git a/server/cmd/api/api/chromium.go b/server/cmd/api/api/chromium.go index a8040905..01a910a7 100644 --- a/server/cmd/api/api/chromium.go +++ b/server/cmd/api/api/chromium.go @@ -168,6 +168,9 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap func (s *ApiService) applyExtensionZipItems(ctx context.Context, items []extensionZipItem) (reqMsg string, err error) { log := logger.FromContext(ctx) extBase := "/home/kernel/extensions" + if err := os.MkdirAll(extBase, 0o755); err != nil { + return "", fmt.Errorf("failed to create extension base dir: %w", err) + } for _, p := range items { dest := filepath.Join(extBase, p.name) @@ -179,12 +182,26 @@ func (s *ApiService) applyExtensionZipItems(ctx context.Context, items []extensi } } + var createdDests []string + success := false + defer func() { + if success { + return + } + for _, dest := range createdDests { + if removeErr := os.RemoveAll(dest); removeErr != nil { + log.Warn("failed to clean up partial extension dir", "error", removeErr, "dest", dest) + } + } + }() + for _, p := range items { dest := filepath.Join(extBase, p.name) if err := os.MkdirAll(dest, 0o755); err != nil { log.Error("failed to create extension dir", "error", err) return "", fmt.Errorf("failed to create extension dir: %w", err) } + createdDests = append(createdDests, dest) if err := ziputil.Unzip(p.zipTemp, dest); err != nil { log.Error("failed to unzip zip file", "error", err) return "invalid zip file", nil @@ -280,6 +297,7 @@ func (s *ApiService) applyExtensionZipItems(ctx context.Context, items []extensi return "", err } + success = true return "", nil } @@ -338,7 +356,7 @@ func (s *ApiService) restartChromiumAndWait(ctx context.Context, operation strin go func() { cmdCtx, cancelCmd := context.WithTimeout(context.WithoutCancel(ctx), 1*time.Minute) defer cancelCmd() - out, err := exec.CommandContext(cmdCtx, "supervisorctl", "-c", "/etc/supervisor/supervisord.conf", "restart", "chromium").CombinedOutput() + out, err := exec.CommandContext(cmdCtx, "supervisorctl", supervisorctlArgv("restart", "chromium")...).CombinedOutput() if err != nil { log.Error("failed to restart chromium", "error", err, "out", string(out)) errCh <- fmt.Errorf("supervisorctl restart failed: %w", err) diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go index ad6a68a0..5c5b8e92 100644 --- a/server/cmd/api/api/chromium_configure.go +++ b/server/cmd/api/api/chromium_configure.go @@ -3,12 +3,14 @@ package api import ( "context" "encoding/json" + "errors" "fmt" "io" "mime/multipart" "net/url" "os" "os/exec" + "path/filepath" "strconv" "strings" "time" @@ -21,10 +23,12 @@ import ( ) const userDataProfileDir = "/home/kernel/user-data" +const maxStartURLLen = 2048 +const startURLDispatchTimeout = 3 * time.Second type chromiumConfigureState struct { - displayJSON *string - chromiumFlagsJSON *string + displayJSON *string + chromiumFlagsJSON *string chromePoliciesJSON *string stripComponents int @@ -46,7 +50,7 @@ func (st *chromiumConfigureState) cleanup() { } // ChromiumConfigure batched Chromium/session configuration plus optional navigation. -func (s *ApiService) ChromiumConfigure(ctx context.Context, request oapi.ChromiumConfigureRequestObject) (oapi.ChromiumConfigureResponseObject, error) { +func (s *ApiService) ChromiumConfigure(ctx context.Context, request oapi.ChromiumConfigureRequestObject) (resp oapi.ChromiumConfigureResponseObject, err error) { start := time.Now() if request.Body == nil { @@ -60,87 +64,116 @@ func (s *ApiService) ChromiumConfigure(ctx context.Context, request oapi.Chromiu } defer st.cleanup() - if cfgActionables(st)+cfgHasStartURL(st.startURLRaw) == 0 { + spec, msgs := chromiumStartURLSpec(st.startURLRaw) + if msgs != "" { + return cfg400(msgs), nil + } + + if cfgActionables(st)+cfgHasStartURLSpec(spec) == 0 { return cfg400("no configuration fields provided"), nil } needsStop := chromiumNeedsStopCycle(st) - - if needsStop { - if chromiumDisplayHasSizedRequest(st.displayJSON) { - stopped, stopErr := s.stopActiveRecordings(ctx) - if stopErr != nil { - return cfg500Configure(fmt.Sprintf("failed to stop recordings: %v", stopErr)), nil - } - if len(stopped) > 0 { - defer func() { - go s.startNewRecordingSegments(context.WithoutCancel(ctx), stopped) - }() + chromiumStopped := false + restartAfterStop := func() error { + if !chromiumStopped { + return nil + } + chromiumStopped = false + return s.startChromiumAndWait(ctx, "batched chromium configure") + } + defer func() { + if restartErr := restartAfterStop(); restartErr != nil { + if resp != nil { + logger.FromContext(ctx).Error("failed to restart chromium after configure error", "error", restartErr) + return } + resp = cfg500ConfigureStep(chromiumConfigureStepStart, restartErr.Error()) + err = nil } + }() + if needsStop { logger.FromContext(ctx).Info("chromium configure (stop/start path)") if err := s.stopChromium(ctx); err != nil { - return cfg500Configure(err.Error()), nil + return cfg500ConfigureStep(chromiumConfigureStepStop, err.Error()), nil } + chromiumStopped = true - if st.hasProfile { - if err := chromiumApplyProfileArchive(st.profileTemp, st.stripComponents); err != nil { - return cfg500Configure(err.Error()), nil - } + policyOverrides, err := chromiumValidatePolicies(st.chromePoliciesJSON) + if err != nil { + return cfgResponseFromStepError(chromiumConfigureStepPolicies, err), nil + } + if err := chromiumApplyPolicies(ctx, s, policyOverrides); err != nil { + return cfgResponseFromStepError(chromiumConfigureStepPolicies, err), nil + } + + if reqMsgs, ierr := chromiumApplyExtensions(ctx, s, st.extItems); reqMsgs != "" { + return cfg400(fmt.Sprintf("%s: %s", chromiumConfigureStepExtensions, reqMsgs)), nil + } else if ierr != nil { + return cfg500ConfigureStep(chromiumConfigureStepExtensions, ierr.Error()), nil } - if chromiumDisplayHasSizedRequest(st.displayJSON) { - b, msgs := chromiumParseDisplayParts(st.displayJSON) - if msgs != "" { - return cfg400(msgs), nil + if st.displayJSON != nil && strings.TrimSpace(*st.displayJSON) != "" { + displayPlan, displayResp := chromiumPrepareDisplay(ctx, s, st.displayJSON) + if displayResp != nil { + return displayResp, nil } - if b != nil { - if rr := chromiumDisplayApplyWhileStopped(ctx, s, b); rr != nil { + if displayPlan != nil { + stopped, stopErr := s.stopActiveRecordings(ctx) + if stopErr != nil { + return cfg500ConfigureStep(chromiumConfigureStepDisplay, fmt.Sprintf("failed to stop recordings: %v", stopErr)), nil + } + if len(stopped) > 0 { + defer func() { + go s.startNewRecordingSegments(context.WithoutCancel(ctx), stopped) + }() + } + if rr := chromiumDisplayApplyWhileStopped(ctx, s, displayPlan); rr != nil { return rr, nil } } } - if msgs := chromiumApplyPolicies(ctx, s, st.chromePoliciesJSON); msgs != "" { - return policyDisposition(msgs), nil + flagsPlan, err := chromiumValidateFlags(st.chromiumFlagsJSON) + if err != nil { + return cfgResponseFromStepError(chromiumConfigureStepFlags, err), nil } - - if reqMsgs, ierr := chromiumApplyExtensions(ctx, s, st.extItems); reqMsgs != "" { - return cfg400(reqMsgs), nil - } else if ierr != nil { - return cfg500Configure(ierr.Error()), nil + if err := chromiumMergeFlags(ctx, s, flagsPlan); err != nil { + return cfgResponseFromStepError(chromiumConfigureStepFlags, err), nil } - if msgs := chromiumMergeFlagsRaw(ctx, s, st.chromiumFlagsJSON); msgs != "" { - if strings.HasPrefix(msgs, "bad:") { - return cfg400(strings.TrimPrefix(msgs, "bad:")), nil + if st.hasProfile { + preparedProfile, cleanupProfile, err := chromiumPrepareProfileArchive(st.profileTemp, st.stripComponents) + if cleanupProfile != nil { + defer cleanupProfile() + } + if err != nil { + return cfg500ConfigureStep(chromiumConfigureStepProfile, err.Error()), nil + } + if err := chromiumInstallPreparedProfile(preparedProfile); err != nil { + return cfg500ConfigureStep(chromiumConfigureStepProfile, err.Error()), nil } - return cfg500Configure(strings.TrimPrefix(msgs, "int:")), nil } - if err := s.startChromiumAndWait(ctx, "batched chromium configure"); err != nil { - return cfg500Configure(err.Error()), nil + if err := restartAfterStop(); err != nil { + return cfg500ConfigureStep(chromiumConfigureStepStart, err.Error()), nil } } else { if st.displayJSON != nil && strings.TrimSpace(*st.displayJSON) != "" { - body, msgs := chromiumParseDisplayParts(st.displayJSON) - if msgs != "" { - return cfg400(msgs), nil + displayPlan, displayResp := chromiumPrepareDisplay(ctx, s, st.displayJSON) + if displayResp != nil { + return displayResp, nil } - if rr := chromiumRunPatchDisplay(ctx, s, body); rr != nil { + if rr := chromiumRunPatchDisplay(ctx, s, displayPlan.body); rr != nil { return rr, nil } } } - spec, msgs := chromiumStartURLSpec(st.startURLRaw) - if msgs != "" { - return cfg400(msgs), nil - } if spec.needsNav { if err := chromiumDoNavigate(ctx, s, spec); err != nil { - return cfg500Navigate(err.Error()), nil + logger.FromContext(ctx).Warn("start_url dispatch failed", "error", err) } } @@ -151,63 +184,45 @@ func (s *ApiService) ChromiumConfigure(ctx context.Context, request oapi.Chromiu type startURLParsed struct { needsNav bool url string - wait cdpclient.NavigateWaitUntil - timeout time.Duration } +type chromiumConfigureStep string + +const ( + chromiumConfigureStepStop chromiumConfigureStep = "stop_chromium" + chromiumConfigureStepStart chromiumConfigureStep = "start_chromium" + chromiumConfigureStepPolicies chromiumConfigureStep = "chrome_policies" + chromiumConfigureStepExtensions chromiumConfigureStep = "extensions" + chromiumConfigureStepDisplay chromiumConfigureStep = "display" + chromiumConfigureStepFlags chromiumConfigureStep = "chromium_flags" + chromiumConfigureStepProfile chromiumConfigureStep = "profile" +) + func chromiumStartURLSpec(raw *string) (startURLParsed, string) { var out startURLParsed - out.timeout = 45 * time.Second - out.wait = cdpclient.NavigateWaitLoad if raw == nil || strings.TrimSpace(*raw) == "" { return out, "" } - s := strings.TrimSpace(*raw) - if strings.HasPrefix(s, "{") { - var v struct { - URL string `json:"url"` - WaitUntil string `json:"wait_until"` - Timeout *int `json:"timeout_sec,omitempty"` - } - if err := json.Unmarshal([]byte(s), &v); err != nil { - return out, "invalid start_url JSON" - } - if strings.TrimSpace(v.URL) == "" { - return out, "start_url JSON requires url" - } - switch strings.TrimSpace(strings.ToLower(v.WaitUntil)) { - case "", "load": - out.wait = cdpclient.NavigateWaitLoad - case "domcontentloaded": - out.wait = cdpclient.NavigateWaitDOMContentLoaded - default: - return out, "wait_until must be load or domcontentloaded" - } - out.url = strings.TrimSpace(v.URL) - if v.Timeout != nil && *v.Timeout > 0 { - out.timeout = time.Duration(*v.Timeout) * time.Second - } - } else { - out.url = s - } - if errMsgs := chromiumValidateNavigateURL(out.url); errMsgs != "" { - return out, errMsgs + if len(*raw) > maxStartURLLen { + return out, fmt.Sprintf("start_url exceeds max length of %d bytes", maxStartURLLen) } + out.url = normalizeStartURL(*raw) out.needsNav = true return out, "" } -func chromiumValidateNavigateURL(u string) string { - parsed, err := url.Parse(u) - if err != nil { - return "invalid start URL" +func normalizeStartURL(rawURL string) string { + rawURL = strings.TrimSpace(rawURL) + if rawURL == "" { + return rawURL } - switch strings.ToLower(parsed.Scheme) { - case "https", "http", "about", "data", "chrome", "devtools": - default: - return fmt.Sprintf("unsupported URL scheme %q", parsed.Scheme) + parsed, err := url.Parse(rawURL) + if err != nil || parsed.Scheme == "" || + strings.Contains(parsed.Scheme, ".") || + strings.EqualFold(parsed.Scheme, "localhost") { + return "https://" + rawURL } - return "" + return rawURL } func chromiumDoNavigate(ctx context.Context, s *ApiService, spec startURLParsed) error { @@ -215,16 +230,47 @@ func chromiumDoNavigate(ctx context.Context, s *ApiService, spec startURLParsed) if upstream == "" { return fmt.Errorf("devtools upstream not available") } - navCtx, cancel := context.WithTimeout(ctx, spec.timeout) + navCtx, cancel := context.WithTimeout(ctx, startURLDispatchTimeout) defer cancel() - return cdpclient.NavigateFirstPage(navCtx, upstream, spec.url, spec.wait) + return cdpclient.DispatchStartURL(navCtx, upstream, spec.url) +} + +type cfgBadRequestError struct { + message string +} + +func (e cfgBadRequestError) Error() string { + return e.message +} + +func cfgBadRequest(msg string) error { + return cfgBadRequestError{message: msg} +} + +func cfgResponseFromStepError(step chromiumConfigureStep, err error) oapi.ChromiumConfigureResponseObject { + var bad cfgBadRequestError + if errors.As(err, &bad) { + return cfg400(fmt.Sprintf("%s: %s", step, bad.message)) + } + return cfg500ConfigureStep(step, err.Error()) +} + +type chromiumFlagsPlan struct { + flags []string +} + +type chromiumDisplayPlan struct { + body *oapi.PatchDisplayJSONRequestBody + width int + height int + refreshRate int } func chromiumNeedsStopCycle(st *chromiumConfigureState) bool { return st.hasProfile || len(st.extItems) > 0 || - policiesContentNonEmpty(st.chromePoliciesJSON) || - flagsContentNonEmpty(st.chromiumFlagsJSON) + policiesNonEmpty(st.chromePoliciesJSON) || + flagsNonEmpty(st.chromiumFlagsJSON) } func policiesContentNonEmpty(s *string) bool { @@ -295,6 +341,15 @@ func cfg500Configure(msg string) oapi.ChromiumConfigure500JSONResponse { }) } +func cfg500ConfigureStep(step chromiumConfigureStep, msg string) oapi.ChromiumConfigure500JSONResponse { + stepValue := string(step) + return oapi.ChromiumConfigure500JSONResponse(oapi.ChromiumConfigureError{ + Phase: oapi.ConfigurePhase, + Step: &stepValue, + Message: msg, + }) +} + func cfg500Navigate(msg string) oapi.ChromiumConfigure500JSONResponse { return oapi.ChromiumConfigure500JSONResponse(oapi.ChromiumConfigureError{ Phase: oapi.NavigatePhase, @@ -307,7 +362,7 @@ func cfgActionables(st *chromiumConfigureState) int { if policiesContentNonEmpty(st.chromePoliciesJSON) { n++ } - if flagsContentNonEmpty(st.chromiumFlagsJSON) { + if flagsNonEmpty(st.chromiumFlagsJSON) { n++ } if len(st.extItems) > 0 { @@ -316,14 +371,14 @@ func cfgActionables(st *chromiumConfigureState) int { if st.hasProfile { n++ } - if chromiumDisplayHasSizedRequest(st.displayJSON) { + if st.displayJSON != nil && strings.TrimSpace(*st.displayJSON) != "" { n++ } return n } -func cfgHasStartURL(s *string) int { - if s == nil || strings.TrimSpace(*s) == "" { +func cfgHasStartURLSpec(spec startURLParsed) int { + if !spec.needsNav { return 0 } return 1 @@ -343,6 +398,7 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) str gotZip bool } var cur *pend + var gotDisplay, gotChromiumFlags, gotChromePolicies, gotStripComponents, gotProfileArchive, gotStartURL bool for { part, err := mr.NextPart() @@ -354,6 +410,10 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) str } switch name := part.FormName(); name { case "display": + if gotDisplay { + return "duplicate display field" + } + gotDisplay = true b, err := io.ReadAll(part) if err != nil { return "read display field" @@ -361,6 +421,10 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) str v := strings.TrimSpace(string(b)) st.displayJSON = &v case "chromium_flags": + if gotChromiumFlags { + return "duplicate chromium_flags field" + } + gotChromiumFlags = true b, err := io.ReadAll(part) if err != nil { return "read chromium_flags field" @@ -368,6 +432,10 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) str v := string(b) st.chromiumFlagsJSON = &v case "chrome_policies": + if gotChromePolicies { + return "duplicate chrome_policies field" + } + gotChromePolicies = true b, err := io.ReadAll(part) if err != nil { return "read chrome_policies field" @@ -375,14 +443,24 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) str v := string(b) st.chromePoliciesJSON = &v case "strip_components": + if gotStripComponents { + return "duplicate strip_components field" + } + gotStripComponents = true b, err := io.ReadAll(part) if err != nil { return "read strip_components" } - if n, err := strconv.Atoi(strings.TrimSpace(string(b))); err == nil && n >= 0 { - st.stripComponents = n + n, err := strconv.Atoi(strings.TrimSpace(string(b))) + if err != nil || n < 0 { + return "strip_components must be a non-negative integer" } + st.stripComponents = n case "profile_archive": + if gotProfileArchive { + return "duplicate profile_archive field" + } + gotProfileArchive = true tmp, err := os.CreateTemp("", "bcc-prof-*.tar.zst") if err != nil { return "temp profile_archive" @@ -398,6 +476,10 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) str st.profileTemp = tmp.Name() st.hasProfile = true case "start_url": + if gotStartURL { + return "duplicate start_url field" + } + gotStartURL = true b, err := io.ReadAll(part) if err != nil { return "read start_url" @@ -455,24 +537,62 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) str return "" } -func chromiumApplyProfileArchive(profilePath string, strip int) error { - if err := os.RemoveAll(userDataProfileDir); err != nil { - return fmt.Errorf("clear user-data: %w", err) +func chromiumPrepareProfileArchive(profilePath string, strip int) (preparedDir string, cleanup func(), err error) { + parent := filepath.Dir(userDataProfileDir) + if err := os.MkdirAll(parent, 0o755); err != nil { + return "", nil, fmt.Errorf("mkdir user-data parent: %w", err) } - if err := os.MkdirAll(userDataProfileDir, 0o755); err != nil { - return fmt.Errorf("mkdir user-data: %w", err) + preparedDir, err = os.MkdirTemp(parent, ".user-data-new-*") + if err != nil { + return "", nil, fmt.Errorf("create temp user-data dir: %w", err) + } + cleanup = func() { + _ = os.RemoveAll(preparedDir) } + f, err := os.Open(profilePath) if err != nil { - return err + cleanup() + return "", nil, err } defer f.Close() - if err := zstdutil.UntarZstd(f, userDataProfileDir, strip); err != nil { - return fmt.Errorf("extract profile archive: %w", err) + if err := zstdutil.UntarZstd(f, preparedDir, strip); err != nil { + cleanup() + return "", nil, fmt.Errorf("extract profile archive: %w", err) } - out, err := exec.Command("chown", "-R", "kernel:kernel", userDataProfileDir).CombinedOutput() + out, err := exec.Command("chown", "-R", "kernel:kernel", preparedDir).CombinedOutput() if err != nil { - return fmt.Errorf("chown user-data: %w (%s)", err, string(out)) + cleanup() + return "", nil, fmt.Errorf("chown user-data: %w (%s)", err, string(out)) + } + return preparedDir, cleanup, nil +} + +func chromiumInstallPreparedProfile(preparedDir string) error { + if preparedDir == "" { + return nil + } + parent := filepath.Dir(userDataProfileDir) + backupDir := filepath.Join(parent, fmt.Sprintf(".user-data-old-%d", time.Now().UnixNano())) + hadExisting := false + + if _, err := os.Stat(userDataProfileDir); err == nil { + hadExisting = true + if err := os.Rename(userDataProfileDir, backupDir); err != nil { + return fmt.Errorf("backup user-data: %w", err) + } + } else if !os.IsNotExist(err) { + return fmt.Errorf("stat user-data: %w", err) + } + + if err := os.Rename(preparedDir, userDataProfileDir); err != nil { + if hadExisting { + _ = os.Rename(backupDir, userDataProfileDir) + } + return fmt.Errorf("replace user-data: %w", err) + } + if hadExisting { + _ = os.RemoveAll(backupDir) } return nil } @@ -499,25 +619,75 @@ func chromiumParseDisplayParts(displayJSON *string) (*oapi.PatchDisplayJSONReque return &body, "" } -func chromiumDisplayApplyWhileStopped(ctx context.Context, s *ApiService, body *oapi.PatchDisplayRequest) oapi.ChromiumConfigureResponseObject { - if body.Width == nil || body.Height == nil { - return nil +func chromiumPrepareDisplay(ctx context.Context, s *ApiService, displayJSON *string) (*chromiumDisplayPlan, oapi.ChromiumConfigureResponseObject) { + if displayJSON == nil { + return nil, nil + } + body, msgs := chromiumParseDisplayParts(displayJSON) + if msgs != "" { + return nil, cfg400(msgs) + } + if body.Width == nil && body.Height == nil { + return nil, cfg400("no display parameters to update") + } + + currentWidth, currentHeight, currentRefreshRate, err := s.getCurrentResolution(ctx) + if err != nil { + logger.FromContext(ctx).Error("failed to get current resolution", "error", err) + return nil, cfg500ConfigureStep(chromiumConfigureStepDisplay, "failed to get current display resolution") + } + width, height, refreshRate := currentWidth, currentHeight, currentRefreshRate + if body.Width != nil { + width = *body.Width } - w, h := *body.Width, *body.Height + if body.Height != nil { + height = *body.Height + } + if body.RefreshRate != nil { + refreshRate = int(*body.RefreshRate) + } + + if width <= 0 || height <= 0 { + return nil, cfg400("invalid width/height") + } + + requireIdle := true + if body.RequireIdle != nil { + requireIdle = *body.RequireIdle + } + if requireIdle { + live := s.getActiveNekoSessions(ctx) + isRecording := s.anyRecordingActive(ctx) + if live != 0 || isRecording { + return nil, oapi.ChromiumConfigure409JSONResponse{ + ConflictErrorJSONResponse: oapi.ConflictErrorJSONResponse{ + Message: "resize refused: live view or recording/replay active", + }, + } + } + } + + return &chromiumDisplayPlan{ + body: body, + width: width, + height: height, + refreshRate: refreshRate, + }, nil +} + +func chromiumDisplayApplyWhileStopped(ctx context.Context, s *ApiService, plan *chromiumDisplayPlan) oapi.ChromiumConfigureResponseObject { + w, h := plan.width, plan.height if w <= 0 || h <= 0 { return cfg400("display width and height must be positive") } mode := s.detectDisplayMode(ctx) - rr := 60 - if body.RefreshRate != nil { - rr = int(*body.RefreshRate) - } + rr := plan.refreshRate if mode == "xvfb" { s.xvfbResizeMu.Lock() err := s.resizeXvfb(ctx, w, h) s.xvfbResizeMu.Unlock() if err != nil { - return cfg500Configure(err.Error()) + return cfg500ConfigureStep(chromiumConfigureStepDisplay, err.Error()) } s.clearViewportOverride() return nil @@ -529,7 +699,7 @@ func chromiumDisplayApplyWhileStopped(ctx context.Context, s *ApiService, body * err = s.setResolutionXorgViaXrandr(ctx, w, h, rr, false) } if err != nil { - return cfg500Configure(err.Error()) + return cfg500ConfigureStep(chromiumConfigureStepDisplay, err.Error()) } return nil } @@ -537,7 +707,7 @@ func chromiumDisplayApplyWhileStopped(ctx context.Context, s *ApiService, body * func chromiumRunPatchDisplay(ctx context.Context, s *ApiService, body *oapi.PatchDisplayJSONRequestBody) oapi.ChromiumConfigureResponseObject { resp, err := s.PatchDisplay(ctx, oapi.PatchDisplayRequestObject{Body: body}) if err != nil { - return cfg500Configure(err.Error()) + return cfg500ConfigureStep(chromiumConfigureStepDisplay, err.Error()) } switch r := resp.(type) { case oapi.PatchDisplay200JSONResponse: @@ -547,44 +717,44 @@ func chromiumRunPatchDisplay(ctx context.Context, s *ApiService, body *oapi.Patc case oapi.PatchDisplay409JSONResponse: return oapi.ChromiumConfigure409JSONResponse{ConflictErrorJSONResponse: r.ConflictErrorJSONResponse} case oapi.PatchDisplay500JSONResponse: - return cfg500Configure(r.Message) + return cfg500ConfigureStep(chromiumConfigureStepDisplay, r.Message) default: - return cfg500Configure("unexpected PatchDisplay response") + return cfg500ConfigureStep(chromiumConfigureStepDisplay, "unexpected PatchDisplay response") } } -func chromiumApplyPolicies(ctx context.Context, s *ApiService, raw *string) string { +func chromiumValidatePolicies(raw *string) (policy.ChromiumPolicyOverrides, error) { if raw == nil || strings.TrimSpace(*raw) == "" { - return "" + return nil, nil } var m map[string]interface{} if err := json.Unmarshal([]byte(*raw), &m); err != nil { - return "bad:invalid chrome_policies JSON" + return nil, cfgBadRequest("invalid chrome_policies JSON") } if len(m) == 0 { - return "" + return nil, nil } overrides, err := policy.NewChromiumPolicyOverrides(m) if err != nil { - if strings.Contains(err.Error(), "cannot be overridden") || strings.Contains(err.Error(), "invalid chromium policy overrides") { - return "bad:" + err.Error() - } - return "int:" + err.Error() + return nil, err } - if err := s.policy.ApplyOverrides(overrides); err != nil { - if strings.Contains(err.Error(), "cannot be overridden") || strings.Contains(err.Error(), "invalid chromium policy overrides") { - return "bad:" + err.Error() - } - return "int:" + err.Error() + if err := overrides.Validate(); err != nil { + return nil, cfgBadRequest(err.Error()) } - return "" + return overrides, nil } -func policyDisposition(msgs string) oapi.ChromiumConfigureResponseObject { - if strings.HasPrefix(msgs, "bad:") { - return cfg400(strings.TrimPrefix(msgs, "bad:")) +func chromiumApplyPolicies(ctx context.Context, s *ApiService, overrides policy.ChromiumPolicyOverrides) error { + if len(overrides) == 0 { + return nil + } + if err := s.policy.ApplyOverrides(overrides); err != nil { + if strings.Contains(err.Error(), "cannot be overridden") || strings.Contains(err.Error(), "invalid chromium policy overrides") { + return cfgBadRequest(err.Error()) + } + return err } - return cfg500Configure(strings.TrimPrefix(msgs, "int:")) + return nil } func chromiumApplyExtensions(ctx context.Context, s *ApiService, items []extensionZipItem) (string, error) { @@ -594,30 +764,35 @@ func chromiumApplyExtensions(ctx context.Context, s *ApiService, items []extensi return s.applyExtensionZipItems(ctx, items) } -func chromiumMergeFlagsRaw(ctx context.Context, s *ApiService, raw *string) string { +func chromiumValidateFlags(raw *string) (*chromiumFlagsPlan, error) { if raw == nil || strings.TrimSpace(*raw) == "" { - return "" + return nil, nil } var body struct { Flags []string `json:"flags"` } if err := json.Unmarshal([]byte(*raw), &body); err != nil { - return "bad:invalid chromium_flags JSON" + return nil, cfgBadRequest("invalid chromium_flags JSON") } if len(body.Flags) == 0 { - return "bad:chromium_flags requires at least one flag" + return nil, cfgBadRequest("chromium_flags requires at least one flag") } for _, flag := range body.Flags { t := strings.TrimSpace(flag) if t == "" { - return "bad:empty flag in chromium_flags" + return nil, cfgBadRequest("empty flag in chromium_flags") } if !strings.HasPrefix(t, "--") { - return fmt.Sprintf("bad:invalid flag format: %s (must start with --)", flag) + return nil, cfgBadRequest(fmt.Sprintf("invalid flag format: %s (must start with --)", flag)) } } - if _, err := s.mergeAndWriteChromiumFlags(ctx, body.Flags); err != nil { - return "int:" + err.Error() + return &chromiumFlagsPlan{flags: body.Flags}, nil +} + +func chromiumMergeFlags(ctx context.Context, s *ApiService, plan *chromiumFlagsPlan) error { + if plan == nil { + return nil } - return "" + _, err := s.mergeAndWriteChromiumFlags(ctx, plan.flags) + return err } diff --git a/server/cmd/api/api/chromium_configure_test.go b/server/cmd/api/api/chromium_configure_test.go index f02fd8c7..9f553e78 100644 --- a/server/cmd/api/api/chromium_configure_test.go +++ b/server/cmd/api/api/chromium_configure_test.go @@ -2,12 +2,11 @@ package api import ( "bytes" + "io" "mime/multipart" "strings" "testing" - "time" - "github.com/kernel/kernel-images/server/lib/cdpclient" "github.com/stretchr/testify/require" ) @@ -27,30 +26,57 @@ func TestPoliciesContentNonEmpty(t *testing.T) { require.True(t, policiesContentNonEmpty(&real)) } -func TestChromiumStartURLSpec_plainAndJSON(t *testing.T) { +func TestChromiumStartURLSpec(t *testing.T) { + bareHost := "roblox.com" + out, errs := chromiumStartURLSpec(&bareHost) + require.Empty(t, errs) + require.True(t, out.needsNav) + require.Equal(t, "https://roblox.com", out.url) + plain := "https://example.com/" - out, errs := chromiumStartURLSpec(&plain) + out, errs = chromiumStartURLSpec(&plain) require.Empty(t, errs) require.True(t, out.needsNav) require.Equal(t, plain, out.url) - require.Equal(t, 45*time.Second, out.timeout) - require.Equal(t, cdpclient.NavigateWaitLoad, out.wait) - raw := `{"url":"https://a.test/x","wait_until":"domcontentloaded","timeout_sec":12}` - out, errs = chromiumStartURLSpec(&raw) + fileURL := "file:///etc/passwd" + out, errs = chromiumStartURLSpec(&fileURL) require.Empty(t, errs) - require.True(t, out.needsNav) - require.Equal(t, "https://a.test/x", out.url) - require.Equal(t, 12*time.Second, out.timeout) - require.Equal(t, cdpclient.NavigateWaitDOMContentLoaded, out.wait) + require.Equal(t, fileURL, out.url) - badScheme := "file:///etc/passwd" - _, errs = chromiumStartURLSpec(&badScheme) + longURL := strings.Repeat("a", maxStartURLLen+1) + _, errs = chromiumStartURLSpec(&longURL) require.NotEmpty(t, errs) +} - badWait := `{"url":"https://x.example","wait_until":"networkidle"}` - _, errs = chromiumStartURLSpec(&badWait) - require.NotEmpty(t, errs) +func TestChromiumValidateFlags(t *testing.T) { + valid := `{"flags":["--kiosk"]}` + plan, err := chromiumValidateFlags(&valid) + require.NoError(t, err) + require.Equal(t, []string{"--kiosk"}, plan.flags) + + cases := []string{ + `{bad-json`, + `{"flags":[]}`, + `{"flags":[""]}`, + `{"flags":["kiosk"]}`, + } + for _, tc := range cases { + _, err := chromiumValidateFlags(&tc) + require.Error(t, err, "case %s", tc) + var bad cfgBadRequestError + require.ErrorAs(t, err, &bad) + } +} + +func TestChromiumParseDisplayPartsValidation(t *testing.T) { + badJSON := `{bad-json` + _, msg := chromiumParseDisplayParts(&badJSON) + require.Equal(t, "invalid display JSON", msg) + + empty := `{}` + _, msg = chromiumParseDisplayParts(&empty) + require.Equal(t, "display payload empty", msg) } func TestChromiumCfgParseMultipart(t *testing.T) { @@ -74,3 +100,77 @@ func TestChromiumCfgParseMultipart(t *testing.T) { require.NotNil(t, st.startURLRaw) require.Equal(t, "https://kernel.example/route", strings.TrimSpace(*st.startURLRaw)) } + +func TestChromiumCfgParseMultipartValidation(t *testing.T) { + cases := []struct { + name string + build func(*testing.T, *multipart.Writer) + want string + }{ + { + name: "invalid strip_components", + build: func(t *testing.T, w *multipart.Writer) { + t.Helper() + require.NoError(t, w.WriteField("strip_components", "-1")) + }, + want: "strip_components must be a non-negative integer", + }, + { + name: "duplicate scalar", + build: func(t *testing.T, w *multipart.Writer) { + t.Helper() + require.NoError(t, w.WriteField("start_url", "https://a.example")) + require.NoError(t, w.WriteField("start_url", "https://b.example")) + }, + want: "duplicate start_url field", + }, + { + name: "incomplete extension pair", + build: func(t *testing.T, w *multipart.Writer) { + t.Helper() + require.NoError(t, w.WriteField("extensions.name", "missingzip")) + }, + want: "each extension pair needs extensions.zip_file plus extensions.name", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + buf := bytes.NewBuffer(nil) + w := multipart.NewWriter(buf) + tc.build(t, w) + require.NoError(t, w.Close()) + + st := &chromiumConfigureState{} + msg := chromiumCfgParseMultipart(multipart.NewReader(buf, w.Boundary()), st) + defer st.cleanup() + require.Equal(t, tc.want, msg) + }) + } +} + +func TestChromiumCfgParseMultipartMultipleExtensionPairs(t *testing.T) { + buf := bytes.NewBuffer(nil) + w := multipart.NewWriter(buf) + + part, err := w.CreateFormFile("extensions.zip_file", "one.zip") + require.NoError(t, err) + _, err = io.WriteString(part, "not validated by parser") + require.NoError(t, err) + require.NoError(t, w.WriteField("extensions.name", "one")) + + require.NoError(t, w.WriteField("extensions.name", "two")) + part, err = w.CreateFormFile("extensions.zip_file", "two.zip") + require.NoError(t, err) + _, err = io.WriteString(part, "not validated by parser") + require.NoError(t, err) + require.NoError(t, w.Close()) + + st := &chromiumConfigureState{} + msg := chromiumCfgParseMultipart(multipart.NewReader(buf, w.Boundary()), st) + defer st.cleanup() + require.Empty(t, msg) + require.Len(t, st.extItems, 2) + require.Equal(t, "one", st.extItems[0].name) + require.Equal(t, "two", st.extItems[1].name) +} diff --git a/server/e2e/e2e_chromium_configure_powerset_test.go b/server/e2e/e2e_chromium_configure_powerset_test.go index c58352c9..eda65b54 100644 --- a/server/e2e/e2e_chromium_configure_powerset_test.go +++ b/server/e2e/e2e_chromium_configure_powerset_test.go @@ -7,6 +7,7 @@ import ( "io" "mime/multipart" "net/http" + "os" "os/exec" "path/filepath" "strings" @@ -27,9 +28,8 @@ const ( matMaxBitmask = matDisplay | matPolicy | matKioskFlags | matExtension | matStartURL // 31 ) -// TestChromiumConfigureMultipartPowerset runs sequential subtests covering every non-empty combination -// of multipart parts (display, chrome_policies, chromium_flags kiosk, extensions, start_url). -// Run after: images/chromium-headless/build-docker.sh (default image onkernel/chromium-headless-test:latest). +// TestChromiumConfigureMultipartPowerset runs a representative matrix by default. +// Set E2E_CHROMIUM_CONFIGURE_POWERSET=1 to run every non-empty combination. func TestChromiumConfigureMultipartPowerset(t *testing.T) { if _, err := exec.LookPath("docker"); err != nil { @@ -41,7 +41,20 @@ func TestChromiumConfigureMultipartPowerset(t *testing.T) { extZip, err := zipDirToBytes(extDir) require.NoError(t, err) - for bits := 1; bits <= matMaxBitmask; bits++ { + matrix := []int{ + matDisplay, + matPolicy | matKioskFlags, + matExtension, + matDisplay | matPolicy | matKioskFlags | matExtension | matStartURL, + } + if os.Getenv("E2E_CHROMIUM_CONFIGURE_POWERSET") == "1" { + matrix = matrix[:0] + for bits := 1; bits <= matMaxBitmask; bits++ { + matrix = append(matrix, bits) + } + } + + for _, bits := range matrix { bits := bits t.Run(chromiumConfigurePowersetLabel(bits), func(t *testing.T) { @@ -150,7 +163,7 @@ func chromiumConfigurePowersetPopulate(t *testing.T, w *multipart.Writer, bits i } if bits&matStartURL != 0 { - if err := w.WriteField("start_url", `https://example.com/`); err != nil { + if err := w.WriteField("start_url", `data:text/html,configure`); err != nil { return err } } @@ -159,8 +172,8 @@ func chromiumConfigurePowersetPopulate(t *testing.T, w *multipart.Writer, bits i func intPtr(i int) *int { return &i } -// TestChromiumConfigureMultistartJSONObject exercises JSON start_url variant (wait_until JSON). -func TestChromiumConfigureStartURLJSONObject(t *testing.T) { +// TestChromiumConfigureStartURLBareHost exercises Kernel-compatible bare host normalization. +func TestChromiumConfigureStartURLBareHost(t *testing.T) { t.Parallel() if _, err := exec.LookPath("docker"); err != nil { @@ -177,16 +190,9 @@ func TestChromiumConfigureStartURLJSONObject(t *testing.T) { defer c.Stop(ctx) require.NoError(t, c.WaitReady(ctx)) - payload := map[string]string{ - "url": "https://example.com/", - "wait_until": "domcontentloaded", - } - raw, err := json.Marshal(payload) - require.NoError(t, err) - var buf bytes.Buffer mw := multipart.NewWriter(&buf) - require.NoError(t, mw.WriteField("start_url", string(raw))) + require.NoError(t, mw.WriteField("start_url", "example.com")) require.NoError(t, mw.Close()) client, err := c.APIClient() diff --git a/server/e2e/e2e_chromium_configure_test.go b/server/e2e/e2e_chromium_configure_test.go index 6d4811f2..282771a5 100644 --- a/server/e2e/e2e_chromium_configure_test.go +++ b/server/e2e/e2e_chromium_configure_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + instanceoapi "github.com/kernel/kernel-images/server/lib/oapi" "github.com/stretchr/testify/require" ) @@ -39,7 +40,8 @@ func TestChromiumConfigureStartURLBare(t *testing.T) { var buf bytes.Buffer w := multipart.NewWriter(&buf) - require.NoError(t, w.WriteField("start_url", `https://example.com/`)) + startURL := `data:text/html,kernel-configure` + require.NoError(t, w.WriteField("start_url", startURL)) require.NoError(t, w.Close()) rsp, err := client.ChromiumConfigureWithBodyWithResponse(ctx, w.FormDataContentType(), io.NopCloser(&buf)) @@ -48,4 +50,17 @@ func TestChromiumConfigureStartURLBare(t *testing.T) { require.Equal(t, http.StatusOK, rsp.StatusCode(), "unexpected status=%s body=%s", rsp.Status(), string(rsp.Body)) require.NotNil(t, rsp.JSON200, "want ok json") require.True(t, rsp.JSON200.Ok) + + require.Eventually(t, func() bool { + timeoutSec := 3 + pwResp, err := client.ExecutePlaywrightCodeWithResponse(ctx, instanceoapi.ExecutePlaywrightRequest{ + Code: "return page.url();", + TimeoutSec: &timeoutSec, + }) + if err != nil || pwResp.JSON200 == nil || !pwResp.JSON200.Success { + return false + } + got, ok := pwResp.JSON200.Result.(string) + return ok && got == startURL + }, 10*time.Second, 250*time.Millisecond) } diff --git a/server/lib/cdpclient/cdpclient.go b/server/lib/cdpclient/cdpclient.go index 6692db88..fd8759c4 100644 --- a/server/lib/cdpclient/cdpclient.go +++ b/server/lib/cdpclient/cdpclient.go @@ -35,7 +35,7 @@ func (e *cdpError) Error() string { // Client is a minimal CDP client that communicates over a browser-level // DevTools WebSocket connection. type Client struct { - conn *websocket.Conn + conn *websocket.Conn nextID atomic.Int64 } @@ -191,6 +191,88 @@ func NavigateFirstPage(ctx context.Context, devtoolsURL, url string, waitUntil N return nil } +// DispatchStartURL closes extra page targets and dispatches a navigation on the +// first page target. It does not wait for lifecycle events; Chrome owns the +// eventual navigation result. +func DispatchStartURL(ctx context.Context, devtoolsURL, url string) error { + c, err := Dial(ctx, devtoolsURL) + if err != nil { + return fmt.Errorf("dial devtools: %w", err) + } + defer c.Close() + + targetsResult, err := c.send(ctx, "Target.getTargets", nil, "") + if err != nil { + return fmt.Errorf("Target.getTargets: %w", err) + } + + var targets struct { + TargetInfos []struct { + TargetID string `json:"targetId"` + Type string `json:"type"` + } `json:"targetInfos"` + } + if err := json.Unmarshal(targetsResult, &targets); err != nil { + return fmt.Errorf("unmarshal targets: %w", err) + } + + var pageTargetID string + for _, t := range targets.TargetInfos { + if t.Type != "page" { + continue + } + if pageTargetID == "" { + pageTargetID = t.TargetID + continue + } + _, _ = c.send(ctx, "Target.closeTarget", map[string]any{ + "targetId": t.TargetID, + }, "") + } + if pageTargetID == "" { + createResult, err := c.send(ctx, "Target.createTarget", map[string]any{ + "url": "about:blank", + }, "") + if err != nil { + return fmt.Errorf("Target.createTarget: %w", err) + } + var created struct { + TargetID string `json:"targetId"` + } + if err := json.Unmarshal(createResult, &created); err != nil { + return fmt.Errorf("unmarshal create target: %w", err) + } + pageTargetID = created.TargetID + } + + attachResult, err := c.send(ctx, "Target.attachToTarget", map[string]any{ + "targetId": pageTargetID, + "flatten": true, + }, "") + if err != nil { + return fmt.Errorf("Target.attachToTarget: %w", err) + } + + var attach struct { + SessionID string `json:"sessionId"` + } + if err := json.Unmarshal(attachResult, &attach); err != nil { + return fmt.Errorf("unmarshal attach: %w", err) + } + defer func() { + detachCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _, _ = c.send(detachCtx, "Target.detachFromTarget", map[string]any{ + "sessionId": attach.SessionID, + }, "") + }() + + if _, err := c.send(ctx, "Page.navigate", map[string]any{"url": url}, attach.SessionID); err != nil { + return fmt.Errorf("Page.navigate: %w", err) + } + return nil +} + func (c *Client) waitForPageEvents(ctx context.Context, sessionID string, want map[string]struct{}) error { for { select { @@ -270,10 +352,10 @@ func (c *Client) SetDeviceMetricsOverride(ctx context.Context, width, height int } _, err = c.send(ctx, "Emulation.setDeviceMetricsOverride", map[string]any{ - "width": width, - "height": height, + "width": width, + "height": height, "deviceScaleFactor": 1, - "mobile": false, + "mobile": false, }, attach.SessionID) if err != nil { return fmt.Errorf("Emulation.setDeviceMetricsOverride: %w", err) diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 4afa5326..a07b3006 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -1841,6 +1841,9 @@ type ChromiumConfigureError struct { // Phase configure_phase maps to restart/filesystem/policy/extension/profile/display work; navigate_phase maps to Page.navigate after readiness. Phase ChromiumConfigureErrorPhase `json:"phase"` + + // Step Optional configure step that failed. + Step *string `json:"step,omitempty"` } // ChromiumConfigureErrorPhase configure_phase maps to restart/filesystem/policy/extension/profile/display work; navigate_phase maps to Page.navigate after readiness. @@ -2514,7 +2517,7 @@ type ChromiumConfigureMultipartBody struct { // ProfileArchive tar.zst of `/home/kernel/user-data` (V2 profiles). Stripped paths use strip_components optional part. ProfileArchive *openapi_types.File `json:"profile_archive,omitempty"` - // StartUrl Bare https? URL text, OR UTF-8 JSON `{"url":"...", "wait_until":"load"|"domcontentloaded"}`. + // StartUrl URL text to navigate after configure. Bare hosts are normalized to https://, length is capped at 2048 bytes, and Chrome decides which schemes are navigable. StartUrl *string `json:"start_url,omitempty"` // StripComponents Leading path components to strip when extracting profile_archive (non-negative integer as text). diff --git a/server/openapi.yaml b/server/openapi.yaml index ab2d75b7..5dd3636e 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1176,8 +1176,13 @@ paths: description: | Optional multipart parts apply configuration while Chromium stays stopped once (policy, flags, extensions, profile archive, optional display sizing), then Chromium is started exactly - once and DevTools readiness is awaited. Optional `start_url` applies a Page.navigate via CDP - after readiness. Omit any part you do not need. At least one actionable part must be present. + once and DevTools readiness is awaited. Optional `start_url` dispatches a best-effort + navigation after readiness without waiting for page load. Bare hosts are normalized to + `https://`. Omit any part you do not need. At least one actionable part must be present. + Required configuration steps run in the same logical order as the control-plane sync path + (policies, extensions, display, flags/kiosk, profile), but Chromium is started only once + at the end. The endpoint is not transactional: if a later required step fails, earlier + successful side effects may remain on the instance while the non-2xx error propagates. Prefer this over separate `/chromium/*` and `/display` calls when multiple restart-triggering steps apply in one session configure. operationId: chromiumConfigure @@ -1191,6 +1196,8 @@ paths: display: description: >- UTF-8 JSON object matching `#/components/schemas/PatchDisplayRequest` (width/height/etc.). + When combined with restart-triggering fields, the resize is applied while Chromium + is stopped and Chromium is started once at the end. type: string chromium_flags: description: >- @@ -1209,7 +1216,8 @@ paths: type: string start_url: description: >- - Bare https? URL text, OR UTF-8 JSON `{"url":"...", "wait_until":"load"|"domcontentloaded"}`. + URL text to navigate after configure. Bare hosts are normalized to https://, + length is capped at 2048 bytes, and Chrome decides which schemes are navigable. type: string extensions: type: array @@ -1236,7 +1244,7 @@ paths: "409": $ref: "#/components/responses/ConflictError" "500": - description: Configure or navigate failure with structured phase + description: Configure failure with structured phase content: application/json: schema: @@ -2564,6 +2572,17 @@ components: properties: message: type: string + step: + type: string + description: Optional configure step that failed. + enum: + - stop_chromium + - start_chromium + - chrome_policies + - extensions + - display + - chromium_flags + - profile ChromiumConfigureError: type: object description: Failure from batched chromium configure — includes which phase failed. @@ -2575,7 +2594,7 @@ components: enum: - configure_phase - navigate_phase - description: configure_phase maps to restart/filesystem/policy/extension/profile/display work; navigate_phase maps to Page.navigate after readiness. + description: configure_phase maps to restart/filesystem/policy/extension/profile/display work; navigate_phase is retained for compatibility. message: type: string RecorderInfo: From ff66cb49a2156b4eaf6a2e35d74a480e6a7de1a9 Mon Sep 17 00:00:00 2001 From: Hiro Tamada Date: Thu, 21 May 2026 09:17:58 -0400 Subject: [PATCH 04/12] Fix chromium configure review comments. Avoid unnecessary stop/start cycles for empty flag updates and remove stale CDP/configure helpers flagged during review. Co-authored-by: Cursor --- server/cmd/api/api/chromium_configure.go | 41 +----- server/cmd/api/api/chromium_configure_test.go | 13 ++ server/lib/cdpclient/cdpclient.go | 125 ------------------ 3 files changed, 15 insertions(+), 164 deletions(-) diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go index 5c5b8e92..bda36483 100644 --- a/server/cmd/api/api/chromium_configure.go +++ b/server/cmd/api/api/chromium_configure.go @@ -270,7 +270,7 @@ func chromiumNeedsStopCycle(st *chromiumConfigureState) bool { return st.hasProfile || len(st.extItems) > 0 || policiesNonEmpty(st.chromePoliciesJSON) || - flagsNonEmpty(st.chromiumFlagsJSON) + flagsContentNonEmpty(st.chromiumFlagsJSON) } func policiesContentNonEmpty(s *string) bool { @@ -305,42 +305,12 @@ func flagsNonEmpty(s *string) bool { return s != nil && strings.TrimSpace(*s) != "" } -func chromiumDisplayHasSizedRequest(displayJSON *string) bool { - if displayJSON == nil { - return false - } - var raw map[string]interface{} - if err := json.Unmarshal([]byte(*displayJSON), &raw); err != nil { - return false - } - w, ow := raw["width"] - h, oh := raw["height"] - if !ow || !oh { - return false - } - fw, wok := w.(float64) - fh, hok := h.(float64) - if wok && hok && fw > 0 && fh > 0 { - return true - } - iw, wok := w.(int) - ih, hok := h.(int) - return wok && hok && iw > 0 && ih > 0 -} - func cfg400(msg string) oapi.ChromiumConfigure400JSONResponse { return oapi.ChromiumConfigure400JSONResponse{ BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: msg}, } } -func cfg500Configure(msg string) oapi.ChromiumConfigure500JSONResponse { - return oapi.ChromiumConfigure500JSONResponse(oapi.ChromiumConfigureError{ - Phase: oapi.ConfigurePhase, - Message: msg, - }) -} - func cfg500ConfigureStep(step chromiumConfigureStep, msg string) oapi.ChromiumConfigure500JSONResponse { stepValue := string(step) return oapi.ChromiumConfigure500JSONResponse(oapi.ChromiumConfigureError{ @@ -350,19 +320,12 @@ func cfg500ConfigureStep(step chromiumConfigureStep, msg string) oapi.ChromiumCo }) } -func cfg500Navigate(msg string) oapi.ChromiumConfigure500JSONResponse { - return oapi.ChromiumConfigure500JSONResponse(oapi.ChromiumConfigureError{ - Phase: oapi.NavigatePhase, - Message: msg, - }) -} - func cfgActionables(st *chromiumConfigureState) int { n := 0 if policiesContentNonEmpty(st.chromePoliciesJSON) { n++ } - if flagsNonEmpty(st.chromiumFlagsJSON) { + if flagsContentNonEmpty(st.chromiumFlagsJSON) { n++ } if len(st.extItems) > 0 { diff --git a/server/cmd/api/api/chromium_configure_test.go b/server/cmd/api/api/chromium_configure_test.go index 9f553e78..b4b41763 100644 --- a/server/cmd/api/api/chromium_configure_test.go +++ b/server/cmd/api/api/chromium_configure_test.go @@ -26,6 +26,19 @@ func TestPoliciesContentNonEmpty(t *testing.T) { require.True(t, policiesContentNonEmpty(&real)) } +func TestChromiumConfigureActionableFlags(t *testing.T) { + emptyFlags := `{"flags":[]}` + realFlags := `{"flags":["--kiosk"]}` + + st := &chromiumConfigureState{chromiumFlagsJSON: &emptyFlags} + require.Equal(t, 0, cfgActionables(st)) + require.False(t, chromiumNeedsStopCycle(st)) + + st = &chromiumConfigureState{chromiumFlagsJSON: &realFlags} + require.Equal(t, 1, cfgActionables(st)) + require.True(t, chromiumNeedsStopCycle(st)) +} + func TestChromiumStartURLSpec(t *testing.T) { bareHost := "roblox.com" out, errs := chromiumStartURLSpec(&bareHost) diff --git a/server/lib/cdpclient/cdpclient.go b/server/lib/cdpclient/cdpclient.go index fd8759c4..a33b6319 100644 --- a/server/lib/cdpclient/cdpclient.go +++ b/server/lib/cdpclient/cdpclient.go @@ -99,98 +99,6 @@ func (c *Client) send(ctx context.Context, method string, params any, sessionID } } -// NavigateWaitUntil controls how long NavigateFirstPage waits after Page.navigate. -type NavigateWaitUntil string - -const ( - NavigateWaitLoad NavigateWaitUntil = "load" - NavigateWaitDOMContentLoaded NavigateWaitUntil = "domcontentloaded" -) - -// NavigateFirstPage attaches to the first page target, navigates to url, and optionally -// waits for load or DOMContentLoaded. Uses a browser-level WebSocket (flattened session). -func NavigateFirstPage(ctx context.Context, devtoolsURL, url string, waitUntil NavigateWaitUntil) error { - c, err := Dial(ctx, devtoolsURL) - if err != nil { - return fmt.Errorf("dial devtools: %w", err) - } - defer c.Close() - - targetsResult, err := c.send(ctx, "Target.getTargets", nil, "") - if err != nil { - return fmt.Errorf("Target.getTargets: %w", err) - } - - var targets struct { - TargetInfos []struct { - TargetID string `json:"targetId"` - Type string `json:"type"` - } `json:"targetInfos"` - } - if err := json.Unmarshal(targetsResult, &targets); err != nil { - return fmt.Errorf("unmarshal targets: %w", err) - } - - var pageTargetID string - for _, t := range targets.TargetInfos { - if t.Type == "page" { - pageTargetID = t.TargetID - break - } - } - if pageTargetID == "" { - return fmt.Errorf("no page target found") - } - - attachResult, err := c.send(ctx, "Target.attachToTarget", map[string]any{ - "targetId": pageTargetID, - "flatten": true, - }, "") - if err != nil { - return fmt.Errorf("Target.attachToTarget: %w", err) - } - - var attach struct { - SessionID string `json:"sessionId"` - } - if err := json.Unmarshal(attachResult, &attach); err != nil { - return fmt.Errorf("unmarshal attach: %w", err) - } - sess := attach.SessionID - defer func() { - detachCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - _, _ = c.send(detachCtx, "Target.detachFromTarget", map[string]any{ - "sessionId": sess, - }, "") - }() - - if _, err := c.send(ctx, "Page.enable", map[string]any{}, sess); err != nil { - return fmt.Errorf("Page.enable: %w", err) - } - - wantEvents := map[string]struct{}{} - switch waitUntil { - case NavigateWaitDOMContentLoaded: - wantEvents["Page.domContentEventFired"] = struct{}{} - case NavigateWaitLoad, "": - wantEvents["Page.loadEventFired"] = struct{}{} - default: - return fmt.Errorf("unsupported wait_until: %q", waitUntil) - } - - // Only one goroutine may read from the WebSocket — never overlap send()'s Read - // loop with waitForPageEvents. - if _, err := c.send(ctx, "Page.navigate", map[string]any{"url": url}, sess); err != nil { - return fmt.Errorf("Page.navigate: %w", err) - } - if err := c.waitForPageEvents(ctx, sess, wantEvents); err != nil { - return err - } - - return nil -} - // DispatchStartURL closes extra page targets and dispatches a navigation on the // first page target. It does not wait for lifecycle events; Chrome owns the // eventual navigation result. @@ -273,39 +181,6 @@ func DispatchStartURL(ctx context.Context, devtoolsURL, url string) error { return nil } -func (c *Client) waitForPageEvents(ctx context.Context, sessionID string, want map[string]struct{}) error { - for { - select { - case <-ctx.Done(): - return fmt.Errorf("wait for navigation event: %w", ctx.Err()) - default: - } - - _, msg, err := c.conn.Read(ctx) - if err != nil { - return fmt.Errorf("read cdp: %w", err) - } - - var envelope struct { - Method string `json:"method"` - SessionID string `json:"sessionId"` - Params json.RawMessage `json:"params"` - } - if err := json.Unmarshal(msg, &envelope); err != nil { - continue - } - if envelope.Method == "" { - continue - } - if envelope.SessionID != sessionID { - continue - } - if _, ok := want[envelope.Method]; ok { - return nil - } - } -} - // SetDeviceMetricsOverride sets the viewport dimensions on the first page // target found in the browser. It attaches to the target with a flattened // session, sends Emulation.setDeviceMetricsOverride, then detaches. From a36897709f2d1dfdcb8e59098ae45797fc3b35f4 Mon Sep 17 00:00:00 2001 From: Hiro Tamada Date: Thu, 21 May 2026 09:30:56 -0400 Subject: [PATCH 05/12] Avoid configure restart for empty policies. Match stop/start gating to policy apply semantics so empty policy objects remain no-op configuration. Co-authored-by: Cursor --- server/cmd/api/api/chromium_configure.go | 2 +- server/cmd/api/api/chromium_configure_test.go | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go index bda36483..80e9d0f8 100644 --- a/server/cmd/api/api/chromium_configure.go +++ b/server/cmd/api/api/chromium_configure.go @@ -269,7 +269,7 @@ type chromiumDisplayPlan struct { func chromiumNeedsStopCycle(st *chromiumConfigureState) bool { return st.hasProfile || len(st.extItems) > 0 || - policiesNonEmpty(st.chromePoliciesJSON) || + policiesContentNonEmpty(st.chromePoliciesJSON) || flagsContentNonEmpty(st.chromiumFlagsJSON) } diff --git a/server/cmd/api/api/chromium_configure_test.go b/server/cmd/api/api/chromium_configure_test.go index b4b41763..4343f1d5 100644 --- a/server/cmd/api/api/chromium_configure_test.go +++ b/server/cmd/api/api/chromium_configure_test.go @@ -39,6 +39,19 @@ func TestChromiumConfigureActionableFlags(t *testing.T) { require.True(t, chromiumNeedsStopCycle(st)) } +func TestChromiumConfigureActionablePolicies(t *testing.T) { + emptyPolicies := `{}` + realPolicies := `{"QuicAllowed":false}` + + st := &chromiumConfigureState{chromePoliciesJSON: &emptyPolicies} + require.Equal(t, 0, cfgActionables(st)) + require.False(t, chromiumNeedsStopCycle(st)) + + st = &chromiumConfigureState{chromePoliciesJSON: &realPolicies} + require.Equal(t, 1, cfgActionables(st)) + require.True(t, chromiumNeedsStopCycle(st)) +} + func TestChromiumStartURLSpec(t *testing.T) { bareHost := "roblox.com" out, errs := chromiumStartURLSpec(&bareHost) From 45c28f02b4395f962a2bb62c3d23041fb6ba7c8b Mon Sep 17 00:00:00 2001 From: Hiro Tamada Date: Thu, 21 May 2026 09:48:04 -0400 Subject: [PATCH 06/12] Guard no-stop display configure plan. Keep the display configure branches consistent if display planning ever becomes a no-op. Co-authored-by: Cursor --- server/cmd/api/api/chromium_configure.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go index 80e9d0f8..f9524894 100644 --- a/server/cmd/api/api/chromium_configure.go +++ b/server/cmd/api/api/chromium_configure.go @@ -165,8 +165,10 @@ func (s *ApiService) ChromiumConfigure(ctx context.Context, request oapi.Chromiu if displayResp != nil { return displayResp, nil } - if rr := chromiumRunPatchDisplay(ctx, s, displayPlan.body); rr != nil { - return rr, nil + if displayPlan != nil { + if rr := chromiumRunPatchDisplay(ctx, s, displayPlan.body); rr != nil { + return rr, nil + } } } } From 99fe997d18c2c9084872f0b5fd1e039abc8e126c Mon Sep 17 00:00:00 2001 From: Hiro Tamada Date: Thu, 21 May 2026 10:27:12 -0400 Subject: [PATCH 07/12] Scope configure step schema to configure errors. Keep shared API errors generic while documenting the configure-specific step field on ChromiumConfigureError. Co-authored-by: Cursor --- server/cmd/api/api/chromium_configure.go | 2 +- server/lib/oapi/oapi.go | 629 ++++++++++++----------- server/openapi.yaml | 22 +- 3 files changed, 347 insertions(+), 306 deletions(-) diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go index f9524894..dc616c41 100644 --- a/server/cmd/api/api/chromium_configure.go +++ b/server/cmd/api/api/chromium_configure.go @@ -314,7 +314,7 @@ func cfg400(msg string) oapi.ChromiumConfigure400JSONResponse { } func cfg500ConfigureStep(step chromiumConfigureStep, msg string) oapi.ChromiumConfigure500JSONResponse { - stepValue := string(step) + stepValue := oapi.ChromiumConfigureErrorStep(step) return oapi.ChromiumConfigure500JSONResponse(oapi.ChromiumConfigureError{ Phase: oapi.ConfigurePhase, Step: &stepValue, diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index a07b3006..2c6bda90 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -455,6 +455,39 @@ func (e ChromiumConfigureErrorPhase) Valid() bool { } } +// Defines values for ChromiumConfigureErrorStep. +const ( + ChromePolicies ChromiumConfigureErrorStep = "chrome_policies" + ChromiumFlags ChromiumConfigureErrorStep = "chromium_flags" + Display ChromiumConfigureErrorStep = "display" + Extensions ChromiumConfigureErrorStep = "extensions" + Profile ChromiumConfigureErrorStep = "profile" + StartChromium ChromiumConfigureErrorStep = "start_chromium" + StopChromium ChromiumConfigureErrorStep = "stop_chromium" +) + +// Valid indicates whether the value is a known member of the ChromiumConfigureErrorStep enum. +func (e ChromiumConfigureErrorStep) Valid() bool { + switch e { + case ChromePolicies: + return true + case ChromiumFlags: + return true + case Display: + return true + case Extensions: + return true + case Profile: + return true + case StartChromium: + return true + case StopChromium: + return true + default: + return false + } +} + // Defines values for ClickMouseRequestButton. const ( ClickMouseRequestButtonBack ClickMouseRequestButton = "back" @@ -1839,16 +1872,19 @@ type BrowserTelemetryConfig struct { type ChromiumConfigureError struct { Message string `json:"message"` - // Phase configure_phase maps to restart/filesystem/policy/extension/profile/display work; navigate_phase maps to Page.navigate after readiness. + // Phase configure_phase maps to restart/filesystem/policy/extension/profile/display work; navigate_phase is retained for compatibility. Phase ChromiumConfigureErrorPhase `json:"phase"` // Step Optional configure step that failed. - Step *string `json:"step,omitempty"` + Step *ChromiumConfigureErrorStep `json:"step,omitempty"` } -// ChromiumConfigureErrorPhase configure_phase maps to restart/filesystem/policy/extension/profile/display work; navigate_phase maps to Page.navigate after readiness. +// ChromiumConfigureErrorPhase configure_phase maps to restart/filesystem/policy/extension/profile/display work; navigate_phase is retained for compatibility. type ChromiumConfigureErrorPhase string +// ChromiumConfigureErrorStep Optional configure step that failed. +type ChromiumConfigureErrorStep string + // ClickMouseRequest defines model for ClickMouseRequest. type ClickMouseRequest struct { // Button Mouse button to interact with @@ -2505,7 +2541,7 @@ type ChromiumConfigureMultipartBody struct { // ChromiumFlags UTF-8 JSON object `{"flags":["--kiosk"]}` — same semantics as PATCH /chromium/flags. ChromiumFlags *string `json:"chromium_flags,omitempty"` - // Display UTF-8 JSON object matching `#/components/schemas/PatchDisplayRequest` (width/height/etc.). + // Display UTF-8 JSON object matching `#/components/schemas/PatchDisplayRequest` (width/height/etc.). When combined with restart-triggering fields, the resize is applied while Chromium is stopped and Chromium is started once at the end. Display *string `json:"display,omitempty"` // Extensions Extension zips paired with consecutive extensions.name fields (same as upload-extensions-and-restart). @@ -17336,296 +17372,301 @@ func (sh *strictHandler) StreamTelemetryEvents(w http.ResponseWriter, r *http.Re // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "AsYNO8cHwcG5YxLPlq71/x8AAP//rDom6iV1AQA=", + "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", + "AA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 5dd3636e..45f48b6a 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -2572,17 +2572,6 @@ components: properties: message: type: string - step: - type: string - description: Optional configure step that failed. - enum: - - stop_chromium - - start_chromium - - chrome_policies - - extensions - - display - - chromium_flags - - profile ChromiumConfigureError: type: object description: Failure from batched chromium configure — includes which phase failed. @@ -2597,6 +2586,17 @@ components: description: configure_phase maps to restart/filesystem/policy/extension/profile/display work; navigate_phase is retained for compatibility. message: type: string + step: + type: string + description: Optional configure step that failed. + enum: + - stop_chromium + - start_chromium + - chrome_policies + - extensions + - display + - chromium_flags + - profile RecorderInfo: type: object required: [id, isRecording] From e7ea12a008aa4dbd40823e50fba05cb35b70c49e Mon Sep 17 00:00:00 2001 From: Hiro Tamada Date: Thu, 21 May 2026 10:45:22 -0400 Subject: [PATCH 08/12] Treat empty configure flags as no-op. Keep flag validation consistent with actionable detection and reject duplicate extension zip parts before copying uploads. Co-authored-by: Cursor --- server/cmd/api/api/chromium_configure.go | 8 +++---- server/cmd/api/api/chromium_configure_test.go | 21 ++++++++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go index dc616c41..b1764dc6 100644 --- a/server/cmd/api/api/chromium_configure.go +++ b/server/cmd/api/api/chromium_configure.go @@ -455,6 +455,9 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) str if cur == nil { cur = &pend{} } + if cur.gotZip { + return "duplicate extensions.zip_file pair" + } tmp, err := os.CreateTemp("", "bcc-ext-*.zip") if err != nil { return "temp extensions.zip_file" @@ -467,9 +470,6 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) str if err := tmp.Close(); err != nil { return "close extensions.zip_file" } - if cur.gotZip { - return "duplicate extensions.zip_file pair" - } cur.zipTmp = tmp.Name() cur.gotZip = true case "extensions.name": @@ -740,7 +740,7 @@ func chromiumValidateFlags(raw *string) (*chromiumFlagsPlan, error) { return nil, cfgBadRequest("invalid chromium_flags JSON") } if len(body.Flags) == 0 { - return nil, cfgBadRequest("chromium_flags requires at least one flag") + return nil, nil } for _, flag := range body.Flags { t := strings.TrimSpace(flag) diff --git a/server/cmd/api/api/chromium_configure_test.go b/server/cmd/api/api/chromium_configure_test.go index 4343f1d5..28c9d5d3 100644 --- a/server/cmd/api/api/chromium_configure_test.go +++ b/server/cmd/api/api/chromium_configure_test.go @@ -81,9 +81,13 @@ func TestChromiumValidateFlags(t *testing.T) { require.NoError(t, err) require.Equal(t, []string{"--kiosk"}, plan.flags) + empty := `{"flags":[]}` + plan, err = chromiumValidateFlags(&empty) + require.NoError(t, err) + require.Nil(t, plan) + cases := []string{ `{bad-json`, - `{"flags":[]}`, `{"flags":[""]}`, `{"flags":["kiosk"]}`, } @@ -158,6 +162,21 @@ func TestChromiumCfgParseMultipartValidation(t *testing.T) { }, want: "each extension pair needs extensions.zip_file plus extensions.name", }, + { + name: "duplicate extension zip", + build: func(t *testing.T, w *multipart.Writer) { + t.Helper() + part, err := w.CreateFormFile("extensions.zip_file", "one.zip") + require.NoError(t, err) + _, err = io.WriteString(part, "first") + require.NoError(t, err) + part, err = w.CreateFormFile("extensions.zip_file", "two.zip") + require.NoError(t, err) + _, err = io.WriteString(part, "second") + require.NoError(t, err) + }, + want: "duplicate extensions.zip_file pair", + }, } for _, tc := range cases { From 5f2e39a78056be327f2a2ff4536d244ff63b3f71 Mon Sep 17 00:00:00 2001 From: Hiro Tamada Date: Thu, 21 May 2026 11:11:37 -0400 Subject: [PATCH 09/12] Tighten chromium configure error handling. Classify multipart parse failures by response type, keep policy validation errors as bad requests, and clear Xvfb viewport overrides under the resize lock. Co-authored-by: Cursor --- server/cmd/api/api/chromium_configure.go | 94 ++++++++++++------- server/cmd/api/api/chromium_configure_test.go | 24 +++-- 2 files changed, 80 insertions(+), 38 deletions(-) diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go index b1764dc6..4be250a8 100644 --- a/server/cmd/api/api/chromium_configure.go +++ b/server/cmd/api/api/chromium_configure.go @@ -58,9 +58,13 @@ func (s *ApiService) ChromiumConfigure(ctx context.Context, request oapi.Chromiu } st := &chromiumConfigureState{} - if msg := chromiumCfgParseMultipart(request.Body, st); msg != "" { + if parseErr := chromiumCfgParseMultipart(request.Body, st); parseErr != nil { st.cleanup() - return cfg400(msg), nil + var cfgErr chromiumCfgParseError + if errors.As(parseErr, &cfgErr) && cfgErr.internal { + return cfg500Configure(parseErr.Error()), nil + } + return cfg400(parseErr.Error()), nil } defer st.cleanup() @@ -249,6 +253,23 @@ func cfgBadRequest(msg string) error { return cfgBadRequestError{message: msg} } +type chromiumCfgParseError struct { + message string + internal bool +} + +func (e chromiumCfgParseError) Error() string { + return e.message +} + +func cfgParseBadRequest(msg string) error { + return chromiumCfgParseError{message: msg} +} + +func cfgParseInternal(msg string) error { + return chromiumCfgParseError{message: msg, internal: true} +} + func cfgResponseFromStepError(step chromiumConfigureStep, err error) oapi.ChromiumConfigureResponseObject { var bad cfgBadRequestError if errors.As(err, &bad) { @@ -313,6 +334,13 @@ func cfg400(msg string) oapi.ChromiumConfigure400JSONResponse { } } +func cfg500Configure(msg string) oapi.ChromiumConfigure500JSONResponse { + return oapi.ChromiumConfigure500JSONResponse(oapi.ChromiumConfigureError{ + Phase: oapi.ConfigurePhase, + Message: msg, + }) +} + func cfg500ConfigureStep(step chromiumConfigureStep, msg string) oapi.ChromiumConfigure500JSONResponse { stepValue := oapi.ChromiumConfigureErrorStep(step) return oapi.ChromiumConfigure500JSONResponse(oapi.ChromiumConfigureError{ @@ -349,12 +377,12 @@ func cfgHasStartURLSpec(spec startURLParsed) int { return 1 } -func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) string { +func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) error { mr, ok := any(body).(interface { NextPart() (*multipart.Part, error) }) if !ok { - return "multipart reader not available" + return cfgParseInternal("multipart reader not available") } type pend struct { @@ -371,83 +399,83 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) str break } if err != nil { - return "failed reading multipart" + return cfgParseBadRequest("failed reading multipart") } switch name := part.FormName(); name { case "display": if gotDisplay { - return "duplicate display field" + return cfgParseBadRequest("duplicate display field") } gotDisplay = true b, err := io.ReadAll(part) if err != nil { - return "read display field" + return cfgParseInternal("read display field") } v := strings.TrimSpace(string(b)) st.displayJSON = &v case "chromium_flags": if gotChromiumFlags { - return "duplicate chromium_flags field" + return cfgParseBadRequest("duplicate chromium_flags field") } gotChromiumFlags = true b, err := io.ReadAll(part) if err != nil { - return "read chromium_flags field" + return cfgParseInternal("read chromium_flags field") } v := string(b) st.chromiumFlagsJSON = &v case "chrome_policies": if gotChromePolicies { - return "duplicate chrome_policies field" + return cfgParseBadRequest("duplicate chrome_policies field") } gotChromePolicies = true b, err := io.ReadAll(part) if err != nil { - return "read chrome_policies field" + return cfgParseInternal("read chrome_policies field") } v := string(b) st.chromePoliciesJSON = &v case "strip_components": if gotStripComponents { - return "duplicate strip_components field" + return cfgParseBadRequest("duplicate strip_components field") } gotStripComponents = true b, err := io.ReadAll(part) if err != nil { - return "read strip_components" + return cfgParseInternal("read strip_components") } n, err := strconv.Atoi(strings.TrimSpace(string(b))) if err != nil || n < 0 { - return "strip_components must be a non-negative integer" + return cfgParseBadRequest("strip_components must be a non-negative integer") } st.stripComponents = n case "profile_archive": if gotProfileArchive { - return "duplicate profile_archive field" + return cfgParseBadRequest("duplicate profile_archive field") } gotProfileArchive = true tmp, err := os.CreateTemp("", "bcc-prof-*.tar.zst") if err != nil { - return "temp profile_archive" + return cfgParseInternal("temp profile_archive") } st.allTemps = append(st.allTemps, tmp.Name()) if _, err := io.Copy(tmp, part); err != nil { tmp.Close() - return "read profile_archive" + return cfgParseInternal("read profile_archive") } if err := tmp.Close(); err != nil { - return "finalize profile_archive" + return cfgParseInternal("finalize profile_archive") } st.profileTemp = tmp.Name() st.hasProfile = true case "start_url": if gotStartURL { - return "duplicate start_url field" + return cfgParseBadRequest("duplicate start_url field") } gotStartURL = true b, err := io.ReadAll(part) if err != nil { - return "read start_url" + return cfgParseInternal("read start_url") } v := string(b) st.startURLRaw = &v @@ -456,19 +484,19 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) str cur = &pend{} } if cur.gotZip { - return "duplicate extensions.zip_file pair" + return cfgParseBadRequest("duplicate extensions.zip_file pair") } tmp, err := os.CreateTemp("", "bcc-ext-*.zip") if err != nil { - return "temp extensions.zip_file" + return cfgParseInternal("temp extensions.zip_file") } st.allTemps = append(st.allTemps, tmp.Name()) if _, err := io.Copy(tmp, part); err != nil { tmp.Close() - return "read extensions.zip_file" + return cfgParseInternal("read extensions.zip_file") } if err := tmp.Close(); err != nil { - return "close extensions.zip_file" + return cfgParseInternal("close extensions.zip_file") } cur.zipTmp = tmp.Name() cur.gotZip = true @@ -478,18 +506,18 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) str } b, err := io.ReadAll(part) if err != nil { - return "read extensions.name" + return cfgParseInternal("read extensions.name") } nm := strings.TrimSpace(string(b)) if nm == "" || !nameRegex.MatchString(nm) { - return "invalid extensions.name" + return cfgParseBadRequest("invalid extensions.name") } if cur.name != "" { - return "duplicate extensions.name in pair" + return cfgParseBadRequest("duplicate extensions.name in pair") } cur.name = nm default: - return fmt.Sprintf("unknown form field %q", name) + return cfgParseBadRequest(fmt.Sprintf("unknown form field %q", name)) } if cur != nil && cur.gotZip && cur.name != "" { st.extItems = append(st.extItems, extensionZipItem{zipTemp: cur.zipTmp, name: cur.name}) @@ -497,9 +525,9 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) str } } if cur != nil && (!cur.gotZip || cur.name == "") { - return "each extension pair needs extensions.zip_file plus extensions.name" + return cfgParseBadRequest("each extension pair needs extensions.zip_file plus extensions.name") } - return "" + return nil } func chromiumPrepareProfileArchive(profilePath string, strip int) (preparedDir string, cleanup func(), err error) { @@ -650,11 +678,13 @@ func chromiumDisplayApplyWhileStopped(ctx context.Context, s *ApiService, plan * if mode == "xvfb" { s.xvfbResizeMu.Lock() err := s.resizeXvfb(ctx, w, h) + if err == nil { + s.clearViewportOverride() + } s.xvfbResizeMu.Unlock() if err != nil { return cfg500ConfigureStep(chromiumConfigureStepDisplay, err.Error()) } - s.clearViewportOverride() return nil } var err error @@ -701,7 +731,7 @@ func chromiumValidatePolicies(raw *string) (policy.ChromiumPolicyOverrides, erro } overrides, err := policy.NewChromiumPolicyOverrides(m) if err != nil { - return nil, err + return nil, cfgBadRequest(err.Error()) } if err := overrides.Validate(); err != nil { return nil, cfgBadRequest(err.Error()) diff --git a/server/cmd/api/api/chromium_configure_test.go b/server/cmd/api/api/chromium_configure_test.go index 28c9d5d3..e4f44f04 100644 --- a/server/cmd/api/api/chromium_configure_test.go +++ b/server/cmd/api/api/chromium_configure_test.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "errors" "io" "mime/multipart" "strings" @@ -99,6 +100,14 @@ func TestChromiumValidateFlags(t *testing.T) { } } +func TestChromiumValidatePoliciesBadRequest(t *testing.T) { + blocked := `{"ExtensionSettings":{}}` + _, err := chromiumValidatePolicies(&blocked) + require.Error(t, err) + var bad cfgBadRequestError + require.ErrorAs(t, err, &bad) +} + func TestChromiumParseDisplayPartsValidation(t *testing.T) { badJSON := `{bad-json` _, msg := chromiumParseDisplayParts(&badJSON) @@ -121,9 +130,9 @@ func TestChromiumCfgParseMultipart(t *testing.T) { br := multipart.NewReader(buf, w.Boundary()) st := &chromiumConfigureState{} - msg := chromiumCfgParseMultipart(br, st) + err := chromiumCfgParseMultipart(br, st) defer st.cleanup() - require.Empty(t, msg) + require.NoError(t, err) require.True(t, policiesContentNonEmpty(st.chromePoliciesJSON)) require.Equal(t, 2, st.stripComponents) @@ -187,9 +196,12 @@ func TestChromiumCfgParseMultipartValidation(t *testing.T) { require.NoError(t, w.Close()) st := &chromiumConfigureState{} - msg := chromiumCfgParseMultipart(multipart.NewReader(buf, w.Boundary()), st) + err := chromiumCfgParseMultipart(multipart.NewReader(buf, w.Boundary()), st) defer st.cleanup() - require.Equal(t, tc.want, msg) + require.EqualError(t, err, tc.want) + var parseErr chromiumCfgParseError + require.True(t, errors.As(err, &parseErr)) + require.False(t, parseErr.internal) }) } } @@ -212,9 +224,9 @@ func TestChromiumCfgParseMultipartMultipleExtensionPairs(t *testing.T) { require.NoError(t, w.Close()) st := &chromiumConfigureState{} - msg := chromiumCfgParseMultipart(multipart.NewReader(buf, w.Boundary()), st) + err = chromiumCfgParseMultipart(multipart.NewReader(buf, w.Boundary()), st) defer st.cleanup() - require.Empty(t, msg) + require.NoError(t, err) require.Len(t, st.extItems, 2) require.Equal(t, "one", st.extItems[0].name) require.Equal(t, "two", st.extItems[1].name) From eccf2a434a7ea46a7b7d7d46d7962ab389ee60ee Mon Sep 17 00:00:00 2001 From: Hiro Tamada Date: Thu, 21 May 2026 13:36:18 -0400 Subject: [PATCH 10/12] Harden Chromium supervisor restarts. Confirm stopped states after supervisor errors and verify DevTools readiness by dialing the current upstream instead of relying only on log notifications. Co-authored-by: Cursor --- server/cmd/api/api/chromium.go | 149 ++++++++++++++++++++++++--------- 1 file changed, 111 insertions(+), 38 deletions(-) diff --git a/server/cmd/api/api/chromium.go b/server/cmd/api/api/chromium.go index 01a910a7..772ba1e9 100644 --- a/server/cmd/api/api/chromium.go +++ b/server/cmd/api/api/chromium.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/kernel/kernel-images/server/lib/cdpclient" "github.com/kernel/kernel-images/server/lib/chromiumflags" "github.com/kernel/kernel-images/server/lib/logger" oapi "github.com/kernel/kernel-images/server/lib/oapi" @@ -345,45 +346,60 @@ func (s *ApiService) restartChromiumAndWait(ctx context.Context, operation strin log := logger.FromContext(ctx) start := time.Now() - // Begin listening for devtools URL updates, since we are about to restart Chromium - updates, cancelSub := s.upstreamMgr.Subscribe() - defer cancelSub() - - // Run supervisorctl restart with a new context to let it run beyond the lifetime of the http request. - // This lets us return as soon as the DevTools URL is updated. - errCh := make(chan error, 1) log.Info("restarting chromium via supervisorctl", "operation", operation) - go func() { - cmdCtx, cancelCmd := context.WithTimeout(context.WithoutCancel(ctx), 1*time.Minute) - defer cancelCmd() - out, err := exec.CommandContext(cmdCtx, "supervisorctl", supervisorctlArgv("restart", "chromium")...).CombinedOutput() - if err != nil { - log.Error("failed to restart chromium", "error", err, "out", string(out)) - errCh <- fmt.Errorf("supervisorctl restart failed: %w", err) - } - }() - - // Wait for either a new upstream, a restart error, or timeout - timeout := time.NewTimer(15 * time.Second) - defer timeout.Stop() - select { - case <-updates: - log.Info("devtools ready", "operation", operation, "elapsed", time.Since(start).String()) - return nil - case err := <-errCh: + if err := s.stopChromium(ctx); err != nil { return err - case <-timeout.C: - log.Info("devtools not ready in time", "operation", operation, "elapsed", time.Since(start).String()) - return fmt.Errorf("devtools not ready in time") } + if err := s.startChromiumAndWait(ctx, operation); err != nil { + return err + } + log.Info("chromium restart complete", "operation", operation, "elapsed", time.Since(start).String()) + return nil } const supervisorCtlConf = "/etc/supervisor/supervisord.conf" +const chromiumDevToolsReadyTimeout = 90 * time.Second func supervisorctlArgv(verb string, prog string) []string { return []string{"-c", supervisorCtlConf, verb, prog} } +func chromiumSupervisorStatus(ctx context.Context) (string, string, error) { + cmdCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) + defer cancel() + out, err := exec.CommandContext(cmdCtx, "supervisorctl", supervisorctlArgv("status", "chromium")...).CombinedOutput() + text := strings.TrimSpace(string(out)) + fields := strings.Fields(text) + if len(fields) >= 2 { + return fields[1], text, nil + } + if err != nil { + return "", text, err + } + return "", text, fmt.Errorf("unexpected supervisorctl status output: %q", text) +} + +func waitChromiumSupervisorStatus(ctx context.Context, want string, timeout time.Duration) (string, error) { + deadline := time.Now().Add(timeout) + var last string + for { + status, out, err := chromiumSupervisorStatus(ctx) + if err == nil && status == want { + return out, nil + } + if out != "" { + last = out + } + if time.Now().After(deadline) { + if err != nil { + return last, err + } + return last, fmt.Errorf("chromium did not reach %s within %s (last status: %s)", want, timeout, last) + } + time.Sleep(500 * time.Millisecond) + } +} + // stopChromium runs supervisorctl stop chromium and waits for the command to complete. func (s *ApiService) stopChromium(ctx context.Context) error { log := logger.FromContext(ctx) @@ -393,8 +409,24 @@ func (s *ApiService) stopChromium(ctx context.Context) error { out, err := exec.CommandContext(cmdCtx, "supervisorctl", supervisorctlArgv("stop", "chromium")...).CombinedOutput() if err != nil { log.Error("failed to stop chromium", "error", err, "out", string(out)) + status, statusOut, statusErr := chromiumSupervisorStatus(ctx) + if statusErr == nil { + switch status { + case "STOPPED": + log.Info("chromium already stopped after supervisorctl stop error", "status", statusOut) + return nil + case "STOPPING": + if stoppedOut, waitErr := waitChromiumSupervisorStatus(ctx, "STOPPED", 30*time.Second); waitErr == nil { + log.Info("chromium reached stopped after supervisorctl stop error", "status", stoppedOut) + return nil + } + } + } return fmt.Errorf("supervisorctl stop chromium failed: %w", err) } + if stoppedOut, waitErr := waitChromiumSupervisorStatus(ctx, "STOPPED", 30*time.Second); waitErr != nil { + log.Warn("chromium stop command completed but stopped status was not confirmed", "error", waitErr, "status", stoppedOut) + } return nil } @@ -403,12 +435,15 @@ func (s *ApiService) startChromiumAndWait(ctx context.Context, operation string) log := logger.FromContext(ctx) start := time.Now() + prevUpstream := s.upstreamMgr.Current() updates, cancelSub := s.upstreamMgr.Subscribe() defer cancelSub() errCh := make(chan error, 1) + doneCh := make(chan struct{}) log.Info("starting chromium via supervisorctl", "operation", operation) go func() { + defer close(doneCh) cmdCtx, cancelCmd := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Minute) defer cancelCmd() out, err := exec.CommandContext(cmdCtx, "supervisorctl", supervisorctlArgv("start", "chromium")...).CombinedOutput() @@ -418,17 +453,55 @@ func (s *ApiService) startChromiumAndWait(ctx context.Context, operation string) } }() - timeout := time.NewTimer(15 * time.Second) + timeout := time.NewTimer(chromiumDevToolsReadyTimeout) defer timeout.Stop() - select { - case <-updates: - log.Info("devtools ready", "operation", operation, "elapsed", time.Since(start).String()) - return nil - case err := <-errCh: - return err - case <-timeout.C: - log.Info("devtools not ready in time", "operation", operation, "elapsed", time.Since(start).String()) - return fmt.Errorf("devtools not ready in time") + ticker := time.NewTicker(250 * time.Millisecond) + defer ticker.Stop() + + commandDone := false + tryReady := func(upstream string, allowCurrent bool) bool { + if upstream == "" { + return false + } + if !allowCurrent && upstream == prevUpstream { + return false + } + dialCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Second) + defer cancel() + c, err := cdpclient.Dial(dialCtx, upstream) + if err != nil { + return false + } + _ = c.Close() + return true + } + + for { + select { + case upstream, ok := <-updates: + if ok && tryReady(upstream, false) { + log.Info("devtools ready", "operation", operation, "elapsed", time.Since(start).String()) + return nil + } + case err := <-errCh: + return err + case <-doneCh: + commandDone = true + doneCh = nil + if tryReady(s.upstreamMgr.Current(), true) { + log.Info("devtools ready", "operation", operation, "elapsed", time.Since(start).String()) + return nil + } + case <-ticker.C: + if commandDone && tryReady(s.upstreamMgr.Current(), true) { + log.Info("devtools ready", "operation", operation, "elapsed", time.Since(start).String()) + return nil + } + case <-timeout.C: + status, statusOut, _ := chromiumSupervisorStatus(ctx) + log.Info("devtools not ready in time", "operation", operation, "elapsed", time.Since(start).String(), "supervisor_status", statusOut) + return fmt.Errorf("devtools not ready in time (chromium status: %s)", status) + } } } From 182f0a88ab40b919887f0052777c099cd4aaebe2 Mon Sep 17 00:00:00 2001 From: Hiro Tamada Date: Thu, 21 May 2026 13:41:41 -0400 Subject: [PATCH 11/12] Rename configure endpoint. Expose batched session configuration at /configure and clean up public OpenAPI wording around execution order and profile archives. Co-authored-by: Cursor --- server/lib/oapi/oapi.go | 1107 ++++++++++++++++++++------------------- server/openapi.yaml | 19 +- 2 files changed, 565 insertions(+), 561 deletions(-) diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 2c6bda90..c720f8b3 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -2533,6 +2533,27 @@ type InternalError = Error // NotFoundError defines model for NotFoundError. type NotFoundError = Error +// PatchChromiumFlagsJSONBody defines parameters for PatchChromiumFlags. +type PatchChromiumFlagsJSONBody struct { + // Flags Chromium flags to merge (e.g., ["--kiosk", "--disable-gpu"]) + Flags []string `json:"flags"` +} + +// PatchChromiumPoliciesJSONBody defines parameters for PatchChromiumPolicies. +type PatchChromiumPoliciesJSONBody map[string]interface{} + +// UploadExtensionsAndRestartMultipartBody defines parameters for UploadExtensionsAndRestart. +type UploadExtensionsAndRestartMultipartBody struct { + // Extensions List of extensions to upload and activate + Extensions []struct { + // Name Folder name to place the extension under /home/kernel/extensions/ + Name string `json:"name"` + + // ZipFile Zip archive containing an unpacked Chromium extension (must include manifest.json) + ZipFile openapi_types.File `json:"zip_file"` + } `json:"extensions"` +} + // ChromiumConfigureMultipartBody defines parameters for ChromiumConfigure. type ChromiumConfigureMultipartBody struct { // ChromePolicies UTF-8 JSON policy override map — same semantics as PATCH /chromium/policies. @@ -2550,37 +2571,16 @@ type ChromiumConfigureMultipartBody struct { ZipFile openapi_types.File `json:"zip_file"` } `json:"extensions,omitempty"` - // ProfileArchive tar.zst of `/home/kernel/user-data` (V2 profiles). Stripped paths use strip_components optional part. + // ProfileArchive tar.zst archive containing the desired `/home/kernel/user-data` profile contents. Prefer archives whose root entries are the profile files/directories themselves (for example `Default/Preferences`). Use `strip_components` only when uploading an archive that includes leading wrapper directories. ProfileArchive *openapi_types.File `json:"profile_archive,omitempty"` // StartUrl URL text to navigate after configure. Bare hosts are normalized to https://, length is capped at 2048 bytes, and Chrome decides which schemes are navigable. StartUrl *string `json:"start_url,omitempty"` - // StripComponents Leading path components to strip when extracting profile_archive (non-negative integer as text). + // StripComponents Optional number of leading path components to strip from profile_archive entries (non-negative integer as text). StripComponents *string `json:"strip_components,omitempty"` } -// PatchChromiumFlagsJSONBody defines parameters for PatchChromiumFlags. -type PatchChromiumFlagsJSONBody struct { - // Flags Chromium flags to merge (e.g., ["--kiosk", "--disable-gpu"]) - Flags []string `json:"flags"` -} - -// PatchChromiumPoliciesJSONBody defines parameters for PatchChromiumPolicies. -type PatchChromiumPoliciesJSONBody map[string]interface{} - -// UploadExtensionsAndRestartMultipartBody defines parameters for UploadExtensionsAndRestart. -type UploadExtensionsAndRestartMultipartBody struct { - // Extensions List of extensions to upload and activate - Extensions []struct { - // Name Folder name to place the extension under /home/kernel/extensions/ - Name string `json:"name"` - - // ZipFile Zip archive containing an unpacked Chromium extension (must include manifest.json) - ZipFile openapi_types.File `json:"zip_file"` - } `json:"extensions"` -} - // DownloadDirZipParams defines parameters for DownloadDirZip. type DownloadDirZipParams struct { // Path Absolute directory path to archive and download. @@ -2685,9 +2685,6 @@ type StreamTelemetryEventsParams struct { LastEventID *string `json:"Last-Event-ID,omitempty"` } -// ChromiumConfigureMultipartRequestBody defines body for ChromiumConfigure for multipart/form-data ContentType. -type ChromiumConfigureMultipartRequestBody ChromiumConfigureMultipartBody - // PatchChromiumFlagsJSONRequestBody defines body for PatchChromiumFlags for application/json ContentType. type PatchChromiumFlagsJSONRequestBody PatchChromiumFlagsJSONBody @@ -2727,6 +2724,9 @@ type ScrollJSONRequestBody = ScrollRequest // TypeTextJSONRequestBody defines body for TypeText for application/json ContentType. type TypeTextJSONRequestBody = TypeTextRequest +// ChromiumConfigureMultipartRequestBody defines body for ChromiumConfigure for multipart/form-data ContentType. +type ChromiumConfigureMultipartRequestBody ChromiumConfigureMultipartBody + // PatchDisplayJSONRequestBody defines body for PatchDisplay for application/json ContentType. type PatchDisplayJSONRequestBody = PatchDisplayRequest @@ -3555,9 +3555,6 @@ func WithRequestEditorFn(fn RequestEditorFn) ClientOption { // The interface specification for the client above. type ClientInterface interface { - // ChromiumConfigureWithBody request with any body - ChromiumConfigureWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - // PatchChromiumFlagsWithBody request with any body PatchChromiumFlagsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -3627,6 +3624,9 @@ type ClientInterface interface { TypeText(ctx context.Context, body TypeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ChromiumConfigureWithBody request with any body + ChromiumConfigureWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + // PatchDisplayWithBody request with any body PatchDisplayWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -3783,18 +3783,6 @@ type ClientInterface interface { StreamTelemetryEvents(ctx context.Context, params *StreamTelemetryEventsParams, reqEditors ...RequestEditorFn) (*http.Response, error) } -func (c *Client) ChromiumConfigureWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewChromiumConfigureRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - func (c *Client) PatchChromiumFlagsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewPatchChromiumFlagsRequestWithBody(c.Server, contentType, body) if err != nil { @@ -4119,6 +4107,18 @@ func (c *Client) TypeText(ctx context.Context, body TypeTextJSONRequestBody, req return c.Client.Do(req) } +func (c *Client) ChromiumConfigureWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewChromiumConfigureRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) PatchDisplayWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewPatchDisplayRequestWithBody(c.Server, contentType, body) if err != nil { @@ -4815,35 +4815,6 @@ func (c *Client) StreamTelemetryEvents(ctx context.Context, params *StreamTeleme return c.Client.Do(req) } -// NewChromiumConfigureRequestWithBody generates requests for ChromiumConfigure with any type of body -func NewChromiumConfigureRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/chromium/configure") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - // NewPatchChromiumFlagsRequest calls the generic PatchChromiumFlags builder with application/json body func NewPatchChromiumFlagsRequest(server string, body PatchChromiumFlagsJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -5407,6 +5378,35 @@ func NewTypeTextRequestWithBody(server string, contentType string, body io.Reade return req, nil } +// NewChromiumConfigureRequestWithBody generates requests for ChromiumConfigure with any type of body +func NewChromiumConfigureRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/configure") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewPatchDisplayRequest calls the generic PatchDisplay builder with application/json body func NewPatchDisplayRequest(server string, body PatchDisplayJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -7050,9 +7050,6 @@ func WithBaseURL(baseURL string) ClientOption { // ClientWithResponsesInterface is the interface specification for the client with responses above. type ClientWithResponsesInterface interface { - // ChromiumConfigureWithBodyWithResponse request with any body - ChromiumConfigureWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ChromiumConfigureResponse, error) - // PatchChromiumFlagsWithBodyWithResponse request with any body PatchChromiumFlagsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchChromiumFlagsResponse, error) @@ -7122,6 +7119,9 @@ type ClientWithResponsesInterface interface { TypeTextWithResponse(ctx context.Context, body TypeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*TypeTextResponse, error) + // ChromiumConfigureWithBodyWithResponse request with any body + ChromiumConfigureWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ChromiumConfigureResponse, error) + // PatchDisplayWithBodyWithResponse request with any body PatchDisplayWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchDisplayResponse, error) @@ -7278,31 +7278,6 @@ type ClientWithResponsesInterface interface { StreamTelemetryEventsWithResponse(ctx context.Context, params *StreamTelemetryEventsParams, reqEditors ...RequestEditorFn) (*StreamTelemetryEventsResponse, error) } -type ChromiumConfigureResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *OkResponse - JSON400 *BadRequestError - JSON409 *ConflictError - JSON500 *ChromiumConfigureError -} - -// Status returns HTTPResponse.Status -func (r ChromiumConfigureResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r ChromiumConfigureResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - type PatchChromiumFlagsResponse struct { Body []byte HTTPResponse *http.Response @@ -7649,6 +7624,31 @@ func (r TypeTextResponse) StatusCode() int { return 0 } +type ChromiumConfigureResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *OkResponse + JSON400 *BadRequestError + JSON409 *ConflictError + JSON500 *ChromiumConfigureError +} + +// Status returns HTTPResponse.Status +func (r ChromiumConfigureResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ChromiumConfigureResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type PatchDisplayResponse struct { Body []byte HTTPResponse *http.Response @@ -8583,15 +8583,6 @@ func (r StreamTelemetryEventsResponse) StatusCode() int { return 0 } -// ChromiumConfigureWithBodyWithResponse request with arbitrary body returning *ChromiumConfigureResponse -func (c *ClientWithResponses) ChromiumConfigureWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ChromiumConfigureResponse, error) { - rsp, err := c.ChromiumConfigureWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseChromiumConfigureResponse(rsp) -} - // PatchChromiumFlagsWithBodyWithResponse request with arbitrary body returning *PatchChromiumFlagsResponse func (c *ClientWithResponses) PatchChromiumFlagsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchChromiumFlagsResponse, error) { rsp, err := c.PatchChromiumFlagsWithBody(ctx, contentType, body, reqEditors...) @@ -8823,6 +8814,15 @@ func (c *ClientWithResponses) TypeTextWithResponse(ctx context.Context, body Typ return ParseTypeTextResponse(rsp) } +// ChromiumConfigureWithBodyWithResponse request with arbitrary body returning *ChromiumConfigureResponse +func (c *ClientWithResponses) ChromiumConfigureWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ChromiumConfigureResponse, error) { + rsp, err := c.ChromiumConfigureWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseChromiumConfigureResponse(rsp) +} + // PatchDisplayWithBodyWithResponse request with arbitrary body returning *PatchDisplayResponse func (c *ClientWithResponses) PatchDisplayWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchDisplayResponse, error) { rsp, err := c.PatchDisplayWithBody(ctx, contentType, body, reqEditors...) @@ -9326,53 +9326,6 @@ func (c *ClientWithResponses) StreamTelemetryEventsWithResponse(ctx context.Cont return ParseStreamTelemetryEventsResponse(rsp) } -// ParseChromiumConfigureResponse parses an HTTP response from a ChromiumConfigureWithResponse call -func ParseChromiumConfigureResponse(rsp *http.Response) (*ChromiumConfigureResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &ChromiumConfigureResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest OkResponse - 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 == 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 == 409: - var dest ConflictError - 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 ChromiumConfigureError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON500 = &dest - - } - - return response, nil -} - // ParsePatchChromiumFlagsResponse parses an HTTP response from a PatchChromiumFlagsWithResponse call func ParsePatchChromiumFlagsResponse(rsp *http.Response) (*PatchChromiumFlagsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -9875,6 +9828,53 @@ func ParseTypeTextResponse(rsp *http.Response) (*TypeTextResponse, error) { return response, nil } +// ParseChromiumConfigureResponse parses an HTTP response from a ChromiumConfigureWithResponse call +func ParseChromiumConfigureResponse(rsp *http.Response) (*ChromiumConfigureResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ChromiumConfigureResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest OkResponse + 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 == 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 == 409: + var dest ConflictError + 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 ChromiumConfigureError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParsePatchDisplayResponse parses an HTTP response from a PatchDisplayWithResponse call func ParsePatchDisplayResponse(rsp *http.Response) (*PatchDisplayResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -11399,9 +11399,6 @@ func ParseStreamTelemetryEventsResponse(rsp *http.Response) (*StreamTelemetryEve // ServerInterface represents all server handlers. type ServerInterface interface { - // Apply batched Chromium filesystem and launch configuration plus optional navigation - // (POST /chromium/configure) - ChromiumConfigure(w http.ResponseWriter, r *http.Request) // Update Chromium launch flags and restart // (PATCH /chromium/flags) PatchChromiumFlags(w http.ResponseWriter, r *http.Request) @@ -11447,6 +11444,9 @@ type ServerInterface interface { // Type text on the host computer // (POST /computer/type) TypeText(w http.ResponseWriter, r *http.Request) + // Apply batched session and browser configuration plus optional navigation + // (POST /configure) + ChromiumConfigure(w http.ResponseWriter, r *http.Request) // Update display configuration // (PATCH /display) PatchDisplay(w http.ResponseWriter, r *http.Request) @@ -11570,12 +11570,6 @@ type ServerInterface interface { type Unimplemented struct{} -// Apply batched Chromium filesystem and launch configuration plus optional navigation -// (POST /chromium/configure) -func (_ Unimplemented) ChromiumConfigure(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotImplemented) -} - // Update Chromium launch flags and restart // (PATCH /chromium/flags) func (_ Unimplemented) PatchChromiumFlags(w http.ResponseWriter, r *http.Request) { @@ -11666,6 +11660,12 @@ func (_ Unimplemented) TypeText(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// Apply batched session and browser configuration plus optional navigation +// (POST /configure) +func (_ Unimplemented) ChromiumConfigure(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Update display configuration // (PATCH /display) func (_ Unimplemented) PatchDisplay(w http.ResponseWriter, r *http.Request) { @@ -11909,20 +11909,6 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(http.Handler) http.Handler -// ChromiumConfigure operation middleware -func (siw *ServerInterfaceWrapper) ChromiumConfigure(w http.ResponseWriter, r *http.Request) { - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.ChromiumConfigure(w, r) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - // PatchChromiumFlags operation middleware func (siw *ServerInterfaceWrapper) PatchChromiumFlags(w http.ResponseWriter, r *http.Request) { @@ -12133,6 +12119,20 @@ func (siw *ServerInterfaceWrapper) TypeText(w http.ResponseWriter, r *http.Reque handler.ServeHTTP(w, r) } +// ChromiumConfigure operation middleware +func (siw *ServerInterfaceWrapper) ChromiumConfigure(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ChromiumConfigure(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // PatchDisplay operation middleware func (siw *ServerInterfaceWrapper) PatchDisplay(w http.ResponseWriter, r *http.Request) { @@ -13088,9 +13088,6 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl ErrorHandlerFunc: options.ErrorHandlerFunc, } - r.Group(func(r chi.Router) { - r.Post(options.BaseURL+"/chromium/configure", wrapper.ChromiumConfigure) - }) r.Group(func(r chi.Router) { r.Patch(options.BaseURL+"/chromium/flags", wrapper.PatchChromiumFlags) }) @@ -13136,6 +13133,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/computer/type", wrapper.TypeText) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/configure", wrapper.ChromiumConfigure) + }) r.Group(func(r chi.Router) { r.Patch(options.BaseURL+"/display", wrapper.PatchDisplay) }) @@ -13265,50 +13265,6 @@ type InternalErrorJSONResponse Error type NotFoundErrorJSONResponse Error -type ChromiumConfigureRequestObject struct { - Body *multipart.Reader -} - -type ChromiumConfigureResponseObject interface { - VisitChromiumConfigureResponse(w http.ResponseWriter) error -} - -type ChromiumConfigure200JSONResponse OkResponse - -func (response ChromiumConfigure200JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type ChromiumConfigure400JSONResponse struct{ BadRequestErrorJSONResponse } - -func (response ChromiumConfigure400JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) - - return json.NewEncoder(w).Encode(response) -} - -type ChromiumConfigure409JSONResponse struct{ ConflictErrorJSONResponse } - -func (response ChromiumConfigure409JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(409) - - return json.NewEncoder(w).Encode(response) -} - -type ChromiumConfigure500JSONResponse ChromiumConfigureError - -func (response ChromiumConfigure500JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) - - return json.NewEncoder(w).Encode(response) -} - type PatchChromiumFlagsRequestObject struct { Body *PatchChromiumFlagsJSONRequestBody } @@ -13813,6 +13769,50 @@ func (response TypeText500JSONResponse) VisitTypeTextResponse(w http.ResponseWri return json.NewEncoder(w).Encode(response) } +type ChromiumConfigureRequestObject struct { + Body *multipart.Reader +} + +type ChromiumConfigureResponseObject interface { + VisitChromiumConfigureResponse(w http.ResponseWriter) error +} + +type ChromiumConfigure200JSONResponse OkResponse + +func (response ChromiumConfigure200JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ChromiumConfigure400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response ChromiumConfigure400JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type ChromiumConfigure409JSONResponse struct{ ConflictErrorJSONResponse } + +func (response ChromiumConfigure409JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + +type ChromiumConfigure500JSONResponse ChromiumConfigureError + +func (response ChromiumConfigure500JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type PatchDisplayRequestObject struct { Body *PatchDisplayJSONRequestBody } @@ -15552,9 +15552,6 @@ func (response StreamTelemetryEvents200TexteventStreamResponse) VisitStreamTelem // StrictServerInterface represents all server handlers. type StrictServerInterface interface { - // Apply batched Chromium filesystem and launch configuration plus optional navigation - // (POST /chromium/configure) - ChromiumConfigure(ctx context.Context, request ChromiumConfigureRequestObject) (ChromiumConfigureResponseObject, error) // Update Chromium launch flags and restart // (PATCH /chromium/flags) PatchChromiumFlags(ctx context.Context, request PatchChromiumFlagsRequestObject) (PatchChromiumFlagsResponseObject, error) @@ -15600,6 +15597,9 @@ type StrictServerInterface interface { // Type text on the host computer // (POST /computer/type) TypeText(ctx context.Context, request TypeTextRequestObject) (TypeTextResponseObject, error) + // Apply batched session and browser configuration plus optional navigation + // (POST /configure) + ChromiumConfigure(ctx context.Context, request ChromiumConfigureRequestObject) (ChromiumConfigureResponseObject, error) // Update display configuration // (PATCH /display) PatchDisplay(ctx context.Context, request PatchDisplayRequestObject) (PatchDisplayResponseObject, error) @@ -15748,37 +15748,6 @@ type strictHandler struct { options StrictHTTPServerOptions } -// ChromiumConfigure operation middleware -func (sh *strictHandler) ChromiumConfigure(w http.ResponseWriter, r *http.Request) { - var request ChromiumConfigureRequestObject - - if reader, err := r.MultipartReader(); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode multipart body: %w", err)) - return - } else { - request.Body = reader - } - - handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { - return sh.ssi.ChromiumConfigure(ctx, request.(ChromiumConfigureRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "ChromiumConfigure") - } - - response, err := handler(r.Context(), w, r, request) - - if err != nil { - sh.options.ResponseErrorHandlerFunc(w, r, err) - } else if validResponse, ok := response.(ChromiumConfigureResponseObject); ok { - if err := validResponse.VisitChromiumConfigureResponse(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)) - } -} - // PatchChromiumFlags operation middleware func (sh *strictHandler) PatchChromiumFlags(w http.ResponseWriter, r *http.Request) { var request PatchChromiumFlagsRequestObject @@ -16233,6 +16202,37 @@ func (sh *strictHandler) TypeText(w http.ResponseWriter, r *http.Request) { } } +// ChromiumConfigure operation middleware +func (sh *strictHandler) ChromiumConfigure(w http.ResponseWriter, r *http.Request) { + var request ChromiumConfigureRequestObject + + if reader, err := r.MultipartReader(); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode multipart body: %w", err)) + return + } else { + request.Body = reader + } + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ChromiumConfigure(ctx, request.(ChromiumConfigureRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ChromiumConfigure") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ChromiumConfigureResponseObject); ok { + if err := validResponse.VisitChromiumConfigureResponse(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)) + } +} + // PatchDisplay operation middleware func (sh *strictHandler) PatchDisplay(w http.ResponseWriter, r *http.Request) { var request PatchDisplayRequestObject @@ -17372,301 +17372,302 @@ func (sh *strictHandler) StreamTelemetryEvents(w http.ResponseWriter, r *http.Re // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "AA==", + "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", + "GJ8K0HgBAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 45f48b6a..0b11325d 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1170,19 +1170,19 @@ paths: "500": $ref: "#/components/responses/InternalError" - /chromium/configure: + /configure: post: - summary: Apply batched Chromium filesystem and launch configuration plus optional navigation + summary: Apply batched session and browser configuration plus optional navigation description: | Optional multipart parts apply configuration while Chromium stays stopped once (policy, flags, extensions, profile archive, optional display sizing), then Chromium is started exactly once and DevTools readiness is awaited. Optional `start_url` dispatches a best-effort navigation after readiness without waiting for page load. Bare hosts are normalized to `https://`. Omit any part you do not need. At least one actionable part must be present. - Required configuration steps run in the same logical order as the control-plane sync path - (policies, extensions, display, flags/kiosk, profile), but Chromium is started only once - at the end. The endpoint is not transactional: if a later required step fails, earlier - successful side effects may remain on the instance while the non-2xx error propagates. + Required configuration steps run in this order: policies, extensions, display, flags, + then profile archive. Chromium is started only once at the end. The endpoint is not + transactional: if a later required step fails, earlier successful side effects may remain + on the instance while the non-2xx error propagates. Prefer this over separate `/chromium/*` and `/display` calls when multiple restart-triggering steps apply in one session configure. operationId: chromiumConfigure @@ -1208,11 +1208,14 @@ paths: type: string profile_archive: description: >- - tar.zst of `/home/kernel/user-data` (V2 profiles). Stripped paths use strip_components optional part. + tar.zst archive containing the desired `/home/kernel/user-data` profile contents. + Prefer archives whose root entries are the profile files/directories themselves + (for example `Default/Preferences`). Use `strip_components` only when uploading + an archive that includes leading wrapper directories. type: string format: binary strip_components: - description: Leading path components to strip when extracting profile_archive (non-negative integer as text). + description: Optional number of leading path components to strip from profile_archive entries (non-negative integer as text). type: string start_url: description: >- From 569d99f46a67ee056c54297a06201beab75d2f75 Mon Sep 17 00:00:00 2001 From: Hiro Tamada Date: Thu, 21 May 2026 15:42:16 -0400 Subject: [PATCH 12/12] Preserve configure restart safety state. Only mark Chromium as restarted after DevTools readiness succeeds so deferred recovery can retry failed starts. Co-authored-by: Cursor --- server/cmd/api/api/chromium_configure.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go index 4be250a8..167ac529 100644 --- a/server/cmd/api/api/chromium_configure.go +++ b/server/cmd/api/api/chromium_configure.go @@ -83,8 +83,11 @@ func (s *ApiService) ChromiumConfigure(ctx context.Context, request oapi.Chromiu if !chromiumStopped { return nil } + if err := s.startChromiumAndWait(ctx, "batched chromium configure"); err != nil { + return err + } chromiumStopped = false - return s.startChromiumAndWait(ctx, "batched chromium configure") + return nil } defer func() { if restartErr := restartAfterStop(); restartErr != nil {