From 977be8b39ac1e650746ed632214044680fed7746 Mon Sep 17 00:00:00 2001 From: raiden-staging Date: Tue, 4 Nov 2025 01:57:39 +0100 Subject: [PATCH 1/3] audio support (rev2) --- images/chromium-headful/Dockerfile | 16 +++++- .../client/src/components/video.vue | 55 ++++++++++++++++++- images/chromium-headful/daemon.conf | 43 +++++++++++++++ images/chromium-headful/dbus-mpris.conf | 27 +++++++++ images/chromium-headful/dbus-pulseaudio.conf | 27 +++++++++ images/chromium-headful/default.pa | 17 ++++++ images/chromium-headful/start-pulseaudio.sh | 28 +++++++--- images/chromium-headful/wrapper.sh | 33 +++++++++-- images/chromium-headful/xorg.conf | 2 + 9 files changed, 232 insertions(+), 16 deletions(-) create mode 100644 images/chromium-headful/daemon.conf create mode 100644 images/chromium-headful/dbus-mpris.conf create mode 100644 images/chromium-headful/dbus-pulseaudio.conf create mode 100644 images/chromium-headful/default.pa diff --git a/images/chromium-headful/Dockerfile b/images/chromium-headful/Dockerfile index 4e87c9a7..0e72f32a 100644 --- a/images/chromium-headful/Dockerfile +++ b/images/chromium-headful/Dockerfile @@ -162,7 +162,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptca apt-get update; \ apt-get --no-install-recommends -y install \ wget ca-certificates python2 supervisor xclip xdotool \ - pulseaudio dbus-x11 xserver-xorg-video-dummy \ + pulseaudio dbus-x11 xserver-xorg-video-dummy rtkit upower \ libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx7 \ x11-xserver-utils \ gstreamer1.0-plugins-base gstreamer1.0-plugins-good \ @@ -187,7 +187,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptca /home/$USERNAME/.local/share/xorg; \ chmod 1777 /var/log/neko; \ chown $USERNAME /var/log/neko/ /tmp/runtime-$USERNAME; \ - chown -R $USERNAME:$USERNAME /home/$USERNAME; + chown -R $USERNAME:$USERNAME /home/$USERNAME; \ + chmod 777 /etc/pulse; # install chromium and sqlite3 for debugging the cookies file RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptcache \ @@ -225,6 +226,10 @@ ENV WITHDOCKER=true COPY images/chromium-headful/xorg.conf /etc/neko/xorg.conf COPY images/chromium-headful/neko.yaml /etc/neko/neko.yaml +COPY images/chromium-headful/default.pa /etc/pulse/default.pa +COPY images/chromium-headful/daemon.conf /etc/pulse/daemon.conf +COPY images/chromium-headful/dbus-pulseaudio.conf /etc/dbus-1/system.d/pulseaudio.conf +COPY images/chromium-headful/dbus-mpris.conf /etc/dbus-1/system.d/mpris.conf COPY --from=neko /usr/bin/neko /usr/bin/neko COPY --from=client /src/dist/ /var/www COPY --from=xorg-deps /usr/local/lib/xorg/modules/drivers/dummy_drv.so /usr/lib/xorg/modules/drivers/dummy_drv.so @@ -244,6 +249,11 @@ COPY --from=server-builder /out/chromium-launcher /usr/local/bin/chromium-launch # Copy the Playwright executor runtime COPY server/runtime/playwright-executor.ts /usr/local/lib/playwright-executor.ts -RUN useradd -m -s /bin/bash kernel +RUN useradd -m -s /bin/bash kernel && \ + usermod -aG audio,video,pulse,pulse-access kernel + +# Environment variables for audio +ENV XDG_RUNTIME_DIR=/tmp/runtime-kernel +ENV PULSE_SERVER=unix:/tmp/runtime-kernel/pulse/native ENTRYPOINT [ "/wrapper.sh" ] diff --git a/images/chromium-headful/client/src/components/video.vue b/images/chromium-headful/client/src/components/video.vue index 4c4fe0f8..744033ac 100644 --- a/images/chromium-headful/client/src/components/video.vue +++ b/images/chromium-headful/client/src/components/video.vue @@ -1,6 +1,6 @@ diff --git a/images/chromium-headful/client/public/kernel.svg b/images/chromium-headful/client/public/kernel.svg new file mode 100644 index 00000000..b9a86da4 --- /dev/null +++ b/images/chromium-headful/client/public/kernel.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/chromium-headful/client/public/site.webmanifest b/images/chromium-headful/client/public/site.webmanifest index 03c2b31f..4d1b7924 100644 --- a/images/chromium-headful/client/public/site.webmanifest +++ b/images/chromium-headful/client/public/site.webmanifest @@ -13,7 +13,7 @@ "type": "image/png" } ], - "theme_color": "#19bd9c", - "background_color": "#19bd9c", + "theme_color": "#7B42F6", + "background_color": "#7B42F6", "display": "standalone" } diff --git a/images/chromium-headful/client/src/assets/styles/_variables.scss b/images/chromium-headful/client/src/assets/styles/_variables.scss index f74ac679..de85520b 100644 --- a/images/chromium-headful/client/src/assets/styles/_variables.scss +++ b/images/chromium-headful/client/src/assets/styles/_variables.scss @@ -21,7 +21,7 @@ $background-modifier-accent: hsla(0, 0%, 100%, 0.06); $elevation-low: 0 1px 0 rgba(4, 4, 5, 0.2), 0 1.5px 0 rgba(6, 6, 7, 0.05), 0 2px 0 rgba(4, 4, 5, 0.05); $elevation-high: 0 8px 16px rgba(0, 0, 0, 0.24); -$style-primary: #19bd9c; +$style-primary: #7B42F6; $style-error: #d32f2f; $menu-height: 40px; diff --git a/images/chromium-headful/client/src/components/connect.vue b/images/chromium-headful/client/src/components/connect.vue index c850c642..b0106d75 100644 --- a/images/chromium-headful/client/src/components/connect.vue +++ b/images/chromium-headful/client/src/components/connect.vue @@ -11,8 +11,7 @@
-
-
+
@@ -103,40 +102,25 @@ .loader { width: 90px; height: 90px; - position: relative; margin: 0 auto; + display: flex; + justify-content: center; + align-items: center; - .bounce1, - .bounce2 { + .spinning-logo { width: 100%; height: 100%; - border-radius: 50%; - background-color: $style-primary; - opacity: 0.6; - position: absolute; - top: 0; - left: 0; - - -webkit-animation: bounce 2s infinite ease-in-out; - animation: bounce 2s infinite ease-in-out; - } - - .bounce2 { - -webkit-animation-delay: -1s; - animation-delay: -1s; + animation: spin 2s linear infinite; } } } - @keyframes bounce { - 0%, - 100% { - transform: scale(0); - -webkit-transform: scale(0); + @keyframes spin { + from { + transform: rotate(0deg); } - 50% { - transform: scale(1); - -webkit-transform: scale(1); + to { + transform: rotate(360deg); } } } @@ -221,4 +205,4 @@ this.$accessor.client.toggleAbout() } } - + \ No newline at end of file From 2050f1ff619d95835c9f2dae0175203b6a3ed52f Mon Sep 17 00:00:00 2001 From: raiden-staging Date: Tue, 2 Dec 2025 14:04:26 +0100 Subject: [PATCH 3/3] [audio] sync to latest --- images/README.md | 15 + images/chromium-headful/Dockerfile | 174 ++++-- images/chromium-headful/build-unikernel.sh | 4 +- images/chromium-headful/client/Dockerfile | 9 +- .../client/src/components/connect.vue | 2 +- .../client/src/components/video.vue | 4 +- images/chromium-headful/daemon.conf | 2 +- images/chromium-headful/dbus-mpris.conf | 2 +- images/chromium-headful/dbus-pulseaudio.conf | 2 +- images/chromium-headful/default.pa | 2 +- images/chromium-headful/run-docker.sh | 4 + images/chromium-headful/run-unikernel.sh | 2 +- images/chromium-headful/start-pulseaudio.sh | 2 +- images/chromium-headful/xorg-deps/Dockerfile | 18 +- .../xorg-deps/xf86-input-neko/Dockerfile | 14 +- images/chromium-headless/build-unikernel.sh | 2 +- images/chromium-headless/image/Dockerfile | 141 +++-- images/chromium-headless/image/wrapper.sh | 4 - images/chromium-headless/run-docker.sh | 4 + images/chromium-headless/run-unikernel.sh | 2 +- server/cmd/api/api/computer.go | 74 ++- server/lib/oapi/oapi.go | 494 +++++++++++++----- server/openapi.yaml | 30 ++ server/runtime/playwright-executor.ts | 5 +- shared/ensure-common-build-run-vars.sh | 28 +- 25 files changed, 778 insertions(+), 262 deletions(-) create mode 100644 images/README.md diff --git a/images/README.md b/images/README.md new file mode 100644 index 00000000..0a2462ac --- /dev/null +++ b/images/README.md @@ -0,0 +1,15 @@ +## How to test kernel-images changes locally with docker + +- Make relevant changes to kernel-images example adding a new endpoint at `kernel-images/server/cmd/api/api/computer.go`, example I added `SetCursor()` endpoint. +- Run openApi to generate the boilerplate for the new endpoints with make oapi-generate +- Check changes at `kernel-images/server/lib/oapi/oapi.go` +- `cd kernel-images/images/chromium-headful` +- Build and run the docker image with `./build-docker.sh && ENABLE_WEBRTC=true ./run-docker.sh` +- Open http://localhost:8080/ in your browser +- Now new endpoint should be available for tests example curl command: +```sh +curl -X POST localhost:444/computer/cursor \ + -H "Content-Type: application/json" \ + -d '{"hidden": true}' +``` + diff --git a/images/chromium-headful/Dockerfile b/images/chromium-headful/Dockerfile index 0e72f32a..031d5545 100644 --- a/images/chromium-headful/Dockerfile +++ b/images/chromium-headful/Dockerfile @@ -1,51 +1,69 @@ FROM docker.io/golang:1.25.0 AS server-builder WORKDIR /workspace/server -ARG TARGETOS +# Allow cross-compilation when building with BuildKit platforms ARG TARGETARCH +ARG TARGETOS +ARG CACHEIDPREFIX=${TARGETOS:-linux}-${TARGETARCH:-amd64}-golang1250 ENV CGO_ENABLED=0 COPY server/go.mod ./ COPY server/go.sum ./ -RUN --mount=type=cache,target=/root/.cache/go-build \ - --mount=type=cache,target=/go/pkg/mod \ +RUN --mount=type=cache,target=/root/.cache/go-build,id=$CACHEIDPREFIX-go-build \ + --mount=type=cache,target=/go/pkg/mod,id=$CACHEIDPREFIX-go-pkg-mod \ go mod download COPY server/ . # Build kernel-images API -RUN --mount=type=cache,target=/root/.cache/go-build \ - --mount=type=cache,target=/go/pkg/mod \ +RUN --mount=type=cache,target=/root/.cache/go-build,id=$CACHEIDPREFIX-go-build \ + --mount=type=cache,target=/go/pkg/mod,id=$CACHEIDPREFIX-go-pkg-mod \ GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \ go build -ldflags="-s -w" -o /out/kernel-images-api ./cmd/api # Build chromium launcher -RUN GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \ +RUN --mount=type=cache,target=/root/.cache/go-build,id=$CACHEIDPREFIX-go-build \ + --mount=type=cache,target=/go/pkg/mod,id=$CACHEIDPREFIX-go-pkg-mod \ + GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \ go build -ldflags="-s -w" -o /out/chromium-launcher ./cmd/chromium-launcher # webrtc client FROM node:22-bullseye-slim AS client WORKDIR /src + +# Allow cross-compilation when building with BuildKit platforms +ARG TARGETARCH +ARG TARGETOS +ARG CACHEIDPREFIX=${TARGETOS:-linux}-${TARGETARCH:-amd64}-node22bullseye + COPY images/chromium-headful/client/package*.json ./ -RUN --mount=type=cache,target=/root/.npm npm install +RUN --mount=type=cache,target=/root/.npm,id=$CACHEIDPREFIX-npm npm install COPY images/chromium-headful/client/ . -RUN --mount=type=cache,target=/root/.npm npm run build +RUN --mount=type=cache,target=/root/.npm,id=$CACHEIDPREFIX-npm npm run build # xorg dependencies FROM docker.io/ubuntu:22.04 AS xorg-deps WORKDIR /xorg + +# Allow cross-compilation when building with BuildKit platforms +ARG TARGETARCH +ARG TARGETOS +ARG CACHEIDPREFIX=${TARGETOS:-linux}-${TARGETARCH:-amd64}-ubuntu2204 + ENV DEBIAN_FRONTEND=noninteractive -RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptcache \ - --mount=type=cache,target=/var/lib/apt,sharing=private,id=ubuntu2204-aptlib \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ + set -eux; \ rm -f /etc/apt/apt.conf.d/docker-clean; \ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \ - set -eux; \ apt-get update; \ apt-get --no-install-recommends -y install \ git gcc pkgconf autoconf automake libtool make xorg-dev xutils-dev; COPY images/chromium-headful/xorg-deps/ /xorg/ # build xf86-video-dummy v0.3.8 with RandR support -RUN set -eux; \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ + set -eux; \ cd xf86-video-dummy/v0.3.8; \ patch -p1 < ../01_v0.3.8_xdummy-randr.patch; \ autoreconf -v --install; \ @@ -53,23 +71,96 @@ RUN set -eux; \ make -j$(nproc); \ make install; # build custom input driver -RUN set -eux; \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ + set -eux; \ cd xf86-input-neko; \ ./autogen.sh --prefix=/usr; \ ./configure; \ make -j$(nproc); \ make install; +FROM docker.io/ubuntu:22.04 AS ffmpeg-downloader + +# Allow cross-compilation when building with BuildKit platforms +ARG TARGETARCH +ARG TARGETOS +ARG CACHEIDPREFIX=${TARGETOS:-linux}-${TARGETARCH:-amd64}-ubuntu2204 + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ + set -xe; \ + rm -f /etc/apt/apt.conf.d/docker-clean; \ + echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \ + apt-get -yqq update; \ + apt-get -yqq --no-install-recommends install ca-certificates curl xz-utils; + +# Download FFmpeg (latest static build) for the recording server +RUN --mount=type=cache,target=/tmp/cache/ffmpeg,sharing=locked,id=$CACHEIDPREFIX-ffmpeg \ + <<-'EOT' + set -eux + FFMPEG_CACHE_PATH="/tmp/cache/ffmpeg" + case ${TARGETARCH:-amd64} in + "amd64") FFMPEG_TARGET_ARCH="64" ;; + "arm64") FFMPEG_TARGET_ARCH="arm64" ;; + esac + FFMPEG_TARGET=linux${FFMPEG_TARGET_ARCH:?} + ARCHIVE_NAME="ffmpeg-n7.1-latest-${FFMPEG_TARGET}-gpl-7.1.tar.xz" + FFMPEG_CACHED_ARCHIVE_PATH="$FFMPEG_CACHE_PATH/$ARCHIVE_NAME" + FFMPEG_CACHED_ARCHIVE_CHECKSUM_PATH="$FFMPEG_CACHED_ARCHIVE_PATH.sha256" + URL="https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/$ARCHIVE_NAME" + TEMPORARY_SHA256_CHECKSUM_PATH=$(mktemp /tmp/tmp_sha256.XXXXXXXXXX) + CONTINUE="true" + echo "Downloading FFmpeg checksum" + if curl --connect-timeout 10 -fsSL "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/checksums.sha256" -o $TEMPORARY_SHA256_CHECKSUM_PATH; then + grep -F "$ARCHIVE_NAME" $TEMPORARY_SHA256_CHECKSUM_PATH > $FFMPEG_CACHED_ARCHIVE_CHECKSUM_PATH + else + echo "Failed to connect to ffmpeg static build provider for checksum." + echo "Checking for cached version to use." + if [ -f "$FFMPEG_CACHED_ARCHIVE_CHECKSUM_PATH" ]; then + echo "Found cached checksum." + else + echo "Unable to locate cached checksum." + CONTINUE="false" + fi + fi + rm $TEMPORARY_SHA256_CHECKSUM_PATH + + if [ "$CONTINUE" = "false" ]; then + exit 1 + fi + + echo "Checking cache for FFmpeg archive and validating checksum" + if (cd $FFMPEG_CACHE_PATH && sha256sum --check $FFMPEG_CACHED_ARCHIVE_CHECKSUM_PATH); then + echo "Checksum validated, using cached FFmpeg archive" + else + echo "Downloading FFmpeg static build from $URL" + curl -fsSL "$URL" -o $FFMPEG_CACHED_ARCHIVE_PATH + echo "Validating checksum of FFmpeg static build download" + (cd $FFMPEG_CACHE_PATH && sha256sum --check $FFMPEG_CACHED_ARCHIVE_CHECKSUM_PATH) + fi + + tar -xJf $FFMPEG_CACHED_ARCHIVE_PATH -C /tmp + install -m755 /tmp/ffmpeg-*/bin/ffmpeg /usr/local/bin/ffmpeg + install -m755 /tmp/ffmpeg-*/bin/ffprobe /usr/local/bin/ffprobe + rm -rf /tmp/ffmpeg* +EOT + FROM ghcr.io/onkernel/neko/base:3.0.8-v1.3.0 AS neko # ^--- now has event.SYSTEM_PONG with legacy support to keepalive FROM node:22-bullseye-slim AS node-22 FROM docker.io/ubuntu:22.04 +# Allow cross-compilation when building with BuildKit platforms +ARG TARGETARCH +ARG TARGETOS +ARG CACHEIDPREFIX=${TARGETOS:-linux}-${TARGETARCH:-amd64}-ubuntu2204 + ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_PRIORITY=high -RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptcache \ - --mount=type=cache,target=/var/lib/apt,sharing=private,id=ubuntu2204-aptlib \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ rm -f /etc/apt/apt.conf.d/docker-clean; \ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \ apt-get update && \ @@ -119,49 +210,20 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptca fonts-nanum \ fontconfig \ unzip && \ - apt-get clean && fc-cache -f + fc-cache -f # install ffmpeg manually since the version available in apt is from the 4.x branch due to #drama. -# as of writing these static builds will be the latest 7.0.x release. -RUN --mount=type=cache,target=/tmp/cache/ffmpeg,id=ffmpeg \ - <<-'EOT' - set -eux - URL="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz" - echo "Downloading FFmpeg MD5 checksum" - if ! curl --connect-timeout 10 -fsSL "${URL}.md5" -o /tmp/cache/ffmpeg/ffmpeg.tar.xz.md5; then - echo "Failed to connect to ffmpeg static build provider for MD5 checksum." - echo "Checking for cached version to use." - if [ ! -f /tmp/cache/ffmpeg/ffmpeg.tar.xz.md5 ]; then - echo "Unable to locate cached MD5 checksum. Exiting." - exit 1 - else - echo "Found cached MD5 checksum." - fi - fi - sed -i -e 's/ .*$/ \/tmp\/cache\/ffmpeg\/ffmpeg.tar.xz/' /tmp/cache/ffmpeg/ffmpeg.tar.xz.md5 - echo "Checking cache for FFmpeg archive and validating MD5 checksum" - if md5sum --check /tmp/cache/ffmpeg/ffmpeg.tar.xz.md5; then - echo "Checksum validated, using cached FFmpeg archive" - else - echo "Downloading FFmpeg static build from $URL" - curl -fsSL "$URL" -o /tmp/cache/ffmpeg/ffmpeg.tar.xz - echo "Validating MD5 checksum of FFmpeg static build download" - md5sum --check /tmp/cache/ffmpeg/ffmpeg.tar.xz.md5 - fi - tar -xJf /tmp/cache/ffmpeg/ffmpeg.tar.xz -C /tmp - install -m755 /tmp/ffmpeg-*/ffmpeg /usr/local/bin/ffmpeg - install -m755 /tmp/ffmpeg-*/ffprobe /usr/local/bin/ffprobe - rm -rf /tmp/ffmpeg* -EOT +COPY --from=ffmpeg-downloader /usr/local/bin/ffmpeg /usr/local/bin/ffmpeg +COPY --from=ffmpeg-downloader /usr/local/bin/ffprobe /usr/local/bin/ffprobe # runtime ENV USERNAME=root -RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptcache \ - --mount=type=cache,target=/var/lib/apt,sharing=private,id=ubuntu2204-aptlib \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ set -eux; \ apt-get update; \ apt-get --no-install-recommends -y install \ - wget ca-certificates python2 supervisor xclip xdotool \ + wget ca-certificates python2 supervisor xclip xdotool unclutter \ pulseaudio dbus-x11 xserver-xorg-video-dummy rtkit upower \ libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx7 \ x11-xserver-utils \ @@ -191,11 +253,11 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptca chmod 777 /etc/pulse; # install chromium and sqlite3 for debugging the cookies file -RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptcache \ - --mount=type=cache,target=/var/lib/apt,sharing=private,id=ubuntu2204-aptlib \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ add-apt-repository -y ppa:xtradeb/apps; -RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptcache \ - --mount=type=cache,target=/var/lib/apt,sharing=private,id=ubuntu2204-aptlib \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ apt update -y && \ apt -y install chromium && \ apt --no-install-recommends -y install sqlite3; @@ -215,8 +277,8 @@ RUN set -eux; \ ln -sf /usr/local/lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack; \ fi -# Install TypeScript and Playwright globally -RUN --mount=type=cache,target=/root/.npm npm install -g typescript playwright-core tsx +# Install TypeScript, Playwright, Patchright globally +RUN --mount=type=cache,target=/root/.npm,id=$CACHEIDPREFIX-npm npm install -g typescript playwright-core patchright tsx # setup desktop env & app ENV DISPLAY_NUM=1 diff --git a/images/chromium-headful/build-unikernel.sh b/images/chromium-headful/build-unikernel.sh index ab860b43..7730468a 100755 --- a/images/chromium-headful/build-unikernel.sh +++ b/images/chromium-headful/build-unikernel.sh @@ -3,7 +3,7 @@ # Move to the script's directory so relative paths work regardless of the caller CWD SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) cd "$SCRIPT_DIR" -source "$SCRIPT_DIR/../../shared/ensure-common-build-run-vars.sh" chromium-headful +source "$SCRIPT_DIR/../../shared/ensure-common-build-run-vars.sh" chromium-headful require-ukc-vars source "$SCRIPT_DIR/../../shared/erofs-utils.sh" # Ensure the mkfs.erofs tool is available @@ -25,6 +25,8 @@ docker cp cnt-"$app_name":/ ./.rootfs rm -f initrd || true sudo mkfs.erofs --all-root -d2 -E noinline_data -b 4096 initrd ./.rootfs +echo "Image index/name: $UKC_INDEX/$IMAGE" + # Package the unikernel (and the new initrd) to KraftCloud kraft pkg \ --name $UKC_INDEX/$IMAGE \ diff --git a/images/chromium-headful/client/Dockerfile b/images/chromium-headful/client/Dockerfile index 85e77c36..44b9eccf 100644 --- a/images/chromium-headful/client/Dockerfile +++ b/images/chromium-headful/client/Dockerfile @@ -3,15 +3,20 @@ FROM $BASE_IMAGE AS client WORKDIR /src +# Allow cross-compilation when building with BuildKit platforms +ARG TARGETARCH +ARG TARGETOS +ARG CACHEIDPREFIX=${TARGETOS:-linux}-${TARGETARCH:-amd64}-node18bullseye + # # install dependencies COPY package*.json ./ -RUN --mount=type=cache,target=/root/.npm npm install +RUN --mount=type=cache,target=/root/.npm,id=$CACHEIDPREFIX-npm npm install # # build client COPY . . -RUN --mount=type=cache,target=/root/.npm npm run build +RUN --mount=type=cache,target=/root/.npm,id=$CACHEIDPREFIX-npm npm run build # # artifacts from this stage diff --git a/images/chromium-headful/client/src/components/connect.vue b/images/chromium-headful/client/src/components/connect.vue index b0106d75..2d175244 100644 --- a/images/chromium-headful/client/src/components/connect.vue +++ b/images/chromium-headful/client/src/components/connect.vue @@ -205,4 +205,4 @@ this.$accessor.client.toggleAbout() } } - \ No newline at end of file + diff --git a/images/chromium-headful/client/src/components/video.vue b/images/chromium-headful/client/src/components/video.vue index 744033ac..69b5bb27 100644 --- a/images/chromium-headful/client/src/components/video.vue +++ b/images/chromium-headful/client/src/components/video.vue @@ -497,11 +497,9 @@ if (this.hasInteracted || !this.muted) { return } - this.hasInteracted = true this.unmute() this.$accessor.video.setVolume(100) - // Clean up global listeners if they were set if (this.unmuteHandler) { document.documentElement.removeEventListener('mousedown', this.unmuteHandler) @@ -509,6 +507,7 @@ } } + mounted() { this._container.addEventListener('resize', this.onResize) this.onVolumeChanged(this.volume) @@ -577,6 +576,7 @@ this.unmuteHandler = this._unmuteOnFirstInteraction.bind(this) document.documentElement.addEventListener('mousedown', this.unmuteHandler, { once: true }) document.documentElement.addEventListener('keydown', this.unmuteHandler, { once: true }) + } beforeDestroy() { diff --git a/images/chromium-headful/daemon.conf b/images/chromium-headful/daemon.conf index fa4c5ce0..efccf92f 100644 --- a/images/chromium-headful/daemon.conf +++ b/images/chromium-headful/daemon.conf @@ -40,4 +40,4 @@ resample-method = speex-float-1 # Critical changes for low-latency default-fragments = 5 -default-fragment-size-msec = 5 +default-fragment-size-msec = 5 \ No newline at end of file diff --git a/images/chromium-headful/dbus-mpris.conf b/images/chromium-headful/dbus-mpris.conf index a366d7c7..2ed00b6d 100644 --- a/images/chromium-headful/dbus-mpris.conf +++ b/images/chromium-headful/dbus-mpris.conf @@ -24,4 +24,4 @@ - + \ No newline at end of file diff --git a/images/chromium-headful/dbus-pulseaudio.conf b/images/chromium-headful/dbus-pulseaudio.conf index 595deda5..ae378b25 100644 --- a/images/chromium-headful/dbus-pulseaudio.conf +++ b/images/chromium-headful/dbus-pulseaudio.conf @@ -24,4 +24,4 @@ - + \ No newline at end of file diff --git a/images/chromium-headful/default.pa b/images/chromium-headful/default.pa index a7aee410..518545d1 100644 --- a/images/chromium-headful/default.pa +++ b/images/chromium-headful/default.pa @@ -14,4 +14,4 @@ load-module module-virtual-source source_name=microphone master=audio_input.moni load-module module-native-protocol-unix auth-anonymous=1 ### Make sure we always have a sink around, even if it is a null sink. -load-module module-always-sink +load-module module-always-sink \ No newline at end of file diff --git a/images/chromium-headful/run-docker.sh b/images/chromium-headful/run-docker.sh index 0202b91a..c3a2972b 100755 --- a/images/chromium-headful/run-docker.sh +++ b/images/chromium-headful/run-docker.sh @@ -66,6 +66,10 @@ RUN_ARGS=( --mount type=bind,src="$FLAGS_FILE",dst=/chromium/flags,ro ) +if [[ -n "${PLAYWRIGHT_ENGINE:-}" ]]; then + RUN_ARGS+=( -e PLAYWRIGHT_ENGINE="$PLAYWRIGHT_ENGINE" ) +fi + # WebRTC port mapping if [[ "${ENABLE_WEBRTC:-}" == "true" ]]; then echo "Running container with WebRTC" diff --git a/images/chromium-headful/run-unikernel.sh b/images/chromium-headful/run-unikernel.sh index 83361161..4c21c70e 100755 --- a/images/chromium-headful/run-unikernel.sh +++ b/images/chromium-headful/run-unikernel.sh @@ -4,7 +4,7 @@ set -euo pipefail # Move to the script's directory so relative paths work regardless of the caller CWD SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) cd "$SCRIPT_DIR" -source ../../shared/ensure-common-build-run-vars.sh chromium-headful +source ../../shared/ensure-common-build-run-vars.sh chromium-headful require-ukc-vars kraft cloud inst rm $NAME || true diff --git a/images/chromium-headful/start-pulseaudio.sh b/images/chromium-headful/start-pulseaudio.sh index 9a56828f..24304256 100644 --- a/images/chromium-headful/start-pulseaudio.sh +++ b/images/chromium-headful/start-pulseaudio.sh @@ -22,4 +22,4 @@ exec runuser -u kernel -- env \ pulseaudio --log-level=error \ --disallow-module-loading \ --disallow-exit \ - --exit-idle-time=-1 + --exit-idle-time=-1 \ No newline at end of file diff --git a/images/chromium-headful/xorg-deps/Dockerfile b/images/chromium-headful/xorg-deps/Dockerfile index 5e38145c..8f61945d 100644 --- a/images/chromium-headful/xorg-deps/Dockerfile +++ b/images/chromium-headful/xorg-deps/Dockerfile @@ -3,12 +3,16 @@ FROM $BASE_IMAGE AS xorg-deps WORKDIR /xorg +# Allow cross-compilation when building with BuildKit platforms +ARG TARGETARCH +ARG TARGETOS +ARG CACHEIDPREFIX=${TARGETOS:-linux}-${TARGETARCH:-amd64}-ubuntu2204 ENV DEBIAN_FRONTEND=noninteractive -RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptcache \ - --mount=type=cache,target=/var/lib/apt,sharing=private,id=ubuntu2204-aptlib \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ + set -eux; \ rm -f /etc/apt/apt.conf.d/docker-clean; \ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \ - set -eux; \ apt-get update; \ apt-get --no-install-recommends -y install \ git gcc pkgconf autoconf automake libtool make xorg-dev xutils-dev; @@ -17,7 +21,9 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptca COPY . /xorg/ # build xf86-video-dummy v0.3.8 with RandR support -RUN set -eux; \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ + set -eux; \ cd xf86-video-dummy/v0.3.8; \ patch -p1 < ../01_v0.3.8_xdummy-randr.patch; \ autoreconf -v --install; \ @@ -26,7 +32,9 @@ RUN set -eux; \ make install; # build custom input driver -RUN set -eux; \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ + set -eux; \ cd xf86-input-neko; \ ./autogen.sh --prefix=/usr; \ ./configure; \ diff --git a/images/chromium-headful/xorg-deps/xf86-input-neko/Dockerfile b/images/chromium-headful/xorg-deps/xf86-input-neko/Dockerfile index 864de108..7e169ee4 100644 --- a/images/chromium-headful/xorg-deps/xf86-input-neko/Dockerfile +++ b/images/chromium-headful/xorg-deps/xf86-input-neko/Dockerfile @@ -1,12 +1,16 @@ FROM debian:bullseye-slim ENV DEBIAN_FRONTEND=noninteractive +# Allow cross-compilation when building with BuildKit platforms +ARG TARGETARCH +ARG TARGETOS +ARG CACHEIDPREFIX=${TARGETOS:-linux}-${TARGETARCH:-amd64}-debianbullseye -RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=debian-bullseye-aptcache \ - --mount=type=cache,target=/var/lib/apt,sharing=private,id=debian-bullseye-aptlib \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ + set -eux; \ rm -f /etc/apt/apt.conf.d/docker-clean; \ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \ - set -eux; \ apt-get update; \ apt-get install --no-install-recommends -y \ gcc pkgconf autoconf automake libtool make xorg-dev xutils-dev; @@ -15,7 +19,9 @@ WORKDIR /app COPY ./ /app/ -RUN set -eux; \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ + set -eux; \ ./autogen.sh --prefix=/usr; \ ./configure; \ make -j$(nproc); \ diff --git a/images/chromium-headless/build-unikernel.sh b/images/chromium-headless/build-unikernel.sh index cc12799d..3843a8b7 100755 --- a/images/chromium-headless/build-unikernel.sh +++ b/images/chromium-headless/build-unikernel.sh @@ -3,7 +3,7 @@ # Move to the script's directory so relative paths work regardless of the caller CWD SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) cd "$SCRIPT_DIR" -source ../../shared/ensure-common-build-run-vars.sh chromium-headless +source ../../shared/ensure-common-build-run-vars.sh chromium-headless require-ukc-vars source ../../shared/erofs-utils.sh # Ensure the mkfs.erofs tool is present diff --git a/images/chromium-headless/image/Dockerfile b/images/chromium-headless/image/Dockerfile index 9c354b8b..f00ad08c 100644 --- a/images/chromium-headless/image/Dockerfile +++ b/images/chromium-headless/image/Dockerfile @@ -2,36 +2,111 @@ FROM docker.io/golang:1.25.0 AS server-builder WORKDIR /workspace/server # Allow cross-compilation when building with BuildKit platforms -ARG TARGETOS ARG TARGETARCH +ARG TARGETOS +ARG CACHEIDPREFIX=${TARGETOS:-linux}-${TARGETARCH:-amd64}-golang1250 ENV CGO_ENABLED=0 # Go module dependencies first for better layer caching COPY server/go.mod ./ COPY server/go.sum ./ -RUN --mount=type=cache,target=/root/.cache/go-build \ - --mount=type=cache,target=/go/pkg/mod \ +RUN --mount=type=cache,target=/root/.cache/go-build,id=$CACHEIDPREFIX-go-build \ + --mount=type=cache,target=/go/pkg/mod,id=$CACHEIDPREFIX-go-pkg-mod \ go mod download COPY server/ . # Build kernel-images API -RUN --mount=type=cache,target=/root/.cache/go-build \ - --mount=type=cache,target=/go/pkg/mod \ +RUN --mount=type=cache,target=/root/.cache/go-build,id=$CACHEIDPREFIX-go-build \ + --mount=type=cache,target=/go/pkg/mod,id=$CACHEIDPREFIX-go-pkg-mod \ GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \ go build -ldflags="-s -w" -o /out/kernel-images-api ./cmd/api # Build chromium launcher -RUN GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \ +RUN --mount=type=cache,target=/root/.cache/go-build,id=$CACHEIDPREFIX-go-build \ + --mount=type=cache,target=/go/pkg/mod,id=$CACHEIDPREFIX-go-pkg-mod \ + GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \ go build -ldflags="-s -w" -o /out/chromium-launcher ./cmd/chromium-launcher +FROM docker.io/ubuntu:22.04 AS ffmpeg-downloader + +# Allow cross-compilation when building with BuildKit platforms +ARG TARGETARCH +ARG TARGETOS +ARG CACHEIDPREFIX=${TARGETOS:-linux}-${TARGETARCH:-amd64}-ubuntu2204 + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ + set -xe; \ + rm -f /etc/apt/apt.conf.d/docker-clean; \ + echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \ + apt-get -yqq update; \ + apt-get -yqq --no-install-recommends install ca-certificates curl xz-utils; + +# Download FFmpeg (latest static build) for the recording server +RUN --mount=type=cache,target=/tmp/cache/ffmpeg,sharing=locked,id=$CACHEIDPREFIX-ffmpeg \ + <<-'EOT' + set -eux + FFMPEG_CACHE_PATH="/tmp/cache/ffmpeg" + case ${TARGETARCH:-amd64} in + "amd64") FFMPEG_TARGET_ARCH="64" ;; + "arm64") FFMPEG_TARGET_ARCH="arm64" ;; + esac + FFMPEG_TARGET=linux${FFMPEG_TARGET_ARCH:?} + ARCHIVE_NAME="ffmpeg-n7.1-latest-${FFMPEG_TARGET}-gpl-7.1.tar.xz" + FFMPEG_CACHED_ARCHIVE_PATH="$FFMPEG_CACHE_PATH/$ARCHIVE_NAME" + FFMPEG_CACHED_ARCHIVE_CHECKSUM_PATH="$FFMPEG_CACHED_ARCHIVE_PATH.sha256" + URL="https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/$ARCHIVE_NAME" + TEMPORARY_SHA256_CHECKSUM_PATH=$(mktemp /tmp/tmp_sha256.XXXXXXXXXX) + CONTINUE="true" + echo "Downloading FFmpeg checksum" + if curl --connect-timeout 10 -fsSL "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/checksums.sha256" -o $TEMPORARY_SHA256_CHECKSUM_PATH; then + grep -F "$ARCHIVE_NAME" $TEMPORARY_SHA256_CHECKSUM_PATH > $FFMPEG_CACHED_ARCHIVE_CHECKSUM_PATH + else + echo "Failed to connect to ffmpeg static build provider for checksum." + echo "Checking for cached version to use." + if [ -f "$FFMPEG_CACHED_ARCHIVE_CHECKSUM_PATH" ]; then + echo "Found cached checksum." + else + echo "Unable to locate cached checksum." + CONTINUE="false" + fi + fi + rm $TEMPORARY_SHA256_CHECKSUM_PATH + + if [ "$CONTINUE" = "false" ]; then + exit 1 + fi + + echo "Checking cache for FFmpeg archive and validating checksum" + if (cd $FFMPEG_CACHE_PATH && sha256sum --check $FFMPEG_CACHED_ARCHIVE_CHECKSUM_PATH); then + echo "Checksum validated, using cached FFmpeg archive" + else + echo "Downloading FFmpeg static build from $URL" + curl -fsSL "$URL" -o $FFMPEG_CACHED_ARCHIVE_PATH + echo "Validating checksum of FFmpeg static build download" + (cd $FFMPEG_CACHE_PATH && sha256sum --check $FFMPEG_CACHED_ARCHIVE_CHECKSUM_PATH) + fi + + tar -xJf $FFMPEG_CACHED_ARCHIVE_PATH -C /tmp + install -m755 /tmp/ffmpeg-*/bin/ffmpeg /usr/local/bin/ffmpeg + install -m755 /tmp/ffmpeg-*/bin/ffprobe /usr/local/bin/ffprobe + rm -rf /tmp/ffmpeg* +EOT + FROM node:22-bullseye-slim AS node-22 FROM docker.io/ubuntu:22.04 -RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptcache \ - --mount=type=cache,target=/var/lib/apt,sharing=private,id=ubuntu2204-aptlib \ + +# Allow cross-compilation when building with BuildKit platforms +ARG TARGETARCH +ARG TARGETOS +ARG CACHEIDPREFIX=${TARGETOS:-linux}-${TARGETARCH:-amd64}-ubuntu2204 + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ + set -xe; \ rm -f /etc/apt/apt.conf.d/docker-clean; \ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \ - set -xe; \ apt-get -yqq update; \ apt-get -yqq --no-install-recommends install \ libcups2 \ @@ -71,11 +146,11 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptca fc-cache -f # install chromium and sqlite3 for debugging the cookies file -RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptcache \ - --mount=type=cache,target=/var/lib/apt,sharing=private,id=ubuntu2204-aptlib \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ add-apt-repository -y ppa:xtradeb/apps -RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptcache \ - --mount=type=cache,target=/var/lib/apt,sharing=private,id=ubuntu2204-aptlib \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ apt-get update -y && \ apt-get -y install chromium && \ apt-get --no-install-recommends -y install sqlite3; @@ -85,40 +160,12 @@ RUN mkdir -p /etc/chromium/policies/managed COPY shared/chromium-policies/managed/policy.json /etc/chromium/policies/managed/policy.json # Install FFmpeg (latest static build) for the recording server -RUN --mount=type=cache,target=/tmp/cache/ffmpeg,id=ffmpeg \ - <<-'EOT' - set -eux - URL="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz" - echo "Downloading FFmpeg MD5 checksum" - if ! curl --connect-timeout 10 -fsSL "${URL}.md5" -o /tmp/cache/ffmpeg/ffmpeg.tar.xz.md5; then - echo "Failed to connect to ffmpeg static build provider for MD5 checksum." - echo "Checking for cached version to use." - if [ ! -f /tmp/cache/ffmpeg/ffmpeg.tar.xz.md5 ]; then - echo "Unable to locate cached MD5 checksum. Exiting." - exit 1 - else - echo "Found cached MD5 checksum." - fi - fi - sed -i -e 's/ .*$/ \/tmp\/cache\/ffmpeg\/ffmpeg.tar.xz/' /tmp/cache/ffmpeg/ffmpeg.tar.xz.md5 - echo "Checking cache for FFmpeg archive and validating MD5 checksum" - if md5sum --check /tmp/cache/ffmpeg/ffmpeg.tar.xz.md5; then - echo "Checksum validated, using cached FFmpeg archive" - else - echo "Downloading FFmpeg static build from $URL" - curl -fsSL "$URL" -o /tmp/cache/ffmpeg/ffmpeg.tar.xz - echo "Validating MD5 checksum of FFmpeg static build download" - md5sum --check /tmp/cache/ffmpeg/ffmpeg.tar.xz.md5 - fi - tar -xJf /tmp/cache/ffmpeg/ffmpeg.tar.xz -C /tmp - install -m755 /tmp/ffmpeg-*/ffmpeg /usr/local/bin/ffmpeg - install -m755 /tmp/ffmpeg-*/ffprobe /usr/local/bin/ffprobe - rm -rf /tmp/ffmpeg* -EOT +COPY --from=ffmpeg-downloader /usr/local/bin/ffmpeg /usr/local/bin/ffmpeg +COPY --from=ffmpeg-downloader /usr/local/bin/ffprobe /usr/local/bin/ffprobe # Remove upower to prevent spurious D-Bus activations and logs -RUN --mount=type=cache,target=/var/cache/apt,sharing=private,id=ubuntu2204-aptcache \ - --mount=type=cache,target=/var/lib/apt,sharing=private,id=ubuntu2204-aptlib \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=$CACHEIDPREFIX-apt-lib \ apt-get -yqq purge upower || true # install Node.js 22.x by copying from the node:22-bullseye-slim stage @@ -132,8 +179,8 @@ RUN set -eux; \ ln -sf /usr/local/lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack; \ fi -# Install TypeScript and Playwright globally -RUN npm install -g typescript playwright-core tsx +# Install TypeScript, Playwright, Patchright globally +RUN --mount=type=cache,target=/root/.npm,id=$CACHEIDPREFIX-npm npm install -g typescript playwright-core patchright tsx ENV WITHDOCKER=true diff --git a/images/chromium-headless/image/wrapper.sh b/images/chromium-headless/image/wrapper.sh index 6ecba0ae..36bddbb4 100755 --- a/images/chromium-headless/image/wrapper.sh +++ b/images/chromium-headless/image/wrapper.sh @@ -44,12 +44,9 @@ if [ -z "${CHROMIUM_FLAGS:-}" ]; then --disable-breakpad \ --disable-client-side-phishing-detection \ --disable-component-extensions-with-background-pages \ - --disable-component-update \ --disable-crash-reporter \ --disable-crashpad \ - --disable-default-apps \ --disable-dev-shm-usage \ - --disable-extensions \ --disable-features=AcceptCHFrame,AutoExpandDetailsElement,AvoidUnnecessaryBeforeUnloadCheckSync,CertificateTransparencyComponentUpdater,DeferRendererTasksAfterInput,DestroyProfileOnBrowserClose,DialMediaRouteProvider,ExtensionManifestV2Disabled,GlobalMediaControls,HttpsUpgrades,ImprovedCookieControls,LazyFrameLoading,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate \ --disable-field-trial-config \ --disable-gcm-registration \ @@ -63,7 +60,6 @@ if [ -z "${CHROMIUM_FLAGS:-}" ]; then --disable-renderer-backgrounding \ --disable-search-engine-choice-screen \ --disable-software-rasterizer \ - --enable-automation \ --enable-use-zoom-for-dsf=false \ --export-tagged-pdf \ --force-color-profile=srgb \ diff --git a/images/chromium-headless/run-docker.sh b/images/chromium-headless/run-docker.sh index 62d74337..df9b88d1 100755 --- a/images/chromium-headless/run-docker.sh +++ b/images/chromium-headless/run-docker.sh @@ -19,6 +19,10 @@ RUN_ARGS=( -v "$HOST_RECORDINGS_DIR:/recordings" ) +if [[ -n "${PLAYWRIGHT_ENGINE:-}" ]]; then + RUN_ARGS+=( -e PLAYWRIGHT_ENGINE="$PLAYWRIGHT_ENGINE" ) +fi + # If a positional argument is given, use it as the entrypoint ENTRYPOINT_ARG=() if [[ $# -ge 1 && -n "$1" ]]; then diff --git a/images/chromium-headless/run-unikernel.sh b/images/chromium-headless/run-unikernel.sh index eb1b98c7..93bab450 100755 --- a/images/chromium-headless/run-unikernel.sh +++ b/images/chromium-headless/run-unikernel.sh @@ -4,7 +4,7 @@ set -euo pipefail # Move to the script's directory so relative paths work regardless of the caller CWD SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) cd "$SCRIPT_DIR" -source ../../shared/ensure-common-build-run-vars.sh chromium-headless +source ../../shared/ensure-common-build-run-vars.sh chromium-headless require-ukc-vars kraft cloud inst rm "$NAME" || true diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index e5a25719..a2d58df5 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "strconv" + "syscall" "time" "github.com/onkernel/kernel-images/server/lib/logger" @@ -62,7 +63,7 @@ func (s *ApiService) MoveMouse(ctx context.Context, request oapi.MoveMouseReques } // Move the cursor to the desired coordinates - args = append(args, "mousemove", "--sync", strconv.Itoa(body.X), strconv.Itoa(body.Y)) + args = append(args, "mousemove", strconv.Itoa(body.X), strconv.Itoa(body.Y)) // Release modifier keys (keyup) if body.HoldKeys != nil { @@ -156,7 +157,7 @@ func (s *ApiService) ClickMouse(ctx context.Context, request oapi.ClickMouseRequ } // Move the cursor - args = append(args, "mousemove", "--sync", strconv.Itoa(body.X), strconv.Itoa(body.Y)) + args = append(args, "mousemove", strconv.Itoa(body.X), strconv.Itoa(body.Y)) // click type defaults to click clickType := oapi.Click @@ -362,6 +363,71 @@ func (s *ApiService) TypeText(ctx context.Context, request oapi.TypeTextRequestO return oapi.TypeText200Response{}, nil } +const ( + + // Unclutter configuration for cursor hiding + // Setting idle to 0 hides the cursor immediately + unclutterIdleSeconds = "0" + + // A very large jitter value (9 million pixels) ensures that all mouse + // movements are treated as "noise", keeping the cursor permanently hidden + // when combined with idle=0 + unclutterJitterPixels = "9000000" +) + +func (s *ApiService) SetCursor(ctx context.Context, request oapi.SetCursorRequestObject) (oapi.SetCursorResponseObject, error) { + log := logger.FromContext(ctx) + + // serialize input operations to avoid overlapping commands + s.inputMu.Lock() + defer s.inputMu.Unlock() + + // Validate request body + if request.Body == nil { + return oapi.SetCursor400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "request body is required"}, + }, nil + } + body := *request.Body + + // Kill any existing unclutter processes first + pkillCmd := exec.CommandContext(ctx, "pkill", "unclutter") + pkillCmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{Uid: 0, Gid: 0}, + } + + if err := pkillCmd.Run(); err != nil { + if exitErr, ok := err.(*exec.ExitError); !ok || exitErr.ExitCode() != 1 { + log.Error("failed to kill existing unclutter processes", "err", err) + return oapi.SetCursor500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to kill existing unclutter processes"}, + }, nil + } + } + + if body.Hidden { + display := s.resolveDisplayFromEnv() + unclutterCmd := exec.CommandContext(context.Background(), + "unclutter", + "-idle", unclutterIdleSeconds, + "-jitter", unclutterJitterPixels, + ) + unclutterCmd.Env = append(os.Environ(), fmt.Sprintf("DISPLAY=%s", display)) + unclutterCmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{Uid: 0, Gid: 0}, + } + + if err := unclutterCmd.Start(); err != nil { + log.Error("failed to start unclutter", "err", err) + return oapi.SetCursor500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to start unclutter"}, + }, nil + } + } + + return oapi.SetCursor200JSONResponse{Ok: true}, nil +} + func (s *ApiService) PressKey(ctx context.Context, request oapi.PressKeyRequestObject) (oapi.PressKeyResponseObject, error) { log := logger.FromContext(ctx) @@ -515,7 +581,7 @@ func (s *ApiService) Scroll(ctx context.Context, request oapi.ScrollRequestObjec args = append(args, "keydown", key) } } - args = append(args, "mousemove", "--sync", strconv.Itoa(body.X), strconv.Itoa(body.Y)) + args = append(args, "mousemove", strconv.Itoa(body.X), strconv.Itoa(body.Y)) // Apply vertical ticks first (sequential as specified) if body.DeltaY != nil && *body.DeltaY != 0 { @@ -626,7 +692,7 @@ func (s *ApiService) DragMouse(ctx context.Context, request oapi.DragMouseReques } } start := body.Path[0] - args1 = append(args1, "mousemove", "--sync", strconv.Itoa(start[0]), strconv.Itoa(start[1])) + args1 = append(args1, "mousemove", strconv.Itoa(start[0]), strconv.Itoa(start[1])) args1 = append(args1, "mousedown", btn) log.Info("executing xdotool (drag start)", "args", args1) if output, err := defaultXdoTool.Run(ctx, args1...); err != nil { diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index e9252d0e..4b08cf14 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -190,7 +190,7 @@ type ExecutePlaywrightRequest struct { // Example: "await page.goto('https://example.com'); return await page.title();" Code string `json:"code"` - // TimeoutSec Maximum execution time in seconds. Default is 30. + // TimeoutSec Maximum execution time in seconds. Default is 60. TimeoutSec *int `json:"timeout_sec,omitempty"` } @@ -488,6 +488,12 @@ type ScrollRequest struct { Y int `json:"y"` } +// SetCursorRequest defines model for SetCursorRequest. +type SetCursorRequest struct { + // Hidden Whether the cursor should be hidden + Hidden bool `json:"hidden"` +} + // SetFilePermissionsRequest defines model for SetFilePermissionsRequest. type SetFilePermissionsRequest struct { // Group New group name or GID. @@ -654,6 +660,9 @@ type UploadExtensionsAndRestartMultipartRequestBody UploadExtensionsAndRestartMu // ClickMouseJSONRequestBody defines body for ClickMouse for application/json ContentType. type ClickMouseJSONRequestBody = ClickMouseRequest +// SetCursorJSONRequestBody defines body for SetCursor for application/json ContentType. +type SetCursorJSONRequestBody = SetCursorRequest + // DragMouseJSONRequestBody defines body for DragMouse for application/json ContentType. type DragMouseJSONRequestBody = DragMouseRequest @@ -809,6 +818,11 @@ type ClientInterface interface { ClickMouse(ctx context.Context, body ClickMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // SetCursorWithBody request with any body + SetCursorWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + SetCursor(ctx context.Context, body SetCursorJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // DragMouseWithBody request with any body DragMouseWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1017,6 +1031,30 @@ func (c *Client) ClickMouse(ctx context.Context, body ClickMouseJSONRequestBody, return c.Client.Do(req) } +func (c *Client) SetCursorWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewSetCursorRequestWithBody(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) SetCursor(ctx context.Context, body SetCursorJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewSetCursorRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) DragMouseWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewDragMouseRequestWithBody(c.Server, contentType, body) if err != nil { @@ -1798,6 +1836,46 @@ func NewClickMouseRequestWithBody(server string, contentType string, body io.Rea return req, nil } +// NewSetCursorRequest calls the generic SetCursor builder with application/json body +func NewSetCursorRequest(server string, body SetCursorJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewSetCursorRequestWithBody(server, "application/json", bodyReader) +} + +// NewSetCursorRequestWithBody generates requests for SetCursor with any type of body +func NewSetCursorRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/computer/cursor") + 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 +} + // NewDragMouseRequest calls the generic DragMouse builder with application/json body func NewDragMouseRequest(server string, body DragMouseJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -3314,6 +3392,11 @@ type ClientWithResponsesInterface interface { ClickMouseWithResponse(ctx context.Context, body ClickMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*ClickMouseResponse, error) + // SetCursorWithBodyWithResponse request with any body + SetCursorWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SetCursorResponse, error) + + SetCursorWithResponse(ctx context.Context, body SetCursorJSONRequestBody, reqEditors ...RequestEditorFn) (*SetCursorResponse, error) + // DragMouseWithBodyWithResponse request with any body DragMouseWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DragMouseResponse, error) @@ -3531,6 +3614,30 @@ func (r ClickMouseResponse) StatusCode() int { return 0 } +type SetCursorResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *OkResponse + JSON400 *BadRequestError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r SetCursorResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r SetCursorResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type DragMouseResponse struct { Body []byte HTTPResponse *http.Response @@ -4412,6 +4519,23 @@ func (c *ClientWithResponses) ClickMouseWithResponse(ctx context.Context, body C return ParseClickMouseResponse(rsp) } +// SetCursorWithBodyWithResponse request with arbitrary body returning *SetCursorResponse +func (c *ClientWithResponses) SetCursorWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SetCursorResponse, error) { + rsp, err := c.SetCursorWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseSetCursorResponse(rsp) +} + +func (c *ClientWithResponses) SetCursorWithResponse(ctx context.Context, body SetCursorJSONRequestBody, reqEditors ...RequestEditorFn) (*SetCursorResponse, error) { + rsp, err := c.SetCursor(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseSetCursorResponse(rsp) +} + // DragMouseWithBodyWithResponse request with arbitrary body returning *DragMouseResponse func (c *ClientWithResponses) DragMouseWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DragMouseResponse, error) { rsp, err := c.DragMouseWithBody(ctx, contentType, body, reqEditors...) @@ -4994,6 +5118,46 @@ func ParseClickMouseResponse(rsp *http.Response) (*ClickMouseResponse, error) { return response, nil } +// ParseSetCursorResponse parses an HTTP response from a SetCursorWithResponse call +func ParseSetCursorResponse(rsp *http.Response) (*SetCursorResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &SetCursorResponse{ + 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 == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseDragMouseResponse parses an HTTP response from a DragMouseWithResponse call func ParseDragMouseResponse(rsp *http.Response) (*DragMouseResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -6370,6 +6534,9 @@ type ServerInterface interface { // Simulate a mouse click action on the host computer // (POST /computer/click_mouse) ClickMouse(w http.ResponseWriter, r *http.Request) + // Hide or show the cursor + // (POST /computer/cursor) + SetCursor(w http.ResponseWriter, r *http.Request) // Drag the mouse along a path // (POST /computer/drag_mouse) DragMouse(w http.ResponseWriter, r *http.Request) @@ -6499,6 +6666,12 @@ func (_ Unimplemented) ClickMouse(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// Hide or show the cursor +// (POST /computer/cursor) +func (_ Unimplemented) SetCursor(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Drag the mouse along a path // (POST /computer/drag_mouse) func (_ Unimplemented) DragMouse(w http.ResponseWriter, r *http.Request) { @@ -6760,6 +6933,20 @@ func (siw *ServerInterfaceWrapper) ClickMouse(w http.ResponseWriter, r *http.Req handler.ServeHTTP(w, r) } +// SetCursor operation middleware +func (siw *ServerInterfaceWrapper) SetCursor(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.SetCursor(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // DragMouse operation middleware func (siw *ServerInterfaceWrapper) DragMouse(w http.ResponseWriter, r *http.Request) { @@ -7603,6 +7790,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/computer/click_mouse", wrapper.ClickMouse) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/computer/cursor", wrapper.SetCursor) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/computer/drag_mouse", wrapper.DragMouse) }) @@ -7822,6 +8012,41 @@ func (response ClickMouse500JSONResponse) VisitClickMouseResponse(w http.Respons return json.NewEncoder(w).Encode(response) } +type SetCursorRequestObject struct { + Body *SetCursorJSONRequestBody +} + +type SetCursorResponseObject interface { + VisitSetCursorResponse(w http.ResponseWriter) error +} + +type SetCursor200JSONResponse OkResponse + +func (response SetCursor200JSONResponse) VisitSetCursorResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type SetCursor400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response SetCursor400JSONResponse) VisitSetCursorResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type SetCursor500JSONResponse struct{ InternalErrorJSONResponse } + +func (response SetCursor500JSONResponse) VisitSetCursorResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type DragMouseRequestObject struct { Body *DragMouseJSONRequestBody } @@ -9393,6 +9618,9 @@ type StrictServerInterface interface { // Simulate a mouse click action on the host computer // (POST /computer/click_mouse) ClickMouse(ctx context.Context, request ClickMouseRequestObject) (ClickMouseResponseObject, error) + // Hide or show the cursor + // (POST /computer/cursor) + SetCursor(ctx context.Context, request SetCursorRequestObject) (SetCursorResponseObject, error) // Drag the mouse along a path // (POST /computer/drag_mouse) DragMouse(ctx context.Context, request DragMouseRequestObject) (DragMouseResponseObject, error) @@ -9622,6 +9850,37 @@ func (sh *strictHandler) ClickMouse(w http.ResponseWriter, r *http.Request) { } } +// SetCursor operation middleware +func (sh *strictHandler) SetCursor(w http.ResponseWriter, r *http.Request) { + var request SetCursorRequestObject + + var body SetCursorJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.SetCursor(ctx, request.(SetCursorRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "SetCursor") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(SetCursorResponseObject); ok { + if err := validResponse.VisitSetCursorResponse(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)) + } +} + // DragMouse operation middleware func (sh *strictHandler) DragMouse(w http.ResponseWriter, r *http.Request) { var request DragMouseRequestObject @@ -10654,122 +10913,123 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "9uCwKl9WP7SR/wkAAP//KSSqVIyVAAA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 62b818ae..9b23c130 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -332,6 +332,27 @@ paths: $ref: "#/components/responses/BadRequestError" "500": $ref: "#/components/responses/InternalError" + /computer/cursor: + post: + summary: Hide or show the cursor + operationId: setCursor + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SetCursorRequest" + responses: + "200": + description: Cursor visibility updated successfully + content: + application/json: + schema: + $ref: "#/components/schemas/OkResponse" + "400": + $ref: "#/components/responses/BadRequestError" + "500": + $ref: "#/components/responses/InternalError" /computer/type: post: summary: Type text on the host computer @@ -1088,6 +1109,15 @@ components: region: $ref: "#/components/schemas/ScreenshotRegion" additionalProperties: false + SetCursorRequest: + type: object + required: + - hidden + properties: + hidden: + type: boolean + description: Whether the cursor should be hidden + additionalProperties: false TypeTextRequest: type: object required: diff --git a/server/runtime/playwright-executor.ts b/server/runtime/playwright-executor.ts index 5246eab4..7bb87052 100644 --- a/server/runtime/playwright-executor.ts +++ b/server/runtime/playwright-executor.ts @@ -1,5 +1,6 @@ import { readFileSync } from 'fs'; -import { chromium } from 'playwright-core'; +import { chromium as chromiumPW } from 'playwright-core'; +import { chromium as chromiumPR } from 'patchright'; async function main() { const codeFilePath = process.argv[2]; @@ -24,6 +25,8 @@ async function main() { let result; try { + const chromium = process.env.PLAYWRIGHT_ENGINE === 'patchright' ? chromiumPR : chromiumPW; + browser = await chromium.connectOverCDP('ws://127.0.0.1:9222'); const contexts = browser.contexts(); const context = contexts.length > 0 ? contexts[0] : await browser.newContext(); diff --git a/shared/ensure-common-build-run-vars.sh b/shared/ensure-common-build-run-vars.sh index 404c6da6..f7627a0d 100644 --- a/shared/ensure-common-build-run-vars.sh +++ b/shared/ensure-common-build-run-vars.sh @@ -3,10 +3,12 @@ set -e -o pipefail IMAGE_TYPE=$1 if [ -z "$IMAGE_TYPE" ]; then - echo "Usage: source ensure-common-build-run-vars.sh " + echo "Usage: source ensure-common-build-run-vars.sh [require-ukc-vars]" echo "e.g. source ensure-common-build-run-vars.sh chromium-headful" + echo " source ensure-common-build-run-vars.sh chromium-headful require-ukc-vars" echo "This will set the defaults for the image name and test instance name" echo "You can override the defaults by setting the IMAGE and NAME variables" + echo "Pass 'require-ukc-vars' as second argument to require UKC_TOKEN/UKC_METRO" return 1 fi IMAGE="${IMAGE:-onkernel/${IMAGE_TYPE}-test:latest}" @@ -14,14 +16,20 @@ NAME="${NAME:-${IMAGE_TYPE}-test}" UKC_INDEX="${UKC_INDEX:-index.unikraft.io}" -# fail if UKC_TOKEN, UKC_METRO are not set -errormsg="" -for var in UKC_TOKEN UKC_METRO; do - if [ -z "${!var}" ]; then - errormsg+="$var " +# Only require UKC_TOKEN and UKC_METRO when explicitly requested +# Pass "require-ukc-vars" as second argument to enable this check +REQUIRE_UKC_VARS="${2:-}" + +if [ "$REQUIRE_UKC_VARS" == "require-ukc-vars" ]; then + # fail if UKC_TOKEN, UKC_METRO are not set + errormsg="" + for var in UKC_TOKEN UKC_METRO; do + if [ -z "${!var}" ]; then + errormsg+="$var " + fi + done + if [ -n "$errormsg" ]; then + echo "Required variables not set: $errormsg" + return 1 fi -done -if [ -n "$errormsg" ]; then - echo "Required variables not set: $errormsg" - exit 1 fi