diff --git a/.dockerignore b/.dockerignore index 3f7749cc..933a9498 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,3 +11,7 @@ coverage .vscode Dockerfile .archive +.rootfs +.unikraft +initrd +.tmp diff --git a/.github/workflows/chromium-headful-image.yaml b/.github/workflows/chromium-headful-image.yaml new file mode 100644 index 00000000..ea56f29d --- /dev/null +++ b/.github/workflows/chromium-headful-image.yaml @@ -0,0 +1,37 @@ +name: chromium-headful-image + +on: + workflow_call: + +jobs: + docker: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Compute short SHA + id: vars + shell: bash + run: echo "short_sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: images/chromium-headful/Dockerfile + push: true + tags: onkernel/chromium-headful:${{ steps.vars.outputs.short_sha }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/chromium-headless-image.yaml b/.github/workflows/chromium-headless-image.yaml new file mode 100644 index 00000000..be00d67b --- /dev/null +++ b/.github/workflows/chromium-headless-image.yaml @@ -0,0 +1,37 @@ +name: chromium-headless-image + +on: + workflow_call: + +jobs: + docker: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Compute short SHA + id: vars + shell: bash + run: echo "short_sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: images/chromium-headless/image/Dockerfile + push: true + tags: onkernel/chromium-headless:${{ steps.vars.outputs.short_sha }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/publish_to_dockerhub.yml b/.github/workflows/publish_to_dockerhub.yml deleted file mode 100644 index f32e32c3..00000000 --- a/.github/workflows/publish_to_dockerhub.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Build and Publish Docker Image - -on: - workflow_dispatch: - inputs: - tag: - description: "Docker image tag" - required: true - default: "latest" - -jobs: - build_and_publish: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - # Verify Docker setup and pull base Anthropic image - - name: Verify Docker and pull Anthropic image - run: | - docker info - docker version - docker ps - # Pull base image explicitly - docker pull ghcr.io/anthropics/anthropic-quickstarts:computer-use-demo-latest - - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: ./containers/docker - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/kernel-chromium:${{ github.event.inputs.tag }} - cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/kernel-chromium:buildcache - cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/kernel-chromium:buildcache,mode=max diff --git a/.github/workflows/server-test.yaml b/.github/workflows/server-test.yaml index 0c02e7c7..620b8fb7 100644 --- a/.github/workflows/server-test.yaml +++ b/.github/workflows/server-test.yaml @@ -5,21 +5,60 @@ on: branches: ["main"] pull_request: branches: ["main"] + workflow_dispatch: jobs: + build-headful: + uses: ./.github/workflows/chromium-headful-image.yaml + secrets: inherit + + build-headless: + uses: ./.github/workflows/chromium-headless-image.yaml + secrets: inherit + test: runs-on: ubuntu-latest + needs: [build-headful, build-headless] + permissions: + contents: read steps: - name: Checkout code uses: actions/checkout@v4 + - name: Setup Chrome + uses: browser-actions/setup-chrome@v2 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: "server/go.mod" cache: true + - name: Compute short SHA for images + id: vars + shell: bash + run: echo "short_sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Run server Makefile tests run: make test working-directory: server + env: + # Prefer explicit images if passed from the caller; otherwise use the commit short sha + E2E_IMAGE_TAG: ${{ steps.vars.outputs.short_sha }} diff --git a/images/chromium-headful/Dockerfile b/images/chromium-headful/Dockerfile index 4be552d4..74679c87 100644 --- a/images/chromium-headful/Dockerfile +++ b/images/chromium-headful/Dockerfile @@ -1,9 +1,24 @@ +FROM docker.io/golang:1.25.0 AS server-builder +WORKDIR /workspace/server + +ARG TARGETOS +ARG TARGETARCH +ENV CGO_ENABLED=0 + +COPY server/go.mod ./ +COPY server/go.sum ./ +RUN go mod download + +COPY server/ . +RUN GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \ + go build -ldflags="-s -w" -o /out/kernel-images-api ./cmd/api + # webrtc client FROM node:22-bullseye-slim AS client WORKDIR /src -COPY client/package*.json ./ +COPY images/chromium-headful/client/package*.json ./ RUN npm install -COPY client/ . +COPY images/chromium-headful/client/ . RUN npm run build # xorg dependencies @@ -15,7 +30,7 @@ RUN set -eux; \ apt-get install -y \ git gcc pkgconf autoconf automake libtool make xorg-dev xutils-dev \ && rm -rf /var/lib/apt/lists/*; -COPY xorg-deps/ /xorg/ +COPY images/chromium-headful/xorg-deps/ /xorg/ # build xf86-video-dummy v0.3.8 with RandR support RUN set -eux; \ cd xf86-video-dummy/v0.3.8; \ @@ -50,7 +65,6 @@ RUN apt-get update && \ imagemagick \ sudo \ mutter \ - x11vnc \ # Python/pyenv reqs build-essential \ libssl-dev \ @@ -137,14 +151,9 @@ RUN set -eux; \ apt-get clean -y; \ rm -rf /var/lib/apt/lists/* /var/cache/apt/ -# install chromium & ncat for proxying the remote debugging port +# install chromium and sqlite3 for debugging the cookies file RUN add-apt-repository -y ppa:xtradeb/apps -RUN apt update -y && apt install -y chromium ncat - -# Install noVNC -RUN git clone --branch v1.5.0 https://github.com/novnc/noVNC.git /opt/noVNC && \ - git clone --branch v0.12.0 https://github.com/novnc/websockify /opt/noVNC/utils/websockify && \ - ln -s /opt/noVNC/vnc.html /opt/noVNC/index.html +RUN apt update -y && apt install -y chromium sqlite3 # setup desktop env & app ENV DISPLAY_NUM=1 @@ -152,22 +161,24 @@ ENV HEIGHT=768 ENV WIDTH=1024 ENV WITHDOCKER=true -COPY xorg.conf /etc/neko/xorg.conf -COPY neko.yaml /etc/neko/neko.yaml +COPY images/chromium-headful/xorg.conf /etc/neko/xorg.conf +COPY images/chromium-headful/neko.yaml /etc/neko/neko.yaml 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 COPY --from=xorg-deps /usr/local/lib/xorg/modules/input/neko_drv.so /usr/lib/xorg/modules/input/neko_drv.so -COPY image-chromium/ / -COPY ./wrapper.sh /wrapper.sh +COPY images/chromium-headful/image-chromium/ / +COPY images/chromium-headful/start-chromium.sh /images/chromium-headful/start-chromium.sh +RUN chmod +x /images/chromium-headful/start-chromium.sh +COPY images/chromium-headful/wrapper.sh /wrapper.sh +COPY images/chromium-headful/supervisord.conf /etc/supervisor/supervisord.conf +COPY images/chromium-headful/supervisor/services/ /etc/supervisor/conf.d/services/ -# copy the kernel-images API binary built externally -COPY bin/kernel-images-api /usr/local/bin/kernel-images-api +# copy the kernel-images API binary built in the builder stage +COPY --from=server-builder /out/kernel-images-api /usr/local/bin/kernel-images-api ENV WITH_KERNEL_IMAGES_API=false RUN useradd -m -s /bin/bash kernel -RUN cp -r ./user-data /home/kernel/user-data ENTRYPOINT [ "/wrapper.sh" ] - diff --git a/images/chromium-headful/build-docker.sh b/images/chromium-headful/build-docker.sh index 5f1db257..c2cd3320 100755 --- a/images/chromium-headful/build-docker.sh +++ b/images/chromium-headful/build-docker.sh @@ -8,8 +8,6 @@ source ../../shared/ensure-common-build-run-vars.sh chromium-headful source ../../shared/start-buildkit.sh -# Build the kernel-images API binary and place it into ./bin for Docker build context -source ../../shared/build-server.sh "$(pwd)/bin" - -# Build (and optionally push) the Docker image. -docker build -t "$IMAGE" . +# Build the Docker image using the repo root as build context +# so the Dockerfile's first stage can access the server sources +(cd "$SCRIPT_DIR/../.." && docker build -f images/chromium-headful/Dockerfile -t "$IMAGE" .) diff --git a/images/chromium-headful/build-unikernel.sh b/images/chromium-headful/build-unikernel.sh index 0b16bf99..ab860b43 100755 --- a/images/chromium-headful/build-unikernel.sh +++ b/images/chromium-headful/build-unikernel.sh @@ -17,10 +17,8 @@ set -euo pipefail # Build the root file system source ../../shared/start-buildkit.sh rm -rf ./.rootfs || true -# Build the API binary -source ../../shared/build-server.sh "$(pwd)/bin" app_name=chromium-headful-build -docker build --platform linux/amd64 -t "$IMAGE" . +(cd "$SCRIPT_DIR/../.." && docker build --platform linux/amd64 -f images/chromium-headful/Dockerfile -t "$IMAGE" .) docker rm cnt-"$app_name" || true docker create --platform linux/amd64 --name cnt-"$app_name" "$IMAGE" /bin/sh docker cp cnt-"$app_name":/ ./.rootfs diff --git a/images/chromium-headful/image-chromium/user-data/Default/Account Web Data b/images/chromium-headful/image-chromium/user-data/Default/Account Web Data deleted file mode 100644 index 1d96f4b3..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Account Web Data and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Account Web Data-journal b/images/chromium-headful/image-chromium/user-data/Default/Account Web Data-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Affiliation Database b/images/chromium-headful/image-chromium/user-data/Default/Affiliation Database deleted file mode 100644 index e355a86e..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Affiliation Database and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Affiliation Database-journal b/images/chromium-headful/image-chromium/user-data/Default/Affiliation Database-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/AutofillStrikeDatabase/LOCK b/images/chromium-headful/image-chromium/user-data/Default/AutofillStrikeDatabase/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/AutofillStrikeDatabase/LOG b/images/chromium-headful/image-chromium/user-data/Default/AutofillStrikeDatabase/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/BookmarkMergedSurfaceOrdering b/images/chromium-headful/image-chromium/user-data/Default/BookmarkMergedSurfaceOrdering deleted file mode 100644 index 2c63c085..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/BookmarkMergedSurfaceOrdering +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/images/chromium-headful/image-chromium/user-data/Default/Bookmarks b/images/chromium-headful/image-chromium/user-data/Default/Bookmarks deleted file mode 100644 index df6bfbc9..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Bookmarks +++ /dev/null @@ -1,36 +0,0 @@ -{ - "checksum": "b6d02a93fae2ae0863aac8814bd10763", - "roots": { - "bookmark_bar": { - "children": [], - "date_added": "13397001174416633", - "date_last_used": "0", - "date_modified": "0", - "guid": "0bc5d13f-2cba-5d74-951f-3f233fe6c908", - "id": "1", - "name": "Bookmarks bar", - "type": "folder" - }, - "other": { - "children": [], - "date_added": "13397001174416633", - "date_last_used": "0", - "date_modified": "0", - "guid": "82b081ec-3dd3-529c-8475-ab6c344590dd", - "id": "2", - "name": "Other bookmarks", - "type": "folder" - }, - "synced": { - "children": [], - "date_added": "13397001174416633", - "date_last_used": "0", - "date_modified": "0", - "guid": "4cf2e351-0e85-532b-bb37-df045d8f8d0f", - "id": "3", - "name": "Mobile bookmarks", - "type": "folder" - } - }, - "version": 1 -} diff --git a/images/chromium-headful/image-chromium/user-data/Default/Bookmarks.bak b/images/chromium-headful/image-chromium/user-data/Default/Bookmarks.bak deleted file mode 100644 index df6bfbc9..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Bookmarks.bak +++ /dev/null @@ -1,36 +0,0 @@ -{ - "checksum": "b6d02a93fae2ae0863aac8814bd10763", - "roots": { - "bookmark_bar": { - "children": [], - "date_added": "13397001174416633", - "date_last_used": "0", - "date_modified": "0", - "guid": "0bc5d13f-2cba-5d74-951f-3f233fe6c908", - "id": "1", - "name": "Bookmarks bar", - "type": "folder" - }, - "other": { - "children": [], - "date_added": "13397001174416633", - "date_last_used": "0", - "date_modified": "0", - "guid": "82b081ec-3dd3-529c-8475-ab6c344590dd", - "id": "2", - "name": "Other bookmarks", - "type": "folder" - }, - "synced": { - "children": [], - "date_added": "13397001174416633", - "date_last_used": "0", - "date_modified": "0", - "guid": "4cf2e351-0e85-532b-bb37-df045d8f8d0f", - "id": "3", - "name": "Mobile bookmarks", - "type": "folder" - } - }, - "version": 1 -} diff --git a/images/chromium-headful/image-chromium/user-data/Default/BudgetDatabase/LOCK b/images/chromium-headful/image-chromium/user-data/Default/BudgetDatabase/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/BudgetDatabase/LOG b/images/chromium-headful/image-chromium/user-data/Default/BudgetDatabase/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/160aa9f9b1d0daa0_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/160aa9f9b1d0daa0_0 deleted file mode 100644 index 5279250d..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/160aa9f9b1d0daa0_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/18f814dc6a970d51_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/18f814dc6a970d51_0 deleted file mode 100644 index 638fe32c..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/18f814dc6a970d51_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/23c80147d8eccb20_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/23c80147d8eccb20_0 deleted file mode 100644 index d4588d5d..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/23c80147d8eccb20_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/2abb8de5d1676d84_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/2abb8de5d1676d84_0 deleted file mode 100644 index 13a3a143..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/2abb8de5d1676d84_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/2af81ecc80645422_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/2af81ecc80645422_0 deleted file mode 100644 index 8c011841..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/2af81ecc80645422_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/332636a61a1a4cd0_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/332636a61a1a4cd0_0 deleted file mode 100644 index cc8b369f..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/332636a61a1a4cd0_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/4d5e10c2441b2a20_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/4d5e10c2441b2a20_0 deleted file mode 100644 index 512be1ff..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/4d5e10c2441b2a20_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/5e14aceeb4058f0d_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/5e14aceeb4058f0d_0 deleted file mode 100644 index f4de8f53..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/5e14aceeb4058f0d_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/63111493413de553_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/63111493413de553_0 deleted file mode 100644 index 246e2a65..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/63111493413de553_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/8fd4e3866272accf_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/8fd4e3866272accf_0 deleted file mode 100644 index 06919b8c..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/8fd4e3866272accf_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/a57a843ad21df102_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/a57a843ad21df102_0 deleted file mode 100644 index 3f857fed..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/a57a843ad21df102_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/ab3fa0201edf7a80_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/ab3fa0201edf7a80_0 deleted file mode 100644 index 5c6caff9..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/ab3fa0201edf7a80_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/adc7be0471dc9726_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/adc7be0471dc9726_0 deleted file mode 100644 index f91b2ea5..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/adc7be0471dc9726_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/b2406f1e04e715ae_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/b2406f1e04e715ae_0 deleted file mode 100644 index 1eaa2018..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/b2406f1e04e715ae_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/b9641970fc16a925_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/b9641970fc16a925_0 deleted file mode 100644 index 3cdb27ff..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/b9641970fc16a925_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/c041acff8074ef3a_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/c041acff8074ef3a_0 deleted file mode 100644 index 5c3ecd67..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/c041acff8074ef3a_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/c8c67c38c1dceefb_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/c8c67c38c1dceefb_0 deleted file mode 100644 index d2ab85f7..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/c8c67c38c1dceefb_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/d6b4b79a25f3621b_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/d6b4b79a25f3621b_0 deleted file mode 100644 index ef57d5d6..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/d6b4b79a25f3621b_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/df4994cdc340cc1f_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/df4994cdc340cc1f_0 deleted file mode 100644 index efb9ed1a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/df4994cdc340cc1f_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/f5f5202928eccfb2_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/f5f5202928eccfb2_0 deleted file mode 100644 index c84dc4aa..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/f5f5202928eccfb2_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/f7c5dc7586ed0b98_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/f7c5dc7586ed0b98_0 deleted file mode 100644 index 61a94f55..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/f7c5dc7586ed0b98_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/f7cb84a72cb6029d_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/f7cb84a72cb6029d_0 deleted file mode 100644 index 17956a94..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/f7cb84a72cb6029d_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/fc8ed6a10b259a5a_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/fc8ed6a10b259a5a_0 deleted file mode 100644 index 23847f5a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/fc8ed6a10b259a5a_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/feda0e50786b7f16_0 b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/feda0e50786b7f16_0 deleted file mode 100644 index 9dd1b908..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/feda0e50786b7f16_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/index b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/index deleted file mode 100644 index 79bd403a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/index and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/index-dir/the-real-index b/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/index-dir/the-real-index deleted file mode 100644 index 0380ace8..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cache/Cache_Data/index-dir/the-real-index and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/ClientCertificates/LOCK b/images/chromium-headful/image-chromium/user-data/Default/ClientCertificates/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/ClientCertificates/LOG b/images/chromium-headful/image-chromium/user-data/Default/ClientCertificates/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/261229008fd31813_0 b/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/261229008fd31813_0 deleted file mode 100644 index 33e0e14f..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/261229008fd31813_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/66b9f1881f0ac9c9_0 b/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/66b9f1881f0ac9c9_0 deleted file mode 100644 index a66b49e4..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/66b9f1881f0ac9c9_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/78d67e085e223791_0 b/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/78d67e085e223791_0 deleted file mode 100644 index 7b45362f..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/78d67e085e223791_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/7be3991b0cb7f3a3_0 b/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/7be3991b0cb7f3a3_0 deleted file mode 100644 index a2e67359..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/7be3991b0cb7f3a3_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/8001a98064fa8c0e_0 b/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/8001a98064fa8c0e_0 deleted file mode 100644 index 95bd7ecd..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/8001a98064fa8c0e_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/8553ad54beb6fcec_0 b/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/8553ad54beb6fcec_0 deleted file mode 100644 index b6ad26c3..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/8553ad54beb6fcec_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/89edbbf1e282446a_0 b/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/89edbbf1e282446a_0 deleted file mode 100644 index 060d8be5..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/89edbbf1e282446a_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/c4726dce073f7bb3_0 b/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/c4726dce073f7bb3_0 deleted file mode 100644 index 23aea685..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/c4726dce073f7bb3_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/index b/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/index deleted file mode 100644 index 79bd403a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/index and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/index-dir/the-real-index b/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/index-dir/the-real-index deleted file mode 100644 index 722bfdfc..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/js/index-dir/the-real-index and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/wasm/index b/images/chromium-headful/image-chromium/user-data/Default/Code Cache/wasm/index deleted file mode 100644 index 79bd403a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/wasm/index and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/wasm/index-dir/the-real-index b/images/chromium-headful/image-chromium/user-data/Default/Code Cache/wasm/index-dir/the-real-index deleted file mode 100644 index 359d8ab0..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Code Cache/wasm/index-dir/the-real-index and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cookies b/images/chromium-headful/image-chromium/user-data/Default/Cookies deleted file mode 100644 index 9827cfa7..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Cookies and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Cookies-journal b/images/chromium-headful/image-chromium/user-data/Default/Cookies-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/DIPS b/images/chromium-headful/image-chromium/user-data/Default/DIPS deleted file mode 100644 index db7a7459..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/DIPS and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/DIPS-wal b/images/chromium-headful/image-chromium/user-data/Default/DIPS-wal deleted file mode 100644 index a77c3985..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/DIPS-wal and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/DawnGraphiteCache/data_0 b/images/chromium-headful/image-chromium/user-data/Default/DawnGraphiteCache/data_0 deleted file mode 100644 index d76fb77e..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/DawnGraphiteCache/data_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/DawnGraphiteCache/data_1 b/images/chromium-headful/image-chromium/user-data/Default/DawnGraphiteCache/data_1 deleted file mode 100644 index 035d06d9..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/DawnGraphiteCache/data_1 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/DawnGraphiteCache/data_2 b/images/chromium-headful/image-chromium/user-data/Default/DawnGraphiteCache/data_2 deleted file mode 100644 index c7e2eb9a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/DawnGraphiteCache/data_2 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/DawnGraphiteCache/data_3 b/images/chromium-headful/image-chromium/user-data/Default/DawnGraphiteCache/data_3 deleted file mode 100644 index 5eec9735..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/DawnGraphiteCache/data_3 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/DawnGraphiteCache/index b/images/chromium-headful/image-chromium/user-data/Default/DawnGraphiteCache/index deleted file mode 100644 index cd19fd2a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/DawnGraphiteCache/index and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/DawnWebGPUCache/data_0 b/images/chromium-headful/image-chromium/user-data/Default/DawnWebGPUCache/data_0 deleted file mode 100644 index d76fb77e..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/DawnWebGPUCache/data_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/DawnWebGPUCache/data_1 b/images/chromium-headful/image-chromium/user-data/Default/DawnWebGPUCache/data_1 deleted file mode 100644 index 035d06d9..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/DawnWebGPUCache/data_1 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/DawnWebGPUCache/data_2 b/images/chromium-headful/image-chromium/user-data/Default/DawnWebGPUCache/data_2 deleted file mode 100644 index c7e2eb9a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/DawnWebGPUCache/data_2 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/DawnWebGPUCache/data_3 b/images/chromium-headful/image-chromium/user-data/Default/DawnWebGPUCache/data_3 deleted file mode 100644 index 5eec9735..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/DawnWebGPUCache/data_3 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/DawnWebGPUCache/index b/images/chromium-headful/image-chromium/user-data/Default/DawnWebGPUCache/index deleted file mode 100644 index 388405a2..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/DawnWebGPUCache/index and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Download Service/EntryDB/LOCK b/images/chromium-headful/image-chromium/user-data/Default/Download Service/EntryDB/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Download Service/EntryDB/LOG b/images/chromium-headful/image-chromium/user-data/Default/Download Service/EntryDB/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Extension Rules/000003.log b/images/chromium-headful/image-chromium/user-data/Default/Extension Rules/000003.log deleted file mode 100644 index f7187670..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Extension Rules/000003.log and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Extension Rules/CURRENT b/images/chromium-headful/image-chromium/user-data/Default/Extension Rules/CURRENT deleted file mode 100644 index 7ed683d1..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Extension Rules/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Extension Rules/LOCK b/images/chromium-headful/image-chromium/user-data/Default/Extension Rules/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Extension Rules/LOG b/images/chromium-headful/image-chromium/user-data/Default/Extension Rules/LOG deleted file mode 100644 index cfb1207d..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Extension Rules/LOG +++ /dev/null @@ -1,2 +0,0 @@ -2025/07/14-21:35:40.347 6d Creating DB /home/kernel/user-data/Default/Extension Rules since it was missing. -2025/07/14-21:35:40.358 6d Reusing MANIFEST /home/kernel/user-data/Default/Extension Rules/MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Extension Rules/MANIFEST-000001 b/images/chromium-headful/image-chromium/user-data/Default/Extension Rules/MANIFEST-000001 deleted file mode 100644 index 18e5cab7..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Extension Rules/MANIFEST-000001 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Extension Scripts/000003.log b/images/chromium-headful/image-chromium/user-data/Default/Extension Scripts/000003.log deleted file mode 100644 index 4acb4c8d..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Extension Scripts/000003.log and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Extension Scripts/CURRENT b/images/chromium-headful/image-chromium/user-data/Default/Extension Scripts/CURRENT deleted file mode 100644 index 7ed683d1..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Extension Scripts/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Extension Scripts/LOCK b/images/chromium-headful/image-chromium/user-data/Default/Extension Scripts/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Extension Scripts/LOG b/images/chromium-headful/image-chromium/user-data/Default/Extension Scripts/LOG deleted file mode 100644 index 748f579a..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Extension Scripts/LOG +++ /dev/null @@ -1,2 +0,0 @@ -2025/07/14-21:35:40.359 6d Creating DB /home/kernel/user-data/Default/Extension Scripts since it was missing. -2025/07/14-21:35:40.367 6d Reusing MANIFEST /home/kernel/user-data/Default/Extension Scripts/MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Extension Scripts/MANIFEST-000001 b/images/chromium-headful/image-chromium/user-data/Default/Extension Scripts/MANIFEST-000001 deleted file mode 100644 index 18e5cab7..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Extension Scripts/MANIFEST-000001 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Extension State/000003.log b/images/chromium-headful/image-chromium/user-data/Default/Extension State/000003.log deleted file mode 100644 index b248f536..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Extension State/000003.log and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Extension State/CURRENT b/images/chromium-headful/image-chromium/user-data/Default/Extension State/CURRENT deleted file mode 100644 index 7ed683d1..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Extension State/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Extension State/LOCK b/images/chromium-headful/image-chromium/user-data/Default/Extension State/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Extension State/LOG b/images/chromium-headful/image-chromium/user-data/Default/Extension State/LOG deleted file mode 100644 index 9ef9be4b..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Extension State/LOG +++ /dev/null @@ -1,2 +0,0 @@ -2025/07/14-21:35:40.386 61 Creating DB /home/kernel/user-data/Default/Extension State since it was missing. -2025/07/14-21:35:40.394 61 Reusing MANIFEST /home/kernel/user-data/Default/Extension State/MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Extension State/MANIFEST-000001 b/images/chromium-headful/image-chromium/user-data/Default/Extension State/MANIFEST-000001 deleted file mode 100644 index 18e5cab7..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Extension State/MANIFEST-000001 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Favicons b/images/chromium-headful/image-chromium/user-data/Default/Favicons deleted file mode 100644 index 94bdad67..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Favicons and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Favicons-journal b/images/chromium-headful/image-chromium/user-data/Default/Favicons-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Feature Engagement Tracker/AvailabilityDB/LOCK b/images/chromium-headful/image-chromium/user-data/Default/Feature Engagement Tracker/AvailabilityDB/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Feature Engagement Tracker/AvailabilityDB/LOG b/images/chromium-headful/image-chromium/user-data/Default/Feature Engagement Tracker/AvailabilityDB/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Feature Engagement Tracker/EventDB/LOCK b/images/chromium-headful/image-chromium/user-data/Default/Feature Engagement Tracker/EventDB/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Feature Engagement Tracker/EventDB/LOG b/images/chromium-headful/image-chromium/user-data/Default/Feature Engagement Tracker/EventDB/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/000003.log b/images/chromium-headful/image-chromium/user-data/Default/GCM Store/000003.log deleted file mode 100644 index 26e0bf42..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/000003.log and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/CURRENT b/images/chromium-headful/image-chromium/user-data/Default/GCM Store/CURRENT deleted file mode 100644 index 7ed683d1..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/Encryption/000003.log b/images/chromium-headful/image-chromium/user-data/Default/GCM Store/Encryption/000003.log deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/Encryption/CURRENT b/images/chromium-headful/image-chromium/user-data/Default/GCM Store/Encryption/CURRENT deleted file mode 100644 index 7ed683d1..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/Encryption/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/Encryption/LOCK b/images/chromium-headful/image-chromium/user-data/Default/GCM Store/Encryption/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/Encryption/LOG b/images/chromium-headful/image-chromium/user-data/Default/GCM Store/Encryption/LOG deleted file mode 100644 index 352d6c21..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/Encryption/LOG +++ /dev/null @@ -1,2 +0,0 @@ -2025/07/14-21:35:43.655 61 Creating DB /home/kernel/user-data/Default/GCM Store/Encryption since it was missing. -2025/07/14-21:35:43.668 61 Reusing MANIFEST /home/kernel/user-data/Default/GCM Store/Encryption/MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/Encryption/MANIFEST-000001 b/images/chromium-headful/image-chromium/user-data/Default/GCM Store/Encryption/MANIFEST-000001 deleted file mode 100644 index 18e5cab7..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/Encryption/MANIFEST-000001 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/LOCK b/images/chromium-headful/image-chromium/user-data/Default/GCM Store/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/LOG b/images/chromium-headful/image-chromium/user-data/Default/GCM Store/LOG deleted file mode 100644 index 0e2c8274..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/LOG +++ /dev/null @@ -1,2 +0,0 @@ -2025/07/14-21:35:43.648 61 Creating DB /home/kernel/user-data/Default/GCM Store since it was missing. -2025/07/14-21:35:43.654 61 Reusing MANIFEST /home/kernel/user-data/Default/GCM Store/MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/MANIFEST-000001 b/images/chromium-headful/image-chromium/user-data/Default/GCM Store/MANIFEST-000001 deleted file mode 100644 index 18e5cab7..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/GCM Store/MANIFEST-000001 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/GPUCache/data_0 b/images/chromium-headful/image-chromium/user-data/Default/GPUCache/data_0 deleted file mode 100644 index d76fb77e..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/GPUCache/data_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/GPUCache/data_1 b/images/chromium-headful/image-chromium/user-data/Default/GPUCache/data_1 deleted file mode 100644 index 035d06d9..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/GPUCache/data_1 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/GPUCache/data_2 b/images/chromium-headful/image-chromium/user-data/Default/GPUCache/data_2 deleted file mode 100644 index c7e2eb9a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/GPUCache/data_2 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/GPUCache/data_3 b/images/chromium-headful/image-chromium/user-data/Default/GPUCache/data_3 deleted file mode 100644 index 5eec9735..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/GPUCache/data_3 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/GPUCache/index b/images/chromium-headful/image-chromium/user-data/Default/GPUCache/index deleted file mode 100644 index 153c320d..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/GPUCache/index and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/History b/images/chromium-headful/image-chromium/user-data/Default/History deleted file mode 100644 index 08af1355..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/History and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/History-journal b/images/chromium-headful/image-chromium/user-data/Default/History-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/LOCK b/images/chromium-headful/image-chromium/user-data/Default/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/LOG b/images/chromium-headful/image-chromium/user-data/Default/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/LOG.old b/images/chromium-headful/image-chromium/user-data/Default/LOG.old deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Local Storage/leveldb/000003.log b/images/chromium-headful/image-chromium/user-data/Default/Local Storage/leveldb/000003.log deleted file mode 100644 index 5cf554b1..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Local Storage/leveldb/000003.log and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Local Storage/leveldb/CURRENT b/images/chromium-headful/image-chromium/user-data/Default/Local Storage/leveldb/CURRENT deleted file mode 100644 index 7ed683d1..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Local Storage/leveldb/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Local Storage/leveldb/LOCK b/images/chromium-headful/image-chromium/user-data/Default/Local Storage/leveldb/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Local Storage/leveldb/LOG b/images/chromium-headful/image-chromium/user-data/Default/Local Storage/leveldb/LOG deleted file mode 100644 index 2c6019c9..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Local Storage/leveldb/LOG +++ /dev/null @@ -1,2 +0,0 @@ -2025/07/14-21:35:40.334 7 Creating DB /home/kernel/user-data/Default/Local Storage/leveldb since it was missing. -2025/07/14-21:35:40.357 7 Reusing MANIFEST /home/kernel/user-data/Default/Local Storage/leveldb/MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Local Storage/leveldb/MANIFEST-000001 b/images/chromium-headful/image-chromium/user-data/Default/Local Storage/leveldb/MANIFEST-000001 deleted file mode 100644 index 18e5cab7..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Local Storage/leveldb/MANIFEST-000001 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Login Data b/images/chromium-headful/image-chromium/user-data/Default/Login Data deleted file mode 100644 index 9b2e044f..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Login Data and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Login Data For Account b/images/chromium-headful/image-chromium/user-data/Default/Login Data For Account deleted file mode 100644 index 9b2e044f..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Login Data For Account and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Login Data For Account-journal b/images/chromium-headful/image-chromium/user-data/Default/Login Data For Account-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Login Data-journal b/images/chromium-headful/image-chromium/user-data/Default/Login Data-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Network Action Predictor b/images/chromium-headful/image-chromium/user-data/Default/Network Action Predictor deleted file mode 100644 index 682c6381..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Network Action Predictor and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Network Action Predictor-journal b/images/chromium-headful/image-chromium/user-data/Default/Network Action Predictor-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Network Persistent State b/images/chromium-headful/image-chromium/user-data/Default/Network Persistent State deleted file mode 100644 index 6e26da06..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Network Persistent State +++ /dev/null @@ -1 +0,0 @@ -{"net":{"network_qualities":{"CAESABiAgICA+P////8B":"4G"}}} \ No newline at end of file diff --git a/images/chromium-headful/image-chromium/user-data/Default/PersistentOriginTrials/LOCK b/images/chromium-headful/image-chromium/user-data/Default/PersistentOriginTrials/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/PersistentOriginTrials/LOG b/images/chromium-headful/image-chromium/user-data/Default/PersistentOriginTrials/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/PersistentOriginTrials/LOG.old b/images/chromium-headful/image-chromium/user-data/Default/PersistentOriginTrials/LOG.old deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Preferences b/images/chromium-headful/image-chromium/user-data/Default/Preferences deleted file mode 100644 index d17941e1..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Preferences +++ /dev/null @@ -1,669 +0,0 @@ -{ - "accessibility": { - "captions": { - "headless_caption_enabled": false - } - }, - "account_tracker_service_last_update": "13397002540379824", - "alternate_error_pages": { - "backup": false - }, - "announcement_notification_service_first_run_time": "13397002540345407", - "apps": { - "shortcuts_arch": "", - "shortcuts_version": 0 - }, - "autocomplete": { - "retention_policy_last_version": 138 - }, - "autofill": { - "credit_card_enabled": false, - "last_version_deduped": 138, - "profile_enabled": false - }, - "bookmark": { - "storage_computation_last_update": "13397002540363506" - }, - "bookmark_bar": { - "show_on_all_tabs": false - }, - "bookmark_editor": { - "expanded_nodes": [] - }, - "browser": { - "check_default_browser": false, - "has_seen_welcome_page": false, - "show_home_button": true, - "window_placement": { - "bottom": 1080, - "left": 0, - "maximized": true, - "right": 1920, - "top": 0, - "work_area_bottom": 1080, - "work_area_left": 0, - "work_area_right": 1920, - "work_area_top": 0 - } - }, - "commerce_daily_metrics_last_update_time": "13397002540365256", - "credentials_enable_autosignin": false, - "credentials_enable_service": false, - "default_apps": "noinstall", - "default_apps_install_state": 2, - "default_search_provider": { - "guid": "" - }, - "dns_prefetching": { - "enabled": false - }, - "domain_diversity": { - "last_reporting_timestamp": "13397002540366740" - }, - "enterprise_profile_guid": "c0f05e91-11b8-4c7f-9874-cb2124bd3e04", - "extensions": { - "alerts": { - "initialized": true - }, - "chrome_url_overrides": {}, - "last_chrome_version": "138.0.7204.92", - "settings": { - "ahfgeienlihckogmohjhadlkjgocpleb": { - "account_extension_type": 0, - "active_permissions": { - "api": [ - "management", - "system.display", - "system.storage", - "webstorePrivate", - "system.cpu", - "system.memory", - "system.network" - ], - "explicit_host": [], - "manifest_permissions": [], - "scriptable_host": [] - }, - "app_launcher_ordinal": "t", - "commands": {}, - "content_settings": [], - "creation_flags": 1, - "disable_reasons": [], - "events": [], - "first_install_time": "13397002540346984", - "from_webstore": false, - "incognito_content_settings": [], - "incognito_preferences": {}, - "last_update_time": "13397002540346984", - "location": 5, - "manifest": { - "app": { - "launch": { - "web_url": "https://chrome.google.com/webstore" - }, - "urls": [ - "https://chrome.google.com/webstore" - ] - }, - "description": "Discover great apps, games, extensions and themes for Chromium.", - "icons": { - "128": "webstore_icon_128.png", - "16": "webstore_icon_16.png" - }, - "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCtl3tO0osjuzRsf6xtD2SKxPlTfuoy7AWoObysitBPvH5fE1NaAA1/2JkPWkVDhdLBWLaIBPYeXbzlHp3y4Vv/4XG+aN5qFE3z+1RU/NqkzVYHtIpVScf3DjTYtKVL66mzVGijSoAIwbFCC3LpGdaoe6Q1rSRDp76wR6jjFzsYwQIDAQAB", - "name": "Web Store", - "permissions": [ - "webstorePrivate", - "management", - "system.cpu", - "system.display", - "system.memory", - "system.network", - "system.storage" - ], - "version": "0.2" - }, - "needs_sync": true, - "page_ordinal": "n", - "path": "/usr/lib/chromium/resources/web_store", - "preferences": {}, - "regular_only_preferences": {}, - "was_installed_by_default": false, - "was_installed_by_oem": false - }, - "mhjfbmdgcfjbbpaeojofohoefgiehjai": { - "account_extension_type": 0, - "active_permissions": { - "api": [ - "contentSettings", - "fileSystem", - "fileSystem.write", - "metricsPrivate", - "tabs", - "resourcesPrivate", - "pdfViewerPrivate" - ], - "explicit_host": [ - "chrome://resources/*", - "chrome://webui-test/*" - ], - "manifest_permissions": [], - "scriptable_host": [] - }, - "commands": {}, - "content_settings": [], - "creation_flags": 1, - "disable_reasons": [], - "events": [], - "first_install_time": "13397002540348317", - "from_webstore": false, - "incognito_content_settings": [], - "incognito_preferences": {}, - "last_update_time": "13397002540348317", - "location": 5, - "manifest": { - "content_security_policy": "script-src 'self' 'wasm-eval' blob: filesystem: chrome://resources chrome://webui-test; object-src * blob: externalfile: file: filesystem: data:", - "description": "", - "incognito": "split", - "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDN6hM0rsDYGbzQPQfOygqlRtQgKUXMfnSjhIBL7LnReAVBEd7ZmKtyN2qmSasMl4HZpMhVe2rPWVVwBDl6iyNE/Kok6E6v6V3vCLGsOpQAuuNVye/3QxzIldzG/jQAdWZiyXReRVapOhZtLjGfywCvlWq7Sl/e3sbc0vWybSDI2QIDAQAB", - "manifest_version": 2, - "mime_types": [ - "application/pdf" - ], - "mime_types_handler": "index.html", - "name": "Chromium PDF Viewer", - "offline_enabled": true, - "permissions": [ - "chrome://resources/", - "chrome://webui-test/", - "contentSettings", - "metricsPrivate", - "pdfViewerPrivate", - "resourcesPrivate", - "tabs", - { - "fileSystem": [ - "write" - ] - } - ], - "version": "1" - }, - "path": "/usr/lib/chromium/resources/pdf", - "preferences": {}, - "regular_only_preferences": {}, - "was_installed_by_default": false, - "was_installed_by_oem": false - } - } - }, - "gaia_cookie": { - "changed_time": 1752528940.551012, - "hash": "2jmj7l5rSw0yVb/vlWAYkK/YBwk=", - "last_list_accounts_data": "[\"gaia.l.a.r\",[]]" - }, - "gcm": { - "product_category_for_subtypes": "org.chromium.linux" - }, - "google": { - "services": { - "signin_scoped_device_id": "430a74f4-d0ef-4e2f-90ba-3f1fd43615cd" - } - }, - "hide_web_store_icon": true, - "homepage": "https://www.debian.org", - "homepage_is_newtabpage": true, - "import_bookmarks": false, - "in_product_help": { - "new_badge": { - "Compose": { - "feature_enabled_time": "13397002540447621", - "show_count": 0, - "used_count": 0 - }, - "ComposeNudge": { - "feature_enabled_time": "13397002540447624", - "show_count": 0, - "used_count": 0 - }, - "ComposeProactiveNudge": { - "feature_enabled_time": "13397002540447627", - "show_count": 0, - "used_count": 0 - }, - "LensOverlay": { - "feature_enabled_time": "13397002540447632", - "show_count": 0, - "used_count": 0 - }, - "PasswordManualFallbackAvailable": { - "feature_enabled_time": "13397002540447607", - "show_count": 0, - "used_count": 0 - } - }, - "recent_session_enabled_time": "13397002540446548", - "recent_session_start_times": [ - "13397002540446548" - ], - "session_last_active_time": "13397002539451164", - "session_start_time": "13397002540446548" - }, - "intl": { - "selected_languages": "en-US,en" - }, - "invalidation": { - "per_sender_topics_to_handler": { - "1013309121859": {} - } - }, - "media": { - "engagement": { - "schema_version": 5 - } - }, - "media_router": { - "receiver_id_hash_token": "PTEzBQppYTBlX7NrVi5hBBqdZWYdFMWWrG+nTRJLy5so3Mgy0PNyZUCEz+/JCaMg0Fn9jCYMRN5HXS5GYBIvzg==" - }, - "migrated_user_scripts_toggle": true, - "net": { - "network_prediction_options": 2 - }, - "ntp": { - "num_personal_suggestions": 0 - }, - "optimization_guide": { - "hintsfetcher": { - "hosts_successfully_fetched": {} - }, - "predictionmodelfetcher": { - "last_fetch_attempt": "13397002550348987" - }, - "previously_registered_optimization_types": { - "ABOUT_THIS_SITE": true, - "HISTORY_CLUSTERS": true, - "LOADING_PREDICTOR": true, - "MERCHANT_TRUST_SIGNALS_V2": true, - "PRICE_TRACKING": true, - "V8_COMPILE_HINTS": true - }, - "store_file_paths_to_delete": {} - }, - "password_manager": { - "autofillable_credentials_account_store_login_database": false, - "autofillable_credentials_profile_store_login_database": false - }, - "payments": { - "can_make_payment_enabled": false - }, - "privacy_sandbox": { - "first_party_sets_data_access_allowed_initialized": true, - "first_party_sets_enabled": false, - "m1": { - "ad_measurement_enabled": false, - "fledge_enabled": false, - "topics_enabled": false - } - }, - "profile": { - "avatar_index": 26, - "background_password_check": { - "check_fri_weight": 9, - "check_interval": "2592000000000", - "check_mon_weight": 6, - "check_sat_weight": 6, - "check_sun_weight": 6, - "check_thu_weight": 9, - "check_tue_weight": 9, - "check_wed_weight": 9, - "next_check_time": "13399474185266031" - }, - "content_settings": { - "exceptions": { - "3pcd_heuristics_grants": {}, - "3pcd_support": {}, - "abusive_notification_permissions": {}, - "access_to_get_all_screens_media_in_session": {}, - "anti_abuse": {}, - "app_banner": {}, - "ar": {}, - "are_suspicious_notifications_allowlisted_by_user": {}, - "auto_picture_in_picture": {}, - "auto_select_certificate": {}, - "automatic_downloads": {}, - "automatic_fullscreen": {}, - "autoplay": {}, - "background_sync": {}, - "bluetooth_chooser_data": {}, - "bluetooth_guard": {}, - "bluetooth_scanning": {}, - "camera_pan_tilt_zoom": {}, - "captured_surface_control": {}, - "client_hints": {}, - "clipboard": {}, - "controlled_frame": {}, - "cookie_controls_metadata": {}, - "cookies": {}, - "direct_sockets": {}, - "direct_sockets_private_network_access": {}, - "display_media_system_audio": {}, - "disruptive_notification_permissions": {}, - "durable_storage": {}, - "fedcm_idp_registration": {}, - "fedcm_idp_signin": { - "https://accounts.google.com:443,*": { - "last_modified": "13397002540564600", - "setting": { - "chosen-objects": [ - { - "idp-origin": "https://accounts.google.com", - "idp-signin-status": false - } - ] - } - } - }, - "fedcm_share": {}, - "file_system_access_chooser_data": {}, - "file_system_access_extended_permission": {}, - "file_system_access_restore_permission": {}, - "file_system_last_picked_directory": {}, - "file_system_read_guard": {}, - "file_system_write_guard": {}, - "formfill_metadata": {}, - "geolocation": {}, - "hand_tracking": {}, - "hid_chooser_data": {}, - "hid_guard": {}, - "http_allowed": {}, - "https_enforced": {}, - "idle_detection": {}, - "images": {}, - "important_site_info": {}, - "initialized_translations": {}, - "intent_picker_auto_display": {}, - "javascript": {}, - "javascript_jit": {}, - "javascript_optimizer": {}, - "keyboard_lock": {}, - "legacy_cookie_access": {}, - "legacy_cookie_scope": {}, - "local_fonts": {}, - "local_network_access": {}, - "media_engagement": {}, - "media_stream_camera": {}, - "media_stream_mic": {}, - "midi_sysex": {}, - "mixed_script": {}, - "nfc_devices": {}, - "notification_interactions": {}, - "notification_permission_review": {}, - "notifications": {}, - "ondevice_languages_downloaded": {}, - "password_protection": {}, - "payment_handler": {}, - "permission_autoblocking_data": {}, - "permission_autorevocation_data": {}, - "pointer_lock": {}, - "popups": {}, - "private_network_chooser_data": {}, - "private_network_guard": {}, - "protocol_handler": {}, - "reduced_accept_language": {}, - "safe_browsing_url_check_data": {}, - "sensors": {}, - "serial_chooser_data": {}, - "serial_guard": {}, - "site_engagement": { - "chrome://newtab/,*": { - "last_modified": "13397002540652513", - "setting": { - "lastEngagementTime": 1.3397002540652496e+16, - "lastShortcutLaunchTime": 0.0, - "pointsAddedToday": 3.0, - "rawScore": 3.0 - } - } - }, - "sound": {}, - "speaker_selection": {}, - "ssl_cert_decisions": {}, - "storage_access": {}, - "storage_access_header_origin_trial": {}, - "subresource_filter": {}, - "subresource_filter_data": {}, - "suspicious_notification_ids": {}, - "third_party_storage_partitioning": {}, - "top_level_3pcd_origin_trial": {}, - "top_level_3pcd_support": {}, - "top_level_storage_access": {}, - "tracking_protection": {}, - "unused_site_permissions": {}, - "usb_chooser_data": {}, - "usb_guard": {}, - "vr": {}, - "web_app_installation": {}, - "webid_api": {}, - "webid_auto_reauthn": {}, - "window_placement": {} - }, - "pref_version": 1 - }, - "created_by_version": "138.0.7204.92", - "creation_time": "13397002539586935", - "default_content_setting_values": { - "payment_handler": 2 - }, - "exit_type": "Normal", - "last_engagement_time": "13397002540652495", - "last_time_password_store_metrics_reported": 1752528970.340227, - "managed": { - "locally_parent_approved_extensions": {}, - "locally_parent_approved_extensions_migration_state": 1 - }, - "managed_user_id": "", - "name": "Work", - "password_hash_data_list": [] - }, - "protection": { - "macs": { - "account_values": { - "browser": { - "show_home_button": "F6B8688079AA4C4657C3FEDCF3311348C99CB4907EEC8560CC323E4F37D1EA41" - }, - "extensions": { - "ui": { - "developer_mode": "C4B515ABCB16541C88B522387A89F47E24F150FF18A7256BF19EE2C0896D7555" - } - }, - "homepage": "0F156BE8FFEDBF22CC2E394D6BD8DDD392F953D0F397958C64EFA57087182457", - "homepage_is_newtabpage": "BAF0DE42A6FEAC5EF6500592AEC92B79DC055C1E352B0F7BAC0BB6E72ECE67D3", - "session": { - "restore_on_startup": "81940185F7F78D0317AB3741FA4A283985C31DB3BF95D3A4DDD4CD78FC5021F1", - "startup_urls": "B41DC391D323AEB2C5465D75AB79284F9F6620BEF161110DE35202261FF03449" - } - }, - "browser": { - "show_home_button": "9463DEC4C15E47646D05FD921E0E475249EA15DE77808C4664EF238C54D89FC7" - }, - "default_search_provider_data": { - "template_url_data": "575D258E47F940C6887685ABA99A5839CBFE4BA30863349DFE0D0C375AAB8816" - }, - "enterprise_signin": { - "policy_recovery_token": "7D3124ECAF7E96407EB65EAF5A43B02C7EE5F2D4A9FA38A9F371F9E1B74D6383" - }, - "extensions": { - "settings": { - "ahfgeienlihckogmohjhadlkjgocpleb": "B03BE93459FFB9DE09C8A3FAB18E9A1AA826422D11C70E65E817360289556243", - "mhjfbmdgcfjbbpaeojofohoefgiehjai": "C5802703F8AB841966A50F555B5CCE1A83645639CB2043D59D34B67184FFFAA1" - }, - "ui": { - "developer_mode": "55A29C051727FCAC1BDA847CCDE40838B2068CFA589E88819EA9FB17E4BD7320" - } - }, - "google": { - "services": { - "account_id": "E5B4CD7C5FA271A47D07D462465AFD63DBF6A8CDFAFEF4839D13F8F552131486", - "last_signed_in_username": "82DB8D884695C643C31778B7B50DBB376848E2F81B5A1AECDA34FD448CECD10D", - "last_username": "24FCEF9BF7DF12A2935BE143E58951E09DBAA1D3E0E24430C0FF93009F5D6AFD" - } - }, - "homepage": "612967A208F154218AEDE6D1D3C112663279440137971DED3749F688D094D9E2", - "homepage_is_newtabpage": "547DD594A200A6C826A149309738E7FAD622F4D7B1B875CD6ECB2E96F2940E21", - "media": { - "storage_id_salt": "E1848263E6199A89D48A7FDF168364BF0F31246A18227F3D149D4088C7F4D667" - }, - "pinned_tabs": "5FF265371BB528ED630092A900058C08217611AB525D4C12B41C44C008BAC799", - "prefs": { - "preference_reset_time": "95C909F3D0669D5931907B455F099C510E7770D9F0BA6FF13E4C76101B44F757" - }, - "safebrowsing": { - "incidents_sent": "569707D9A4676B72F48BE92B740BE3EF895419C8A646F1AE1BA70BD9C3B41845" - }, - "search_provider_overrides": "359EACF4B741613E008062C66B6DAD80C68DDBC38DABA7455548A8BEE34C3FE7", - "session": { - "restore_on_startup": "F9BD26F5D1AA6AB5258754888529CB2A82AE68D1703BCC2A97DEAEE5DDDA190E", - "startup_urls": "8BB8DBC1D7CA5C58F821C38254FB2B9C874F8EE9B9905B57DE48C731C6C91837" - } - } - }, - "safebrowsing": { - "enabled": false, - "event_timestamps": {}, - "metrics_last_log_time": "13397002540", - "scout_reporting_enabled_when_deprecated": false - }, - "safety_hub": { - "unused_site_permissions_revocation": { - "migration_completed": true - } - }, - "saved_tab_groups": { - "did_enable_shared_tab_groups_in_last_session": false, - "specifics_to_data_migration": true - }, - "search": { - "suggest_enabled": false - }, - "search_provider_overrides": [ - { - "enabled": true, - "encoding": "UTF-8", - "favicon_url": "https://duckduckgo.com/favicon.ico", - "id": 2, - "keyword": "duckduckgo.com", - "name": "DuckDuckGo", - "new_tab_url": "https://duckduckgo.com/chrome_newtab", - "search_url": "https://duckduckgo.com/?q={searchTerms}", - "suggest_url": "https://duckduckgo.com/ac/?q={searchTerms}&type=list" - } - ], - "search_provider_overrides_version": 1, - "segmentation_platform": { - "client_result_prefs": "ClIKDXNob3BwaW5nX3VzZXISQQo2DQAAAAAQ5NLuwoaQ5hcaJAocChoNAAAAPxIMU2hvcHBpbmdVc2VyGgVPdGhlchIEEAIYBCADEMaBl86LkOYX", - "last_db_compaction_time": "13396838399000000", - "uma_in_sql_start_time": "13397002540342241" - }, - "sessions": { - "event_log": [ - { - "crashed": false, - "time": "13397002540340127", - "type": 0 - } - ], - "session_data_status": 1 - }, - "settings": { - "force_google_safesearch": false - }, - "signin": { - "allowed": false, - "allowed_on_next_startup": false, - "cookie_clear_on_exit_migration_notice_complete": true - }, - "spellcheck": { - "dictionaries": [ - "en-US" - ], - "dictionary": "" - }, - "sync": { - "data_type_status_for_sync_to_signin": { - "app_list": false, - "app_settings": false, - "apps": false, - "arc_package": false, - "autofill": false, - "autofill_profiles": false, - "autofill_valuable": false, - "autofill_wallet": false, - "autofill_wallet_credential": false, - "autofill_wallet_metadata": false, - "autofill_wallet_offer": false, - "autofill_wallet_usage": false, - "bookmarks": false, - "collaboration_group": false, - "contact_info": false, - "cookies": false, - "device_info": false, - "dictionary": false, - "extension_settings": false, - "extensions": false, - "history": false, - "history_delete_directives": false, - "incoming_password_sharing_invitation": false, - "managed_user_settings": false, - "nigori": false, - "os_preferences": false, - "os_priority_preferences": false, - "outgoing_password_sharing_invitation": false, - "passwords": false, - "plus_address": false, - "plus_address_setting": false, - "power_bookmark": false, - "preferences": false, - "printers": false, - "printers_authorization_servers": false, - "priority_preferences": false, - "product_comparison": false, - "reading_list": false, - "saved_tab_group": false, - "search_engines": false, - "security_events": false, - "send_tab_to_self": false, - "sessions": false, - "shared_tab_group_account_data": false, - "shared_tab_group_data": false, - "sharing_message": false, - "themes": false, - "user_consent": false, - "user_events": false, - "web_apps": false, - "webapks": false, - "webauthn_credential": false, - "wifi_configurations": false, - "workspace_desk": false - }, - "encryption_bootstrap_token_per_account_migration_done": true, - "feature_status_for_sync_to_signin": 5, - "passwords_per_account_pref_migration_done": true - }, - "syncing_theme_prefs_migrated_to_non_syncing": true, - "tab_group_saves_ui_update_migrated": true, - "toolbar": { - "pinned_cast_migration_complete": true, - "pinned_chrome_labs_migration_complete": true - }, - "total_passwords_available_for_account": 0, - "total_passwords_available_for_profile": 0, - "translate_site_blacklist": [], - "translate_site_blocklist_with_time": {}, - "web_apps": { - "did_migrate_default_chrome_apps": [ - "MigrateDefaultChromeAppToWebAppsGSuite", - "MigrateDefaultChromeAppToWebAppsNonGSuite" - ], - "last_preinstall_synchronize_version": "138" - } -} diff --git a/images/chromium-headful/image-chromium/user-data/Default/PreferredApps b/images/chromium-headful/image-chromium/user-data/Default/PreferredApps deleted file mode 100644 index 7d3a4259..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/PreferredApps +++ /dev/null @@ -1 +0,0 @@ -{"preferred_apps":[],"version":1} \ No newline at end of file diff --git a/images/chromium-headful/image-chromium/user-data/Default/Reporting and NEL b/images/chromium-headful/image-chromium/user-data/Default/Reporting and NEL deleted file mode 100644 index 9ee6cf21..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Reporting and NEL and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Reporting and NEL-journal b/images/chromium-headful/image-chromium/user-data/Default/Reporting and NEL-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/SCT Auditing Pending Reports b/images/chromium-headful/image-chromium/user-data/Default/SCT Auditing Pending Reports deleted file mode 100644 index 0637a088..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/SCT Auditing Pending Reports +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/images/chromium-headful/image-chromium/user-data/Default/Safe Browsing Cookies b/images/chromium-headful/image-chromium/user-data/Default/Safe Browsing Cookies deleted file mode 100644 index 9827cfa7..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Safe Browsing Cookies and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Safe Browsing Cookies-journal b/images/chromium-headful/image-chromium/user-data/Default/Safe Browsing Cookies-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Secure Preferences b/images/chromium-headful/image-chromium/user-data/Default/Secure Preferences deleted file mode 100644 index 5fa475e9..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Secure Preferences +++ /dev/null @@ -1 +0,0 @@ -{"protection":{"super_mac":"B613679A0814D9EC772F95D778C35FC5FF1697C493715653C6C712144292C5AD"}} \ No newline at end of file diff --git a/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SegmentInfoDB/LOCK b/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SegmentInfoDB/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SegmentInfoDB/LOG b/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SegmentInfoDB/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SegmentInfoDB/LOG.old b/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SegmentInfoDB/LOG.old deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SignalDB/LOCK b/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SignalDB/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SignalDB/LOG b/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SignalDB/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SignalDB/LOG.old b/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SignalDB/LOG.old deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SignalStorageConfigDB/LOCK b/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SignalStorageConfigDB/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SignalStorageConfigDB/LOG b/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SignalStorageConfigDB/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SignalStorageConfigDB/LOG.old b/images/chromium-headful/image-chromium/user-data/Default/Segmentation Platform/SignalStorageConfigDB/LOG.old deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/ServerCertificate b/images/chromium-headful/image-chromium/user-data/Default/ServerCertificate deleted file mode 100644 index 2e1495ab..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/ServerCertificate and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/ServerCertificate-journal b/images/chromium-headful/image-chromium/user-data/Default/ServerCertificate-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Session Storage/000003.log b/images/chromium-headful/image-chromium/user-data/Default/Session Storage/000003.log deleted file mode 100644 index 881811d9..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Session Storage/000003.log and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Session Storage/CURRENT b/images/chromium-headful/image-chromium/user-data/Default/Session Storage/CURRENT deleted file mode 100644 index 7ed683d1..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Session Storage/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Session Storage/LOCK b/images/chromium-headful/image-chromium/user-data/Default/Session Storage/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Session Storage/LOG b/images/chromium-headful/image-chromium/user-data/Default/Session Storage/LOG deleted file mode 100644 index a1eb8118..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Session Storage/LOG +++ /dev/null @@ -1,2 +0,0 @@ -2025/07/14-21:35:40.542 7 Creating DB /home/kernel/user-data/Default/Session Storage since it was missing. -2025/07/14-21:35:40.557 7 Reusing MANIFEST /home/kernel/user-data/Default/Session Storage/MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Session Storage/MANIFEST-000001 b/images/chromium-headful/image-chromium/user-data/Default/Session Storage/MANIFEST-000001 deleted file mode 100644 index 18e5cab7..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Session Storage/MANIFEST-000001 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Sessions/Session_13397002542845659 b/images/chromium-headful/image-chromium/user-data/Default/Sessions/Session_13397002542845659 deleted file mode 100644 index 5411adde..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Sessions/Session_13397002542845659 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Shared Dictionary/cache/index b/images/chromium-headful/image-chromium/user-data/Default/Shared Dictionary/cache/index deleted file mode 100644 index 79bd403a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Shared Dictionary/cache/index and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Shared Dictionary/cache/index-dir/the-real-index b/images/chromium-headful/image-chromium/user-data/Default/Shared Dictionary/cache/index-dir/the-real-index deleted file mode 100644 index 048d66c0..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Shared Dictionary/cache/index-dir/the-real-index and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Shared Dictionary/db b/images/chromium-headful/image-chromium/user-data/Default/Shared Dictionary/db deleted file mode 100644 index 2daa288c..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Shared Dictionary/db and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Shared Dictionary/db-journal b/images/chromium-headful/image-chromium/user-data/Default/Shared Dictionary/db-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/SharedStorage b/images/chromium-headful/image-chromium/user-data/Default/SharedStorage deleted file mode 100644 index db7a7459..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/SharedStorage and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/SharedStorage-wal b/images/chromium-headful/image-chromium/user-data/Default/SharedStorage-wal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Shortcuts b/images/chromium-headful/image-chromium/user-data/Default/Shortcuts deleted file mode 100644 index e3fed18e..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Shortcuts and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Shortcuts-journal b/images/chromium-headful/image-chromium/user-data/Default/Shortcuts-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/000003.log b/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/000003.log deleted file mode 100644 index 46580fb8..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/000003.log and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/CURRENT b/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/CURRENT deleted file mode 100644 index 7ed683d1..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/LOCK b/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/LOG b/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/LOG deleted file mode 100644 index cffe2e42..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/LOG +++ /dev/null @@ -1,3 +0,0 @@ -2025/07/14-21:35:40.335 6d Reusing MANIFEST /home/kernel/user-data/Default/Site Characteristics Database/MANIFEST-000001 -2025/07/14-21:35:40.335 6d Recovering log #3 -2025/07/14-21:35:40.335 6d Reusing old log /home/kernel/user-data/Default/Site Characteristics Database/000003.log diff --git a/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/LOG.old b/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/LOG.old deleted file mode 100644 index bd85fc55..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/LOG.old +++ /dev/null @@ -1,2 +0,0 @@ -2025/07/14-21:12:54.425 71 Creating DB /home/kernel/user-data/Default/Site Characteristics Database since it was missing. -2025/07/14-21:12:54.437 71 Reusing MANIFEST /home/kernel/user-data/Default/Site Characteristics Database/MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/MANIFEST-000001 b/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/MANIFEST-000001 deleted file mode 100644 index 18e5cab7..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Site Characteristics Database/MANIFEST-000001 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Sync Data/LevelDB/000003.log b/images/chromium-headful/image-chromium/user-data/Default/Sync Data/LevelDB/000003.log deleted file mode 100644 index 3c85b66d..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Sync Data/LevelDB/000003.log and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Sync Data/LevelDB/CURRENT b/images/chromium-headful/image-chromium/user-data/Default/Sync Data/LevelDB/CURRENT deleted file mode 100644 index 7ed683d1..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Sync Data/LevelDB/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Sync Data/LevelDB/LOCK b/images/chromium-headful/image-chromium/user-data/Default/Sync Data/LevelDB/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Sync Data/LevelDB/LOG b/images/chromium-headful/image-chromium/user-data/Default/Sync Data/LevelDB/LOG deleted file mode 100644 index 7320b57f..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/Sync Data/LevelDB/LOG +++ /dev/null @@ -1,2 +0,0 @@ -2025/07/14-21:35:40.319 62 Creating DB /home/kernel/user-data/Default/Sync Data/LevelDB since it was missing. -2025/07/14-21:35:40.344 62 Reusing MANIFEST /home/kernel/user-data/Default/Sync Data/LevelDB/MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Sync Data/LevelDB/MANIFEST-000001 b/images/chromium-headful/image-chromium/user-data/Default/Sync Data/LevelDB/MANIFEST-000001 deleted file mode 100644 index 18e5cab7..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Sync Data/LevelDB/MANIFEST-000001 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Top Sites b/images/chromium-headful/image-chromium/user-data/Default/Top Sites deleted file mode 100644 index 4c37d6d1..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Top Sites and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Top Sites-journal b/images/chromium-headful/image-chromium/user-data/Default/Top Sites-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Trust Tokens b/images/chromium-headful/image-chromium/user-data/Default/Trust Tokens deleted file mode 100644 index 8f79546a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Trust Tokens and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Trust Tokens-journal b/images/chromium-headful/image-chromium/user-data/Default/Trust Tokens-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/Web Data b/images/chromium-headful/image-chromium/user-data/Default/Web Data deleted file mode 100644 index 27e2c8ac..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/Web Data and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/Web Data-journal b/images/chromium-headful/image-chromium/user-data/Default/Web Data-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/WebStorage/QuotaManager b/images/chromium-headful/image-chromium/user-data/Default/WebStorage/QuotaManager deleted file mode 100644 index 95f8a62a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/WebStorage/QuotaManager and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/WebStorage/QuotaManager-journal b/images/chromium-headful/image-chromium/user-data/Default/WebStorage/QuotaManager-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/chrome_cart_db/LOCK b/images/chromium-headful/image-chromium/user-data/Default/chrome_cart_db/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/chrome_cart_db/LOG b/images/chromium-headful/image-chromium/user-data/Default/chrome_cart_db/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/commerce_subscription_db/LOCK b/images/chromium-headful/image-chromium/user-data/Default/commerce_subscription_db/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/commerce_subscription_db/LOG b/images/chromium-headful/image-chromium/user-data/Default/commerce_subscription_db/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/discounts_db/LOCK b/images/chromium-headful/image-chromium/user-data/Default/discounts_db/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/discounts_db/LOG b/images/chromium-headful/image-chromium/user-data/Default/discounts_db/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/heavy_ad_intervention_opt_out.db b/images/chromium-headful/image-chromium/user-data/Default/heavy_ad_intervention_opt_out.db deleted file mode 100644 index ad6748db..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/heavy_ad_intervention_opt_out.db and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/heavy_ad_intervention_opt_out.db-journal b/images/chromium-headful/image-chromium/user-data/Default/heavy_ad_intervention_opt_out.db-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/optimization_guide_hint_cache_store/LOCK b/images/chromium-headful/image-chromium/user-data/Default/optimization_guide_hint_cache_store/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/optimization_guide_hint_cache_store/LOG b/images/chromium-headful/image-chromium/user-data/Default/optimization_guide_hint_cache_store/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/optimization_guide_hint_cache_store/LOG.old b/images/chromium-headful/image-chromium/user-data/Default/optimization_guide_hint_cache_store/LOG.old deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/parcel_tracking_db/LOCK b/images/chromium-headful/image-chromium/user-data/Default/parcel_tracking_db/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/parcel_tracking_db/LOG b/images/chromium-headful/image-chromium/user-data/Default/parcel_tracking_db/LOG deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/parcel_tracking_db/LOG.old b/images/chromium-headful/image-chromium/user-data/Default/parcel_tracking_db/LOG.old deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/000003.log b/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/000003.log deleted file mode 100644 index 99e66752..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/000003.log and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/CURRENT b/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/CURRENT deleted file mode 100644 index 7ed683d1..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/LOCK b/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/LOG b/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/LOG deleted file mode 100644 index 6c7b1607..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/LOG +++ /dev/null @@ -1,3 +0,0 @@ -2025/07/14-21:35:40.367 6e Reusing MANIFEST /home/kernel/user-data/Default/shared_proto_db/MANIFEST-000001 -2025/07/14-21:35:40.367 6e Recovering log #3 -2025/07/14-21:35:40.368 6e Reusing old log /home/kernel/user-data/Default/shared_proto_db/000003.log diff --git a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/LOG.old b/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/LOG.old deleted file mode 100644 index 79791817..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/LOG.old +++ /dev/null @@ -1,2 +0,0 @@ -2025/07/14-21:12:54.454 61 Creating DB /home/kernel/user-data/Default/shared_proto_db since it was missing. -2025/07/14-21:12:54.459 61 Reusing MANIFEST /home/kernel/user-data/Default/shared_proto_db/MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/MANIFEST-000001 b/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/MANIFEST-000001 deleted file mode 100644 index 18e5cab7..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/MANIFEST-000001 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/000003.log b/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/000003.log deleted file mode 100644 index 4446596a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/000003.log and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/CURRENT b/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/CURRENT deleted file mode 100644 index 7ed683d1..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/LOCK b/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/LOG b/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/LOG deleted file mode 100644 index a450e914..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/LOG +++ /dev/null @@ -1,3 +0,0 @@ -2025/07/14-21:35:40.366 6e Reusing MANIFEST /home/kernel/user-data/Default/shared_proto_db/metadata/MANIFEST-000001 -2025/07/14-21:35:40.366 6e Recovering log #3 -2025/07/14-21:35:40.367 6e Reusing old log /home/kernel/user-data/Default/shared_proto_db/metadata/000003.log diff --git a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/LOG.old b/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/LOG.old deleted file mode 100644 index dacf1b1b..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/LOG.old +++ /dev/null @@ -1,2 +0,0 @@ -2025/07/14-21:12:54.447 61 Creating DB /home/kernel/user-data/Default/shared_proto_db/metadata since it was missing. -2025/07/14-21:12:54.452 61 Reusing MANIFEST /home/kernel/user-data/Default/shared_proto_db/metadata/MANIFEST-000001 diff --git a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/MANIFEST-000001 b/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/MANIFEST-000001 deleted file mode 100644 index 18e5cab7..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Default/shared_proto_db/metadata/MANIFEST-000001 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Default/trusted_vault.pb b/images/chromium-headful/image-chromium/user-data/Default/trusted_vault.pb deleted file mode 100644 index 0aac9031..00000000 --- a/images/chromium-headful/image-chromium/user-data/Default/trusted_vault.pb +++ /dev/null @@ -1,2 +0,0 @@ - - 0ba4067c95d8d92744702afdd1697107 \ No newline at end of file diff --git a/images/chromium-headful/image-chromium/user-data/Dictionaries/en-US-10-1.bdic b/images/chromium-headful/image-chromium/user-data/Dictionaries/en-US-10-1.bdic deleted file mode 100644 index a4533584..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/Dictionaries/en-US-10-1.bdic and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/First Run b/images/chromium-headful/image-chromium/user-data/First Run deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/GrShaderCache/data_0 b/images/chromium-headful/image-chromium/user-data/GrShaderCache/data_0 deleted file mode 100644 index d76fb77e..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/GrShaderCache/data_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/GrShaderCache/data_1 b/images/chromium-headful/image-chromium/user-data/GrShaderCache/data_1 deleted file mode 100644 index 035d06d9..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/GrShaderCache/data_1 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/GrShaderCache/data_2 b/images/chromium-headful/image-chromium/user-data/GrShaderCache/data_2 deleted file mode 100644 index c7e2eb9a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/GrShaderCache/data_2 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/GrShaderCache/data_3 b/images/chromium-headful/image-chromium/user-data/GrShaderCache/data_3 deleted file mode 100644 index 5eec9735..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/GrShaderCache/data_3 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/GrShaderCache/index b/images/chromium-headful/image-chromium/user-data/GrShaderCache/index deleted file mode 100644 index 97de65cd..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/GrShaderCache/index and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/GraphiteDawnCache/data_0 b/images/chromium-headful/image-chromium/user-data/GraphiteDawnCache/data_0 deleted file mode 100644 index d76fb77e..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/GraphiteDawnCache/data_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/GraphiteDawnCache/data_1 b/images/chromium-headful/image-chromium/user-data/GraphiteDawnCache/data_1 deleted file mode 100644 index 035d06d9..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/GraphiteDawnCache/data_1 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/GraphiteDawnCache/data_2 b/images/chromium-headful/image-chromium/user-data/GraphiteDawnCache/data_2 deleted file mode 100644 index c7e2eb9a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/GraphiteDawnCache/data_2 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/GraphiteDawnCache/data_3 b/images/chromium-headful/image-chromium/user-data/GraphiteDawnCache/data_3 deleted file mode 100644 index 5eec9735..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/GraphiteDawnCache/data_3 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/GraphiteDawnCache/index b/images/chromium-headful/image-chromium/user-data/GraphiteDawnCache/index deleted file mode 100644 index 5849a92c..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/GraphiteDawnCache/index and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Last Version b/images/chromium-headful/image-chromium/user-data/Last Version deleted file mode 100644 index c7e8f35e..00000000 --- a/images/chromium-headful/image-chromium/user-data/Last Version +++ /dev/null @@ -1 +0,0 @@ -138.0.7204.92 \ No newline at end of file diff --git a/images/chromium-headful/image-chromium/user-data/Local State b/images/chromium-headful/image-chromium/user-data/Local State deleted file mode 100644 index 06e0b480..00000000 --- a/images/chromium-headful/image-chromium/user-data/Local State +++ /dev/null @@ -1 +0,0 @@ -{"autofill":{"ablation_seed":"RlhGLF81jws="},"breadcrumbs":{"enabled":false,"enabled_time":"13397001174396465"},"browser":{"first_run_finished":true,"whats_new":{"enabled_order":["PdfSearchify"]}},"chrome_labs_activation_threshold":42,"chrome_labs_new_badge_dict":{},"hardware_acceleration_mode_previous":true,"legacy":{"profile":{"name":{"migrated":true}}},"local":{"password_hash_data_list":[]},"network_time":{"network_time_mapping":{"local":1.752527574570403e+12,"network":1.752527574482e+12,"ticks":233216669518.0,"uncertainty":10034761.0}},"optimization_guide":{"model_execution":{"last_usage_by_feature":{}},"model_store_metadata":{},"on_device":{"last_version":"138.0.7204.92","model_crash_count":0}},"os_crypt":{"portal":{"prev_desktop":"","prev_init_success":false}},"performance_intervention":{"last_daily_sample":"13397001174564458"},"policy":{"last_statistics_update":"13397001174393531"},"privacy_budget":{"meta_experiment_activation_salt":0.7939077203717484},"profile":{"info_cache":{"Default":{"active_time":1752527574.563824,"avatar_icon":"chrome://theme/IDR_PROFILE_AVATAR_26","background_apps":false,"default_avatar_fill_color":-2890755,"default_avatar_stroke_color":-16166200,"enterprise_label":"","force_signin_profile_locked":false,"gaia_given_name":"","gaia_id":"","gaia_name":"","hosted_domain":"","is_consented_primary_account":false,"is_ephemeral":false,"is_glic_eligible":false,"is_using_default_avatar":true,"is_using_default_name":false,"managed_user_id":"","metrics_bucket_index":1,"name":"Work","profile_color_seed":-16033840,"profile_highlight_color":-2890755,"signin.with_credential_provider":false,"user_name":""}},"last_active_profiles":["Default"],"metrics":{"next_bucket_index":2},"profile_counts_reported":"13397001174401670","profiles_order":["Default"]},"profile_network_context_service":{"http_cache_finch_experiment_groups":"None None None None"},"session_id_generator_last_value":"245997286","signin":{"active_accounts_last_emitted":"13397001174321236"},"subresource_filter":{"ruleset_version":{"checksum":0,"content":"","format":0}},"tab_stats":{"discards_external":0,"discards_frozen":0,"discards_proactive":0,"discards_suggested":0,"discards_urgent":0,"last_daily_sample":"13397001174386773","max_tabs_per_window":1,"reloads_external":0,"reloads_frozen":0,"reloads_proactive":0,"reloads_suggested":0,"reloads_urgent":0,"total_tab_count_max":1,"window_count_max":1},"ukm":{"persisted_logs":[]},"uninstall_metrics":{"installation_date2":"1752527574"},"user_experience_metrics":{"limited_entropy_randomization_source":"7FEB1E55BC257148E41C169CE3739F87","low_entropy_source3":4533,"pseudo_low_entropy_source":4231,"session_id":1,"stability":{"browser_last_live_timestamp":"13397002540118569","stats_buildtime":"1751059322","stats_version":"138.0.7204.92-64-devel"}},"variations_crash_streak":1,"variations_google_groups":{"Default":[]},"variations_limited_entropy_synthetic_trial_seed_v2":"16","was":{"restarted":false}} \ No newline at end of file diff --git a/images/chromium-headful/image-chromium/user-data/ShaderCache/data_0 b/images/chromium-headful/image-chromium/user-data/ShaderCache/data_0 deleted file mode 100644 index d76fb77e..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/ShaderCache/data_0 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/ShaderCache/data_1 b/images/chromium-headful/image-chromium/user-data/ShaderCache/data_1 deleted file mode 100644 index 035d06d9..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/ShaderCache/data_1 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/ShaderCache/data_2 b/images/chromium-headful/image-chromium/user-data/ShaderCache/data_2 deleted file mode 100644 index c7e2eb9a..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/ShaderCache/data_2 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/ShaderCache/data_3 b/images/chromium-headful/image-chromium/user-data/ShaderCache/data_3 deleted file mode 100644 index 5eec9735..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/ShaderCache/data_3 and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/ShaderCache/index b/images/chromium-headful/image-chromium/user-data/ShaderCache/index deleted file mode 100644 index 3b956b07..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/ShaderCache/index and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/Variations b/images/chromium-headful/image-chromium/user-data/Variations deleted file mode 100644 index 5d75f573..00000000 --- a/images/chromium-headful/image-chromium/user-data/Variations +++ /dev/null @@ -1 +0,0 @@ -{"user_experience_metrics.stability.exited_cleanly":false,"variations_crash_streak":1} \ No newline at end of file diff --git a/images/chromium-headful/image-chromium/user-data/component_crx_cache/metadata.json b/images/chromium-headful/image-chromium/user-data/component_crx_cache/metadata.json deleted file mode 100644 index 4d156105..00000000 --- a/images/chromium-headful/image-chromium/user-data/component_crx_cache/metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"hashes":{}} \ No newline at end of file diff --git a/images/chromium-headful/image-chromium/user-data/extensions_crx_cache/metadata.json b/images/chromium-headful/image-chromium/user-data/extensions_crx_cache/metadata.json deleted file mode 100644 index 4d156105..00000000 --- a/images/chromium-headful/image-chromium/user-data/extensions_crx_cache/metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"hashes":{}} \ No newline at end of file diff --git a/images/chromium-headful/image-chromium/user-data/first_party_sets.db b/images/chromium-headful/image-chromium/user-data/first_party_sets.db deleted file mode 100644 index 39c45843..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/first_party_sets.db and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/first_party_sets.db-journal b/images/chromium-headful/image-chromium/user-data/first_party_sets.db-journal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/user-data/segmentation_platform/ukm_db b/images/chromium-headful/image-chromium/user-data/segmentation_platform/ukm_db deleted file mode 100644 index 6a11a4f0..00000000 Binary files a/images/chromium-headful/image-chromium/user-data/segmentation_platform/ukm_db and /dev/null differ diff --git a/images/chromium-headful/image-chromium/user-data/segmentation_platform/ukm_db-wal b/images/chromium-headful/image-chromium/user-data/segmentation_platform/ukm_db-wal deleted file mode 100644 index e69de29b..00000000 diff --git a/images/chromium-headful/image-chromium/x11vnc_startup.sh b/images/chromium-headful/image-chromium/x11vnc_startup.sh deleted file mode 100755 index 6f16940f..00000000 --- a/images/chromium-headful/image-chromium/x11vnc_startup.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -echo "starting vnc" - -(x11vnc -display $DISPLAY \ - -forever \ - -threads \ - -shared \ - -wait 50 \ - -rfbport 5900 \ - -nopw \ - 2>/tmp/x11vnc_stderr.log) & - -x11vnc_pid=$! - -# Wait for x11vnc to start -timeout=10 -while [ $timeout -gt 0 ]; do - if netstat -tuln | grep -q ":5900 "; then - break - fi - sleep 1 - ((timeout--)) -done - -if [ $timeout -eq 0 ]; then - echo "x11vnc failed to start, stderr output:" >&2 - cat /tmp/x11vnc_stderr.log >&2 - exit 1 -fi - -: > /tmp/x11vnc_stderr.log - -# Monitor x11vnc process in the background -( - while true; do - if ! kill -0 $x11vnc_pid 2>/dev/null; then - echo "x11vnc process crashed, restarting..." >&2 - if [ -f /tmp/x11vnc_stderr.log ]; then - echo "x11vnc stderr output:" >&2 - cat /tmp/x11vnc_stderr.log >&2 - rm /tmp/x11vnc_stderr.log - fi - exec "$0" - fi - sleep 5 - done -) & diff --git a/images/chromium-headful/start-chromium.sh b/images/chromium-headful/start-chromium.sh new file mode 100644 index 00000000..bc1ff17c --- /dev/null +++ b/images/chromium-headful/start-chromium.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -o pipefail -o errexit -o nounset + +# This script is launched by supervisord to start Chromium in the foreground. +# It mirrors the logic previously embedded in wrapper.sh. + +echo "Starting Chromium launcher" + +# Resolve internal port for the remote debugging interface +INTERNAL_PORT="${INTERNAL_PORT:-9223}" + +# Load additional Chromium flags from env and optional file +CHROMIUM_FLAGS="${CHROMIUM_FLAGS:-}" +if [[ -f /chromium/flags ]]; then + CHROMIUM_FLAGS="$CHROMIUM_FLAGS $(cat /chromium/flags)" +fi +echo "CHROMIUM_FLAGS: $CHROMIUM_FLAGS" + +# Always use display :1 and point DBus to the system bus socket +export DISPLAY=":1" +export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/dbus/system_bus_socket" + +RUN_AS_ROOT="${RUN_AS_ROOT:-false}" + +if [[ "$RUN_AS_ROOT" == "true" ]]; then + exec chromium \ + --remote-debugging-port="$INTERNAL_PORT" \ + --user-data-dir=/home/kernel/user-data \ + --password-store=basic \ + --no-first-run \ + ${CHROMIUM_FLAGS:-} +else + exec runuser -u kernel -- env \ + DISPLAY=":1" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=/run/dbus/system_bus_socket" \ + XDG_CONFIG_HOME=/home/kernel/.config \ + XDG_CACHE_HOME=/home/kernel/.cache \ + HOME=/home/kernel \ + chromium \ + --remote-debugging-port="$INTERNAL_PORT" \ + --user-data-dir=/home/kernel/user-data \ + --password-store=basic \ + --no-first-run \ + ${CHROMIUM_FLAGS:-} +fi diff --git a/images/chromium-headful/supervisor/services/chromium.conf b/images/chromium-headful/supervisor/services/chromium.conf new file mode 100644 index 00000000..53265b0f --- /dev/null +++ b/images/chromium-headful/supervisor/services/chromium.conf @@ -0,0 +1,7 @@ +[program:chromium] +command=/bin/bash -lc '/images/chromium-headful/start-chromium.sh' +autostart=false +autorestart=true +startsecs=5 +stdout_logfile=/var/log/supervisord/chromium +redirect_stderr=true diff --git a/images/chromium-headful/supervisor/services/dbus.conf b/images/chromium-headful/supervisor/services/dbus.conf new file mode 100644 index 00000000..7edc479c --- /dev/null +++ b/images/chromium-headful/supervisor/services/dbus.conf @@ -0,0 +1,7 @@ +[program:dbus] +command=/bin/bash -lc 'mkdir -p /run/dbus && dbus-uuidgen --ensure && dbus-daemon --system --address=unix:path=/run/dbus/system_bus_socket --nopidfile --nosyslog --nofork' +autostart=false +autorestart=true +startsecs=2 +stdout_logfile=/var/log/supervisord/dbus +redirect_stderr=true diff --git a/images/chromium-headful/supervisor/services/kernel-images-api.conf b/images/chromium-headful/supervisor/services/kernel-images-api.conf new file mode 100644 index 00000000..a04bfb35 --- /dev/null +++ b/images/chromium-headful/supervisor/services/kernel-images-api.conf @@ -0,0 +1,7 @@ +[program:kernel-images-api] +command=/bin/bash -lc 'mkdir -p "${KERNEL_IMAGES_API_OUTPUT_DIR:-/recordings}" && PORT="${KERNEL_IMAGES_API_PORT:-10001}" FRAME_RATE="${KERNEL_IMAGES_API_FRAME_RATE:-10}" DISPLAY_NUM="${KERNEL_IMAGES_API_DISPLAY_NUM:-${DISPLAY_NUM:-1}}" MAX_SIZE_MB="${KERNEL_IMAGES_API_MAX_SIZE_MB:-500}" OUTPUT_DIR="${KERNEL_IMAGES_API_OUTPUT_DIR:-/recordings}" exec /usr/local/bin/kernel-images-api' +autostart=false +autorestart=true +startsecs=2 +stdout_logfile=/var/log/supervisord/kernel-images-api +redirect_stderr=true diff --git a/images/chromium-headful/supervisor/services/mutter.conf b/images/chromium-headful/supervisor/services/mutter.conf new file mode 100644 index 00000000..5de00213 --- /dev/null +++ b/images/chromium-headful/supervisor/services/mutter.conf @@ -0,0 +1,7 @@ +[program:mutter] +command=/bin/bash -lc 'XDG_SESSION_TYPE=x11 mutter --replace --sm-disable' +autostart=false +autorestart=true +startsecs=2 +stdout_logfile=/var/log/supervisord/mutter +redirect_stderr=true diff --git a/images/chromium-headful/supervisor/services/neko.conf b/images/chromium-headful/supervisor/services/neko.conf new file mode 100644 index 00000000..c30c8b46 --- /dev/null +++ b/images/chromium-headful/supervisor/services/neko.conf @@ -0,0 +1,7 @@ +[program:neko] +command=/usr/bin/neko serve --server.static /var/www --server.bind 0.0.0.0:8080 +autostart=false +autorestart=true +startsecs=2 +stdout_logfile=/var/log/supervisord/neko +redirect_stderr=true diff --git a/images/chromium-headful/supervisor/services/xorg.conf b/images/chromium-headful/supervisor/services/xorg.conf new file mode 100644 index 00000000..72e515e5 --- /dev/null +++ b/images/chromium-headful/supervisor/services/xorg.conf @@ -0,0 +1,7 @@ +[program:xorg] +command=/usr/bin/Xorg :1 -config /etc/neko/xorg.conf -noreset -nolisten tcp +autostart=false +autorestart=true +startsecs=2 +stdout_logfile=/var/log/supervisord/xorg +redirect_stderr=true diff --git a/images/chromium-headful/supervisord.conf b/images/chromium-headful/supervisord.conf new file mode 100644 index 00000000..603e9ca4 --- /dev/null +++ b/images/chromium-headful/supervisord.conf @@ -0,0 +1,16 @@ +[unix_http_server] +file=/var/run/supervisor.sock + +[supervisord] +logfile=/var/log/supervisord.log +pidfile=/var/run/supervisord.pid +childlogdir=/var/log + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///var/run/supervisor.sock + +[include] +files = /etc/supervisor/conf.d/services/*.conf diff --git a/images/chromium-headful/wrapper.sh b/images/chromium-headful/wrapper.sh index 7da22c79..bd2a9dcf 100755 --- a/images/chromium-headful/wrapper.sh +++ b/images/chromium-headful/wrapper.sh @@ -21,7 +21,7 @@ scale_to_zero_write() { if [[ -e "$SCALE_TO_ZERO_FILE" ]]; then # Write the character, but do not fail the whole script if this errors out echo -n "$char" > "$SCALE_TO_ZERO_FILE" 2>/dev/null || \ - echo "Failed to write to scale-to-zero control file" >&2 + echo "[wrapper] Failed to write to scale-to-zero control file" >&2 fi } disable_scale_to_zero() { scale_to_zero_write "+"; } @@ -29,20 +29,10 @@ enable_scale_to_zero() { scale_to_zero_write "-"; } # Disable scale-to-zero for the duration of the script when not running under Docker if [[ -z "${WITHDOCKER:-}" ]]; then - echo "Disabling scale-to-zero" + echo "[wrapper] Disabling scale-to-zero" disable_scale_to_zero fi -export DISPLAY=:1 - -/usr/bin/Xorg :1 -config /etc/neko/xorg.conf -noreset -nolisten tcp & - -./mutter_startup.sh - -if [[ "${ENABLE_WEBRTC:-}" != "true" ]]; then - ./x11vnc_startup.sh -fi - # ----------------------------------------------------------------------------- # House-keeping for the unprivileged "kernel" user -------------------------------- # Some Chromium subsystems want to create files under $HOME (NSS cert DB, dconf @@ -51,121 +41,178 @@ fi # [ERROR:crypto/nss_util.cc:48] Failed to create /home/kernel/.pki/nssdb ... # dconf-CRITICAL **: unable to create directory '/home/kernel/.cache/dconf' # Pre-create them and hand ownership to the user so the messages disappear. +# When RUN_AS_ROOT is true, we skip ownership changes since we're running as root. -dirs=( - /home/kernel/user-data - /home/kernel/.config/chromium - /home/kernel/.pki/nssdb - /home/kernel/.cache/dconf - /tmp - /var/log -) - -for dir in "${dirs[@]}"; do - if [ ! -d "$dir" ]; then - mkdir -p "$dir" - fi -done +if [[ "${RUN_AS_ROOT:-}" != "true" ]]; then + dirs=( + /home/kernel/user-data + /home/kernel/.config/chromium + /home/kernel/.pki/nssdb + /home/kernel/.cache/dconf + /tmp + /var/log + /var/log/supervisord + ) + + for dir in "${dirs[@]}"; do + if [ ! -d "$dir" ]; then + mkdir -p "$dir" + fi + done -# Ensure correct ownership (ignore errors if already correct) -chown -R kernel:kernel /home/kernel/user-data /home/kernel/.config /home/kernel/.pki /home/kernel/.cache 2>/dev/null || true + # Ensure correct ownership (ignore errors if already correct) + chown -R kernel:kernel /home/kernel /home/kernel/user-data /home/kernel/.config /home/kernel/.pki /home/kernel/.cache 2>/dev/null || true +else + # When running as root, just create the necessary directories without ownership changes + dirs=( + /tmp + /var/log + /var/log/supervisord + /home/kernel + /home/kernel/user-data + ) + + for dir in "${dirs[@]}"; do + if [ ! -d "$dir" ]; then + mkdir -p "$dir" + fi + done +fi # ----------------------------------------------------------------------------- -# System-bus setup -------------------------------------------------------------- +# Dynamic log aggregation for /var/log/supervisord ----------------------------- # ----------------------------------------------------------------------------- -# Start a lightweight system D-Bus daemon if one is not already running. We -# will later use this very same bus as the *session* bus as well, avoiding the -# autolaunch fallback that produced many "Connection refused" errors. -# Start a lightweight system D-Bus daemon if one is not already running (Chromium complains otherwise) -if [ ! -S /run/dbus/system_bus_socket ]; then - echo "Starting system D-Bus daemon" - mkdir -p /run/dbus - # Ensure a machine-id exists (required by dbus-daemon) - dbus-uuidgen --ensure - # Launch dbus-daemon in the background and remember its PID for cleanup - dbus-daemon --system \ - --address=unix:path=/run/dbus/system_bus_socket \ - --nopidfile --nosyslog --nofork >/dev/null 2>&1 & - dbus_pid=$! -fi +# Tails any existing and future files under /var/log/supervisord, +# prefixing each line with the relative filepath, e.g. [chromium] ... +start_dynamic_log_aggregator() { + echo "[wrapper] Starting dynamic log aggregator for /var/log/supervisord" + ( + declare -A tailed_files=() + start_tail() { + local f="$1" + [[ -f "$f" ]] || return 0 + [[ -n "${tailed_files[$f]:-}" ]] && return 0 + local label="${f#/var/log/supervisord/}" + # Tie tails to this subshell lifetime so they exit when we stop it + tail --pid="$$" -n +1 -F "$f" 2>/dev/null | sed -u "s/^/[${label}] /" & + tailed_files[$f]=1 + } + # Periodically scan for new *.log files without extra dependencies + while true; do + while IFS= read -r -d '' f; do + start_tail "$f" + done < <(find /var/log/supervisord -type f -print0 2>/dev/null || true) + sleep 1 + done + ) & + tail_pids+=("$!") +} -# We will point DBUS_SESSION_BUS_ADDRESS at the system bus socket to suppress -# autolaunch attempts that failed and spammed logs. -export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/dbus/system_bus_socket" +# Start log aggregator early so we see supervisor and service logs as they appear +start_dynamic_log_aggregator -# Start Chromium with display :1 and remote debugging, loading our recorder extension. -# Use ncat to listen on 0.0.0.0:9222 since chromium does not let you listen on 0.0.0.0 anymore: https://github.com/pyppeteer/pyppeteer/pull/379#issuecomment-217029626 +export DISPLAY=:1 + +# Predefine ports and export for services +export INTERNAL_PORT="${INTERNAL_PORT:-9223}" +export CHROME_PORT="${CHROME_PORT:-9222}" + +# Track background tailing processes for cleanup +tail_pids=() + +# Cleanup handler (set early so we catch early failures) cleanup () { - echo "Cleaning up..." + echo "[wrapper] Cleaning up..." # Re-enable scale-to-zero if the script terminates early enable_scale_to_zero - kill -TERM $pid - kill -TERM $pid2 - # Kill the API server if it was started - if [[ -n "${pid3:-}" ]]; then - kill -TERM $pid3 || true - fi - if [ -n "${dbus_pid:-}" ]; then - kill -TERM $dbus_pid 2>/dev/null || true + supervisorctl -c /etc/supervisor/supervisord.conf stop chromium || true + supervisorctl -c /etc/supervisor/supervisord.conf stop kernel-images-api || true + supervisorctl -c /etc/supervisor/supervisord.conf stop dbus || true + # Stop log tailers + if [[ -n "${tail_pids[*]:-}" ]]; then + for tp in "${tail_pids[@]}"; do + kill -TERM "$tp" 2>/dev/null || true + done fi } trap cleanup TERM INT -pid= -pid2= -pid3= -INTERNAL_PORT=9223 -CHROME_PORT=9222 # External port mapped to host -echo "Starting Chromium on internal port $INTERNAL_PORT" - -# Load additional Chromium flags from /chromium/flags if present -CHROMIUM_FLAGS="${CHROMIUM_FLAGS:-}" -if [[ -f /chromium/flags ]]; then - CHROMIUM_FLAGS="$CHROMIUM_FLAGS $(cat /chromium/flags)" -fi -echo "CHROMIUM_FLAGS: $CHROMIUM_FLAGS" -RUN_AS_ROOT=${RUN_AS_ROOT:-false} -if [[ "$RUN_AS_ROOT" == "true" ]]; then - DISPLAY=:1 DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" chromium \ - --remote-debugging-port=$INTERNAL_PORT \ - ${CHROMIUM_FLAGS:-} >&2 & pid=$! -else - runuser -u kernel -- env \ - DISPLAY=:1 \ - DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" \ - XDG_CONFIG_HOME=/home/kernel/.config \ - XDG_CACHE_HOME=/home/kernel/.cache \ - HOME=/home/kernel \ - chromium \ - --remote-debugging-port=$INTERNAL_PORT \ - ${CHROMIUM_FLAGS:-} >&2 & pid=$! +# Start supervisord early so it can manage Xorg and Mutter +echo "[wrapper] Starting supervisord" +supervisord -c /etc/supervisor/supervisord.conf +echo "[wrapper] Waiting for supervisord socket..." +for i in {1..30}; do +if [ -S /var/run/supervisor.sock ]; then + break fi +sleep 0.2 +done -echo "Setting up ncat proxy on port $CHROME_PORT" -ncat \ - --sh-exec "ncat 0.0.0.0 $INTERNAL_PORT" \ - -l "$CHROME_PORT" \ - --keep-open & pid2=$! +echo "[wrapper] Starting Xorg via supervisord" +supervisorctl -c /etc/supervisor/supervisord.conf start xorg +echo "[wrapper] Waiting for Xorg to open display $DISPLAY..." +for i in {1..50}; do + if xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then + break + fi + sleep 0.2 +done + +echo "[wrapper] Starting Mutter via supervisord" +supervisorctl -c /etc/supervisor/supervisord.conf start mutter +echo "[wrapper] Waiting for Mutter to be ready..." +timeout=30 +while [ $timeout -gt 0 ]; do + if xdotool search --class "mutter" >/dev/null 2>&1; then + break + fi + sleep 1 + ((timeout--)) +done + +# ----------------------------------------------------------------------------- +# System-bus setup via supervisord -------------------------------------------- +# ----------------------------------------------------------------------------- +echo "[wrapper] Starting system D-Bus daemon via supervisord" +supervisorctl -c /etc/supervisor/supervisord.conf start dbus +echo "[wrapper] Waiting for D-Bus system bus socket..." +for i in {1..50}; do + if [ -S /run/dbus/system_bus_socket ]; then + break + fi + sleep 0.2 +done + +# We will point DBUS_SESSION_BUS_ADDRESS at the system bus socket to suppress +# autolaunch attempts that failed and spammed logs. +export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/dbus/system_bus_socket" + +# Start Chromium with display :1 and remote debugging, loading our recorder extension. +echo "[wrapper] Starting Chromium via supervisord on internal port $INTERNAL_PORT" +supervisorctl -c /etc/supervisor/supervisord.conf start chromium +echo "[wrapper] Waiting for Chromium remote debugging on 127.0.0.1:$INTERNAL_PORT..." +for i in {1..100}; do + if nc -z 127.0.0.1 "$INTERNAL_PORT" 2>/dev/null; then + break + fi + sleep 0.2 +done if [[ "${ENABLE_WEBRTC:-}" == "true" ]]; then # use webrtc - echo "✨ Starting neko (webrtc server)." - /usr/bin/neko serve --server.static /var/www --server.bind 0.0.0.0:8080 >&2 & + echo "[wrapper] ✨ Starting neko (webrtc server) via supervisord." + supervisorctl -c /etc/supervisor/supervisord.conf start neko # Wait for neko to be ready. - echo "Waiting for neko port 0.0.0.0:8080..." + echo "[wrapper] Waiting for neko port 0.0.0.0:8080..." while ! nc -z 127.0.0.1 8080 2>/dev/null; do sleep 0.5 done - echo "Port 8080 is open" -else - # use novnc - ./novnc_startup.sh - echo "✨ noVNC demo is ready to use!" + echo "[wrapper] Port 8080 is open" fi if [[ "${WITH_KERNEL_IMAGES_API:-}" == "true" ]]; then - echo "✨ Starting kernel-images API." + echo "[wrapper] ✨ Starting kernel-images API." API_PORT="${KERNEL_IMAGES_API_PORT:-10001}" API_FRAME_RATE="${KERNEL_IMAGES_API_FRAME_RATE:-10}" @@ -173,19 +220,13 @@ if [[ "${WITH_KERNEL_IMAGES_API:-}" == "true" ]]; then API_MAX_SIZE_MB="${KERNEL_IMAGES_API_MAX_SIZE_MB:-500}" API_OUTPUT_DIR="${KERNEL_IMAGES_API_OUTPUT_DIR:-/recordings}" - mkdir -p "$API_OUTPUT_DIR" - - PORT="$API_PORT" \ - FRAME_RATE="$API_FRAME_RATE" \ - DISPLAY_NUM="$API_DISPLAY_NUM" \ - MAX_SIZE_MB="$API_MAX_SIZE_MB" \ - OUTPUT_DIR="$API_OUTPUT_DIR" \ - /usr/local/bin/kernel-images-api & pid3=$! + # Start via supervisord (env overrides are read by the service's command) + supervisorctl -c /etc/supervisor/supervisord.conf start kernel-images-api # close the "--no-sandbox unsupported flag" warning when running as root # in the unikernel runtime we haven't been able to get chromium to launch as non-root without cryptic crashpad errors # and when running as root you must use the --no-sandbox flag, which generates a warning if [[ "${RUN_AS_ROOT:-}" == "true" ]]; then - echo "Running as root, attempting to dismiss the --no-sandbox unsupported flag warning" + echo "[wrapper] Running as root, attempting to dismiss the --no-sandbox unsupported flag warning" if read -r WIDTH HEIGHT <<< "$(xdotool getdisplaygeometry 2>/dev/null)"; then # Work out an x-coordinate slightly inside the right-hand edge of the OFFSET_X=$(( WIDTH - 30 )) @@ -193,22 +234,22 @@ if [[ "${WITH_KERNEL_IMAGES_API:-}" == "true" ]]; then OFFSET_X=0 fi - # Wait for kernel-images API port 10001 to be ready. - echo "Waiting for kernel-images API port 127.0.0.1:10001..." - while ! nc -z 127.0.0.1 10001 2>/dev/null; do + # Wait for kernel-images API port to be ready. + echo "[wrapper] Waiting for kernel-images API port 127.0.0.1:${API_PORT}..." + while ! nc -z 127.0.0.1 "${API_PORT}" 2>/dev/null; do sleep 0.5 done - echo "Port 10001 is open" + echo "[wrapper] Port ${API_PORT} is open" # Wait for Chromium window to open before dismissing the --no-sandbox warning. target='New Tab - Chromium' - echo "Waiting for Chromium window \"${target}\" to appear and become active..." + echo "[wrapper] Waiting for Chromium window \"${target}\" to appear and become active..." while :; do win_id=$(xwininfo -root -tree 2>/dev/null | awk -v t="$target" '$0 ~ t {print $1; exit}') if [[ -n $win_id ]]; then win_id=${win_id%:} if xdotool windowactivate --sync "$win_id"; then - echo "Focused window $win_id ($target) on $DISPLAY" + echo "[wrapper] Focused window $win_id ($target) on $DISPLAY" break fi fi @@ -220,17 +261,17 @@ if [[ "${WITH_KERNEL_IMAGES_API:-}" == "true" ]]; then sleep 5 # Attempt to click the warning's close button - echo "Clicking the warning's close button at x=$OFFSET_X y=115" + echo "[wrapper] Clicking the warning's close button at x=$OFFSET_X y=115" if curl -s -o /dev/null -X POST \ - http://localhost:10001/computer/click_mouse \ + http://localhost:${API_PORT}/computer/click_mouse \ -H "Content-Type: application/json" \ -d "{\"x\":${OFFSET_X},\"y\":115}"; then - echo "Successfully clicked the warning's close button" + echo "[wrapper] Successfully clicked the warning's close button" else - echo "Failed to click the warning's close button" >&2 + echo "[wrapper] Failed to click the warning's close button" >&2 fi else - echo "xdotool failed to obtain display geometry; skipping sandbox warning dismissal." >&2 + echo "[wrapper] xdotool failed to obtain display geometry; skipping sandbox warning dismissal." >&2 fi fi fi @@ -239,5 +280,5 @@ if [[ -z "${WITHDOCKER:-}" ]]; then enable_scale_to_zero fi -# Keep the container running -tail -f /dev/null +# Keep the container running while streaming logs +wait diff --git a/images/chromium-headless/build-docker.sh b/images/chromium-headless/build-docker.sh index 6e45ce1a..e616f528 100755 --- a/images/chromium-headless/build-docker.sh +++ b/images/chromium-headless/build-docker.sh @@ -8,8 +8,6 @@ source ../../shared/ensure-common-build-run-vars.sh chromium-headless source ../../shared/start-buildkit.sh -# Build the kernel-images API binary and place it into the Docker build context -source ../../shared/build-server.sh "$SCRIPT_DIR/image/bin" - -# Build (and optionally push) the Docker image -(cd image && docker build -t "$IMAGE" .) +# Build the Docker image using the repo root as build context +# so the Dockerfile's first stage can access the server sources +(cd "$SCRIPT_DIR/../.." && docker build -f images/chromium-headless/image/Dockerfile -t "$IMAGE" .) diff --git a/images/chromium-headless/build-unikernel.sh b/images/chromium-headless/build-unikernel.sh index a54c930c..cc12799d 100755 --- a/images/chromium-headless/build-unikernel.sh +++ b/images/chromium-headless/build-unikernel.sh @@ -19,10 +19,8 @@ cd image/ # Build the root file system source ../../../shared/start-buildkit.sh rm -rf ./.rootfs || true -# Build the API binary -source ../../../shared/build-server.sh "$(pwd)/bin" app_name=chromium-headless-build -docker build --platform linux/amd64 -t "$IMAGE" . +(cd "$SCRIPT_DIR/../.." && docker build --platform linux/amd64 -f images/chromium-headless/image/Dockerfile -t "$IMAGE" .) docker rm cnt-"$app_name" || true docker create --platform linux/amd64 --name cnt-"$app_name" "$IMAGE" /bin/sh docker cp cnt-"$app_name":/ ./.rootfs diff --git a/images/chromium-headless/image/Dockerfile b/images/chromium-headless/image/Dockerfile index 86a41f0c..ad23071d 100644 --- a/images/chromium-headless/image/Dockerfile +++ b/images/chromium-headless/image/Dockerfile @@ -1,3 +1,21 @@ +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 +ENV CGO_ENABLED=0 + +# Go module dependencies first for better layer caching +COPY server/go.mod ./ +COPY server/go.sum ./ +RUN go mod download + +# Copy the rest of the server source and build the binary +COPY server/ . +RUN GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \ + go build -ldflags="-s -w" -o /out/kernel-images-api ./cmd/api + FROM docker.io/ubuntu:22.04 RUN set -xe; \ @@ -29,10 +47,12 @@ RUN set -xe; \ dbus-x11 \ xvfb \ x11-utils \ - software-properties-common; + software-properties-common \ + supervisor; +# install chromium and sqlite3 for debugging the cookies file RUN add-apt-repository -y ppa:xtradeb/apps -RUN apt update -y && apt install -y chromium ncat +RUN apt update -y && apt install -y chromium sqlite3 # Install FFmpeg (latest static build) for the recording server RUN set -eux; \ @@ -50,11 +70,20 @@ RUN apt-get -yqq purge upower || true && rm -rf /var/lib/apt/lists/* # Create a non-root user with a home directory RUN useradd -m -s /bin/bash kernel -COPY ./xvfb_startup.sh /usr/bin/xvfb_startup.sh +# Xvfb helper and supervisor-managed start scripts +COPY images/chromium-headless/image/start-chromium.sh /images/chromium-headless/image/start-chromium.sh +COPY images/chromium-headless/image/start-xvfb.sh /images/chromium-headless/image/start-xvfb.sh +RUN chmod +x /images/chromium-headless/image/start-chromium.sh /images/chromium-headless/image/start-xvfb.sh # Wrapper script set environment -COPY ./wrapper.sh /usr/bin/wrapper.sh +COPY images/chromium-headless/image/wrapper.sh /usr/bin/wrapper.sh + +# Supervisord configuration +COPY images/chromium-headless/image/supervisord.conf /etc/supervisor/supervisord.conf +COPY images/chromium-headless/image/supervisor/services/ /etc/supervisor/conf.d/services/ -# Copy the kernel-images API binary built during the build process -COPY bin/kernel-images-api /usr/local/bin/kernel-images-api +# Copy the kernel-images API binary built in the builder stage +COPY --from=server-builder /out/kernel-images-api /usr/local/bin/kernel-images-api ENV WITH_KERNEL_IMAGES_API=false + +ENTRYPOINT [ "/usr/bin/wrapper.sh" ] diff --git a/images/chromium-headless/image/start-chromium.sh b/images/chromium-headless/image/start-chromium.sh new file mode 100644 index 00000000..81a277c1 --- /dev/null +++ b/images/chromium-headless/image/start-chromium.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +set -o pipefail -o errexit -o nounset + +echo "Starting Chromium launcher (headless)" + +# Resolve internal port for the remote debugging interface +INTERNAL_PORT="${INTERNAL_PORT:-9223}" + +# Load additional Chromium flags from env and optional file +CHROMIUM_FLAGS="${CHROMIUM_FLAGS:-}" +if [[ -f /chromium/flags ]]; then + CHROMIUM_FLAGS="$CHROMIUM_FLAGS $(cat /chromium/flags)" +fi +echo "CHROMIUM_FLAGS: $CHROMIUM_FLAGS" + +# Always use display :1 and point DBus to the system bus socket +export DISPLAY=":1" +export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/dbus/system_bus_socket" + +RUN_AS_ROOT="${RUN_AS_ROOT:-false}" + +if [[ "$RUN_AS_ROOT" == "true" ]]; then + exec chromium \ + --headless=new \ + --remote-debugging-port="$INTERNAL_PORT" \ + --remote-allow-origins=* \ + --user-data-dir=/home/kernel/user-data \ + --password-store=basic \ + --no-first-run \ + ${CHROMIUM_FLAGS:-} +else + exec runuser -u kernel -- env \ + DISPLAY=":1" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=/run/dbus/system_bus_socket" \ + XDG_CONFIG_HOME=/home/kernel/.config \ + XDG_CACHE_HOME=/home/kernel/.cache \ + HOME=/home/kernel \ + chromium \ + --headless=new \ + --remote-debugging-port="$INTERNAL_PORT" \ + --remote-allow-origins=* \ + --user-data-dir=/home/kernel/user-data \ + --password-store=basic \ + --no-first-run \ + ${CHROMIUM_FLAGS:-} +fi diff --git a/images/chromium-headless/image/start-xvfb.sh b/images/chromium-headless/image/start-xvfb.sh new file mode 100644 index 00000000..72c5a408 --- /dev/null +++ b/images/chromium-headless/image/start-xvfb.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -o pipefail -o errexit -o nounset + +DISPLAY="${DISPLAY:-:1}" +WIDTH="${WIDTH:-1024}" +HEIGHT="${HEIGHT:-768}" +DPI="${DPI:-96}" + +echo "Starting Xvfb on ${DISPLAY} with ${WIDTH}x${HEIGHT}x24, DPI ${DPI}" + +exec Xvfb "$DISPLAY" -ac -screen 0 "${WIDTH}x${HEIGHT}x24" -retro -dpi "$DPI" -nolisten tcp -nolisten unix diff --git a/images/chromium-headless/image/supervisor/services/chromium.conf b/images/chromium-headless/image/supervisor/services/chromium.conf new file mode 100644 index 00000000..d979df75 --- /dev/null +++ b/images/chromium-headless/image/supervisor/services/chromium.conf @@ -0,0 +1,7 @@ +[program:chromium] +command=/bin/bash -lc '/images/chromium-headless/image/start-chromium.sh' +autostart=false +autorestart=true +startsecs=5 +stdout_logfile=/var/log/supervisord/chromium +redirect_stderr=true diff --git a/images/chromium-headless/image/supervisor/services/dbus.conf b/images/chromium-headless/image/supervisor/services/dbus.conf new file mode 100644 index 00000000..7edc479c --- /dev/null +++ b/images/chromium-headless/image/supervisor/services/dbus.conf @@ -0,0 +1,7 @@ +[program:dbus] +command=/bin/bash -lc 'mkdir -p /run/dbus && dbus-uuidgen --ensure && dbus-daemon --system --address=unix:path=/run/dbus/system_bus_socket --nopidfile --nosyslog --nofork' +autostart=false +autorestart=true +startsecs=2 +stdout_logfile=/var/log/supervisord/dbus +redirect_stderr=true diff --git a/images/chromium-headless/image/supervisor/services/kernel-images-api.conf b/images/chromium-headless/image/supervisor/services/kernel-images-api.conf new file mode 100644 index 00000000..a04bfb35 --- /dev/null +++ b/images/chromium-headless/image/supervisor/services/kernel-images-api.conf @@ -0,0 +1,7 @@ +[program:kernel-images-api] +command=/bin/bash -lc 'mkdir -p "${KERNEL_IMAGES_API_OUTPUT_DIR:-/recordings}" && PORT="${KERNEL_IMAGES_API_PORT:-10001}" FRAME_RATE="${KERNEL_IMAGES_API_FRAME_RATE:-10}" DISPLAY_NUM="${KERNEL_IMAGES_API_DISPLAY_NUM:-${DISPLAY_NUM:-1}}" MAX_SIZE_MB="${KERNEL_IMAGES_API_MAX_SIZE_MB:-500}" OUTPUT_DIR="${KERNEL_IMAGES_API_OUTPUT_DIR:-/recordings}" exec /usr/local/bin/kernel-images-api' +autostart=false +autorestart=true +startsecs=2 +stdout_logfile=/var/log/supervisord/kernel-images-api +redirect_stderr=true diff --git a/images/chromium-headless/image/supervisor/services/xvfb.conf b/images/chromium-headless/image/supervisor/services/xvfb.conf new file mode 100644 index 00000000..5279bda4 --- /dev/null +++ b/images/chromium-headless/image/supervisor/services/xvfb.conf @@ -0,0 +1,7 @@ +[program:xvfb] +command=/bin/bash -lc '/images/chromium-headless/image/start-xvfb.sh' +autostart=false +autorestart=true +startsecs=2 +stdout_logfile=/var/log/supervisord/xvfb +redirect_stderr=true diff --git a/images/chromium-headless/image/supervisord.conf b/images/chromium-headless/image/supervisord.conf new file mode 100644 index 00000000..603e9ca4 --- /dev/null +++ b/images/chromium-headless/image/supervisord.conf @@ -0,0 +1,16 @@ +[unix_http_server] +file=/var/run/supervisor.sock + +[supervisord] +logfile=/var/log/supervisord.log +pidfile=/var/run/supervisord.pid +childlogdir=/var/log + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///var/run/supervisor.sock + +[include] +files = /etc/supervisor/conf.d/services/*.conf diff --git a/images/chromium-headless/image/wrapper.sh b/images/chromium-headless/image/wrapper.sh index 51baec59..a7159a61 100755 --- a/images/chromium-headless/image/wrapper.sh +++ b/images/chromium-headless/image/wrapper.sh @@ -9,6 +9,27 @@ if [ -z "${WITH_DOCKER:-}" ]; then mount -t tmpfs tmpfs /dev/shm fi +# We disable scale-to-zero for the lifetime of this script and restore +# the original setting on exit. +SCALE_TO_ZERO_FILE="/uk/libukp/scale_to_zero_disable" +scale_to_zero_write() { + local char="$1" + # Skip when not running inside Unikraft Cloud (control file absent) + if [[ -e "$SCALE_TO_ZERO_FILE" ]]; then + # Write the character, but do not fail the whole script if this errors out + echo -n "$char" > "$SCALE_TO_ZERO_FILE" 2>/dev/null || \ + echo "[wrapper] Failed to write to scale-to-zero control file" >&2 + fi +} +disable_scale_to_zero() { scale_to_zero_write "+"; } +enable_scale_to_zero() { scale_to_zero_write "-"; } + +# Disable scale-to-zero for the duration of the script when not running under Docker +if [[ -z "${WITHDOCKER:-}" ]]; then + echo "[wrapper] Disabling scale-to-zero" + disable_scale_to_zero +fi + # if CHROMIUM_FLAGS is not set, default to the flags used in playwright_stealth if [ -z "${CHROMIUM_FLAGS:-}" ]; then CHROMIUM_FLAGS="--accept-lang=en-US,en \ @@ -61,115 +82,153 @@ if [ -z "${CHROMIUM_FLAGS:-}" ]; then --use-gl=disabled \ --use-mock-keychain" fi +export CHROMIUM_FLAGS # ----------------------------------------------------------------------------- -# House-keeping for the unprivileged "kernel" user -------------------------------- -# Some Chromium subsystems want to create files under $HOME (NSS cert DB, dconf -# cache). If those directories are missing or owned by root Chromium emits -# noisy error messages such as: -# [ERROR:crypto/nss_util.cc:48] Failed to create /home/kernel/.pki/nssdb ... -# dconf-CRITICAL **: unable to create directory '/home/kernel/.cache/dconf' -# Pre-create them and hand ownership to the user so the messages disappear. - -for dir in /home/kernel/.pki/nssdb /home/kernel/.cache/dconf; do - if [ ! -d "$dir" ]; then - mkdir -p "$dir" - fi -done -# Ensure correct ownership (ignore errors if already correct) -chown -R kernel:kernel /home/kernel/.pki /home/kernel/.cache 2>/dev/null || true +# House-keeping for the unprivileged "kernel" user ---------------------------- +# When RUN_AS_ROOT is true, we skip ownership changes since we're running as root. +# ----------------------------------------------------------------------------- +if [[ "${RUN_AS_ROOT:-}" != "true" ]]; then + dirs=( + /home/kernel/.pki/nssdb + /home/kernel/.cache/dconf + /var/log + /var/log/supervisord + ) + for dir in "${dirs[@]}"; do + if [ ! -d "$dir" ]; then + mkdir -p "$dir" + fi + done + # Ensure correct ownership (ignore errors if already correct) + chown -R kernel:kernel /home/kernel/.pki /home/kernel/.cache 2>/dev/null || true +else + # When running as root, just create the necessary directories without ownership changes + dirs=( + /var/log + /var/log/supervisord + /home/kernel + /home/kernel/user-data + ) + for dir in "${dirs[@]}"; do + if [ ! -d "$dir" ]; then + mkdir -p "$dir" + fi + done +fi # ----------------------------------------------------------------------------- -# System-bus setup -------------------------------------------------------------- +# Dynamic log aggregation for /var/log/supervisord ----------------------------- # ----------------------------------------------------------------------------- -# Start a lightweight system D-Bus daemon if one is not already running. We -# will later use this very same bus as the *session* bus as well, avoiding the -# autolaunch fallback that produced many "Connection refused" errors. -# Start a lightweight system D-Bus daemon if one is not already running (Chromium complains otherwise) -if [ ! -S /run/dbus/system_bus_socket ]; then - echo "Starting system D-Bus daemon" - mkdir -p /run/dbus - # Ensure a machine-id exists (required by dbus-daemon) - dbus-uuidgen --ensure - # Launch dbus-daemon in the background and remember its PID for cleanup - dbus-daemon --system \ - --address=unix:path=/run/dbus/system_bus_socket \ - --nopidfile --nosyslog --nofork >/dev/null 2>&1 & - dbus_pid=$! -fi +# Tails any existing and future files under /var/log/supervisord, +# prefixing each line with the relative filepath, e.g. [chromium] ... +start_dynamic_log_aggregator() { + echo "[wrapper] Starting dynamic log aggregator for /var/log/supervisord" + ( + declare -A tailed_files=() + start_tail() { + local f="$1" + [[ -f "$f" ]] || return 0 + [[ -n "${tailed_files[$f]:-}" ]] && return 0 + local label="${f#/var/log/supervisord/}" + # Tie tails to this subshell lifetime so they exit when we stop it + tail --pid="$$" -n +1 -F "$f" 2>/dev/null | sed -u "s/^/[${label}] /" & + tailed_files[$f]=1 + } + # Periodically scan for new *.log files without extra dependencies + while true; do + while IFS= read -r -d '' f; do + start_tail "$f" + done < <(find /var/log/supervisord -type f -print0 2>/dev/null || true) + sleep 1 + done + ) & + tail_pids+=("$!") +} -# We will point DBUS_SESSION_BUS_ADDRESS at the system bus socket to suppress -# autolaunch attempts that failed and spammed logs. -export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/dbus/system_bus_socket" +# Track background tailing processes for cleanup +tail_pids=() -# Start Chromium in headless mode with remote debugging -# Use ncat to listen on 0.0.0.0:9222 since chromium does not let you listen on 0.0.0.0 anymore: https://github.com/pyppeteer/pyppeteer/pull/379#issuecomment-217029626 +# Start log aggregator early so we see supervisor and service logs as they appear +start_dynamic_log_aggregator + +# Export common env used by services +export DISPLAY=:1 +export HEIGHT=${HEIGHT:-768} +export WIDTH=${WIDTH:-1024} +export INTERNAL_PORT="${INTERNAL_PORT:-9223}" +export CHROME_PORT="${CHROME_PORT:-9222}" + +# Cleanup handler cleanup () { - echo "Cleaning up..." - kill -TERM $pid 2>/dev/null || true - kill -TERM $pid2 2>/dev/null || true - if [[ -n "${pid3:-}" ]]; then - kill -TERM $pid3 2>/dev/null || true - fi - if [ -n "${dbus_pid:-}" ]; then - kill -TERM $dbus_pid 2>/dev/null || true + echo "[wrapper] Cleaning up..." + # Re-enable scale-to-zero if the script terminates early + enable_scale_to_zero + supervisorctl -c /etc/supervisor/supervisord.conf stop chromium || true + supervisorctl -c /etc/supervisor/supervisord.conf stop xvfb || true + supervisorctl -c /etc/supervisor/supervisord.conf stop dbus || true + supervisorctl -c /etc/supervisor/supervisord.conf stop kernel-images-api || true + # Stop log tailers + if [[ -n "${tail_pids[*]:-}" ]]; then + for tp in "${tail_pids[@]}"; do + kill -TERM "$tp" 2>/dev/null || true + done fi } trap cleanup TERM INT -pid= -pid2= -pid3= -INTERNAL_PORT=9223 -CHROME_PORT=9222 # External port mapped to host -echo "Starting Chromium on internal port $INTERNAL_PORT" -export CHROMIUM_FLAGS -# Launch Chromium as the non-root user "kernel" -export HEIGHT=768 -export WIDTH=1024 -export DISPLAY=:1 -echo "Starting Xvfb" -/usr/bin/xvfb_startup.sh -runuser -u kernel -- env DISPLAY=:1 DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" chromium \ - --headless \ - --remote-debugging-port=$INTERNAL_PORT \ - --remote-allow-origins=* \ - ${CHROMIUM_FLAGS:-} 2>&1 \ - | grep -vE "org\.freedesktop\.UPower|Failed to connect to the bus|google_apis" >&2 & -pid=$! -echo "Setting up ncat proxy on port $CHROME_PORT" -ncat \ - --sh-exec "ncat 0.0.0.0 $INTERNAL_PORT" \ - -l "$CHROME_PORT" \ - --keep-open & pid2=$! - -# Optionally start the kernel-images API server file i/o + +echo "[wrapper] Starting supervisord" +supervisord -c /etc/supervisor/supervisord.conf +echo "[wrapper] Waiting for supervisord socket..." +for i in {1..30}; do + if [ -S /var/run/supervisor.sock ]; then + break + fi + sleep 0.2 +done + +echo "[wrapper] Starting system D-Bus daemon via supervisord" +supervisorctl -c /etc/supervisor/supervisord.conf start dbus +for i in {1..50}; do + if [ -S /run/dbus/system_bus_socket ]; then + break + fi + sleep 0.2 +done +export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/dbus/system_bus_socket" + +echo "[wrapper] Starting Xvfb via supervisord" +supervisorctl -c /etc/supervisor/supervisord.conf start xvfb +for i in {1..50}; do + if xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then + break + fi + sleep 0.2 +done + +echo "[wrapper] Starting Chromium via supervisord on internal port $INTERNAL_PORT" +supervisorctl -c /etc/supervisor/supervisord.conf start chromium +for i in {1..100}; do + if (echo >/dev/tcp/127.0.0.1/"$INTERNAL_PORT") >/dev/null 2>&1; then + break + fi + sleep 0.2 +done + if [[ "${WITH_KERNEL_IMAGES_API:-}" == "true" ]]; then - echo "✨ Starting kernel-images API." + echo "[wrapper] ✨ Starting kernel-images API via supervisord." + supervisorctl -c /etc/supervisor/supervisord.conf start kernel-images-api API_PORT="${KERNEL_IMAGES_API_PORT:-10001}" - API_FRAME_RATE="${KERNEL_IMAGES_API_FRAME_RATE:-10}" - API_DISPLAY_NUM="${KERNEL_IMAGES_API_DISPLAY_NUM:-${DISPLAY_NUM:-1}}" - API_MAX_SIZE_MB="${KERNEL_IMAGES_API_MAX_SIZE_MB:-500}" - API_OUTPUT_DIR="${KERNEL_IMAGES_API_OUTPUT_DIR:-/recordings}" - - mkdir -p "$API_OUTPUT_DIR" - - PORT="$API_PORT" \ - FRAME_RATE="$API_FRAME_RATE" \ - DISPLAY_NUM="$API_DISPLAY_NUM" \ - MAX_SIZE_MB="$API_MAX_SIZE_MB" \ - OUTPUT_DIR="$API_OUTPUT_DIR" \ - /usr/local/bin/kernel-images-api & pid3=$! + echo "[wrapper] Waiting for kernel-images API on 127.0.0.1:${API_PORT}..." + while ! (echo >/dev/tcp/127.0.0.1/"${API_PORT}") >/dev/null 2>&1; do + sleep 0.5 + done fi -# Wait for Chromium to exit; propagate its exit code -wait "$pid" -exit_code=$? -echo "Chromium exited with code $exit_code" -# Ensure ncat proxy is terminated -kill -TERM "$pid2" 2>/dev/null || true -# Ensure kernel-images API server is terminated -if [[ -n "${pid3:-}" ]]; then - kill -TERM "$pid3" 2>/dev/null || true +echo "[wrapper] startup complete!" +# Re-enable scale-to-zero once startup has completed (when not under Docker) +if [[ -z "${WITHDOCKER:-}" ]]; then + enable_scale_to_zero fi - -exit "$exit_code" +# Keep the container running while streaming logs +wait diff --git a/images/chromium-headless/image/xvfb_startup.sh b/images/chromium-headless/image/xvfb_startup.sh deleted file mode 100755 index dab83000..00000000 --- a/images/chromium-headless/image/xvfb_startup.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash -set -e # Exit on error - -DPI=96 -RES_AND_DEPTH=${WIDTH}x${HEIGHT}x24 - -# Default DISPLAY to :1 if not provided and extract the numeric part -: ${DISPLAY:=':1'} -DISPLAY_NUM=${DISPLAY#:} - -# Function to check if Xvfb is already running -check_xvfb_running() { - if [ -e /tmp/.X${DISPLAY_NUM}-lock ]; then - return 0 # Xvfb is already running - else - return 1 # Xvfb is not running - fi -} - -# Function to check if Xvfb is ready -wait_for_xvfb() { - local timeout=10 - local start_time=$(date +%s) - while ! xdpyinfo >/dev/null 2>&1; do - if [ $(($(date +%s) - start_time)) -gt $timeout ]; then - echo "Xvfb failed to start within $timeout seconds" >&2 - return 1 - fi - sleep 0.1 - done - return 0 -} - -# Check if Xvfb is already running -if check_xvfb_running; then - echo "Xvfb is already running on display ${DISPLAY}" - exit 0 -fi - -# Start Xvfb -Xvfb $DISPLAY -ac -screen 0 $RES_AND_DEPTH -retro -dpi $DPI -nolisten tcp -nolisten unix & -XVFB_PID=$! - -# Wait for Xvfb to start -if wait_for_xvfb; then - echo "Xvfb started successfully on display ${DISPLAY}" - echo "Xvfb PID: $XVFB_PID" -else - echo "Xvfb failed to start" - kill $XVFB_PID - exit 1 -fi diff --git a/images/chromium-headless/run-docker.sh b/images/chromium-headless/run-docker.sh index 1057b434..57fa74fe 100755 --- a/images/chromium-headless/run-docker.sh +++ b/images/chromium-headless/run-docker.sh @@ -24,5 +24,11 @@ if [[ "${WITH_KERNEL_IMAGES_API:-}" == "true" ]]; then RUN_ARGS+=( -v "$HOST_RECORDINGS_DIR:/recordings" ) fi +# If a positional argument is given, use it as the entrypoint +ENTRYPOINT_ARG=() +if [[ $# -ge 1 && -n "$1" ]]; then + ENTRYPOINT_ARG+=(--entrypoint "$1") +fi + docker rm -f "$NAME" 2>/dev/null || true -docker run -it --rm "${RUN_ARGS[@]}" "$IMAGE" /usr/bin/wrapper.sh +docker run -it --rm "${ENTRYPOINT_ARG[@]}" "${RUN_ARGS[@]}" "$IMAGE" diff --git a/server/Makefile b/server/Makefile index a45d3203..83e78c96 100644 --- a/server/Makefile +++ b/server/Makefile @@ -23,7 +23,7 @@ oapi-generate: $(OAPI_CODEGEN) openapi-down-convert --input openapi.yaml --output openapi-3.0.yaml $(OAPI_CODEGEN) -config ./oapi-codegen.yaml ./openapi-3.0.yaml @echo "Fixing oapi-codegen issue https://github.com/oapi-codegen/oapi-codegen/issues/1764..." - go run ./scripts/oapi/patch_sse_methods.go -file ./lib/oapi/oapi.go -expected-replacements 1 + go run ./scripts/oapi/patch_sse_methods.go -file ./lib/oapi/oapi.go -expected-replacements 3 go fmt ./lib/oapi/oapi.go go mod tidy diff --git a/server/cmd/api/api/api.go b/server/cmd/api/api/api.go index 21f5794c..36aa3acb 100644 --- a/server/cmd/api/api/api.go +++ b/server/cmd/api/api/api.go @@ -22,6 +22,10 @@ type ApiService struct { // Filesystem watch management watchMu sync.RWMutex watches map[string]*fsWatch + + // Process management + procMu sync.RWMutex + procs map[string]*processHandle } var _ oapi.StrictServerInterface = (*ApiService)(nil) @@ -39,6 +43,7 @@ func New(recordManager recorder.RecordManager, factory recorder.FFmpegRecorderFa factory: factory, defaultRecorderID: "default", watches: make(map[string]*fsWatch), + procs: make(map[string]*processHandle), }, nil } diff --git a/server/cmd/api/api/fs.go b/server/cmd/api/api/fs.go index 3ba23fc1..980236b0 100644 --- a/server/cmd/api/api/fs.go +++ b/server/cmd/api/api/fs.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "sync" @@ -18,6 +19,7 @@ import ( "github.com/nrednav/cuid2" "github.com/onkernel/kernel-images/server/lib/logger" oapi "github.com/onkernel/kernel-images/server/lib/oapi" + "github.com/onkernel/kernel-images/server/lib/ziputil" ) // fsWatch represents an in-memory directory watch. @@ -536,3 +538,315 @@ func (s *ApiService) StreamFsEvents(ctx context.Context, req oapi.StreamFsEvents headers := oapi.StreamFsEvents200ResponseHeaders{XSSEContentType: "application/json"} return oapi.StreamFsEvents200TexteventStreamResponse{Body: pr, Headers: headers, ContentLength: 0}, nil } + +// UploadZip handles a multipart upload of a zip archive and extracts it to dest_path. +func (s *ApiService) UploadZip(ctx context.Context, request oapi.UploadZipRequestObject) (oapi.UploadZipResponseObject, error) { + log := logger.FromContext(ctx) + + if request.Body == nil { + return oapi.UploadZip400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body required"}}, nil + } + + // Create temp file for uploaded zip + tmpZip, err := os.CreateTemp("", "upload-*.zip") + if err != nil { + log.Error("failed to create temporary file", "err", err) + return oapi.UploadZip500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "internal error"}}, nil + } + defer os.Remove(tmpZip.Name()) + defer tmpZip.Close() + + var destPath string + var fileReceived bool + + for { + part, err := request.Body.NextPart() + if err == io.EOF { + break + } + if err != nil { + log.Error("failed to read form part", "err", err) + return oapi.UploadZip400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "failed to read form part"}}, nil + } + + switch part.FormName() { + case "zip_file": + fileReceived = true + if _, err := io.Copy(tmpZip, part); err != nil { + log.Error("failed to read zip data", "err", err) + return oapi.UploadZip400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "failed to read zip file"}}, nil + } + case "dest_path": + data, err := io.ReadAll(part) + if err != nil { + log.Error("failed to read dest_path", "err", err) + return oapi.UploadZip400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "failed to read dest_path"}}, nil + } + destPath = strings.TrimSpace(string(data)) + if destPath == "" || !filepath.IsAbs(destPath) { + return oapi.UploadZip400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "dest_path must be an absolute path"}}, nil + } + default: + return oapi.UploadZip400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "invalid form field: " + part.FormName()}}, nil + } + } + + // Validate required parts + if !fileReceived || destPath == "" { + return oapi.UploadZip400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "zip_file and dest_path are required"}}, nil + } + + // Ensure destination directory exists + if err := os.MkdirAll(destPath, 0o755); err != nil { + log.Error("failed to create destination directory", "err", err, "path", destPath) + return oapi.UploadZip500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to create destination directory"}}, nil + } + + // Close temp writer prior to unzip + if err := tmpZip.Close(); err != nil { + log.Error("failed to finalize temporary zip", "err", err) + return oapi.UploadZip500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "internal error"}}, nil + } + + if err := ziputil.Unzip(tmpZip.Name(), destPath); err != nil { + // Map common user errors to 400, otherwise 500 + msg := err.Error() + if strings.Contains(msg, "failed to open zip file") || strings.Contains(msg, "illegal file path") { + return oapi.UploadZip400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "invalid zip file"}}, nil + } + log.Error("failed to extract zip", "err", err) + return oapi.UploadZip500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to extract zip"}}, nil + } + + return oapi.UploadZip201Response{}, nil +} + +// UploadFiles handles multipart form uploads for one or more files. It supports the following +// field name encodings: +// - files[].file and files[].dest_path +// - files[][file] and files[][dest_path] +// - files..file and files..dest_path +// +// Additionally, for single-file uploads it accepts: +// - file and dest_path +func (s *ApiService) UploadFiles(ctx context.Context, request oapi.UploadFilesRequestObject) (oapi.UploadFilesResponseObject, error) { + log := logger.FromContext(ctx) + + if request.Body == nil { + return oapi.UploadFiles400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body required"}}, nil + } + + // Track per-index pending uploads so order of parts does not matter. + type pendingUpload struct { + tempPath string + destPath string + fileReceived bool + } + + uploads := map[int]*pendingUpload{} + createdTemps := []string{} + defer func() { + for _, p := range createdTemps { + _ = os.Remove(p) + } + }() + + parseIndexAndField := func(name string) (int, string, bool) { + // single-file shorthand + if name == "file" || name == "dest_path" { + if name == "file" { + return 0, "file", true + } + return 0, "dest_path", true + } + if !strings.HasPrefix(name, "files") { + return 0, "", false + } + // forms like files[0].file or files[0][file] + if strings.HasPrefix(name, "files[") { + end := strings.Index(name, "]") + if end == -1 { + return 0, "", false + } + idxStr := name[len("files["):end] + rest := name[end+1:] + rest = strings.TrimPrefix(rest, ".") + var field string + if strings.HasPrefix(rest, "[") && strings.HasSuffix(rest, "]") { + field = rest[1 : len(rest)-1] + } else { + field = rest + } + idx := 0 + if v, err := strconv.Atoi(idxStr); err == nil && v >= 0 { + idx = v + } else { + return 0, "", false + } + return idx, field, true + } + // forms like files.0.file + if strings.HasPrefix(name, "files.") { + parts := strings.Split(name, ".") + if len(parts) != 3 { + return 0, "", false + } + idx := 0 + if v, err := strconv.Atoi(parts[1]); err == nil && v >= 0 { + idx = v + } else { + return 0, "", false + } + return idx, parts[2], true + } + return 0, "", false + } + + for { + part, err := request.Body.NextPart() + if err == io.EOF { + break + } + if err != nil { + log.Error("failed to read form part", "err", err) + return oapi.UploadFiles400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "failed to read form part"}}, nil + } + + name := part.FormName() + idx, field, ok := parseIndexAndField(name) + if !ok { + return oapi.UploadFiles400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "invalid form field: " + name}}, nil + } + + pu, exists := uploads[idx] + if !exists { + pu = &pendingUpload{} + uploads[idx] = pu + } + + switch field { + case "file": + // Create temp for the file contents + tmp, err := os.CreateTemp("", "upload-*") + if err != nil { + log.Error("failed to create temporary file", "err", err) + return oapi.UploadFiles500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "internal error"}}, nil + } + tmpPath := tmp.Name() + createdTemps = append(createdTemps, tmpPath) + if _, err := io.Copy(tmp, part); err != nil { + tmp.Close() + log.Error("failed to read upload data", "err", err) + return oapi.UploadFiles400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "failed to read upload data"}}, nil + } + if err := tmp.Close(); err != nil { + log.Error("failed to finalize temporary file", "err", err) + return oapi.UploadFiles500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "internal error"}}, nil + } + pu.tempPath = tmpPath + pu.fileReceived = true + + case "dest_path": + data, err := io.ReadAll(part) + if err != nil { + log.Error("failed to read dest_path", "err", err) + return oapi.UploadFiles400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "failed to read dest_path"}}, nil + } + dest := strings.TrimSpace(string(data)) + if dest == "" || !filepath.IsAbs(dest) { + return oapi.UploadFiles400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "dest_path must be an absolute path"}}, nil + } + pu.destPath = dest + + default: + return oapi.UploadFiles400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "invalid field: " + field}}, nil + } + } + + // Validate and materialize uploads + if len(uploads) == 0 { + return oapi.UploadFiles400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "no files provided"}}, nil + } + + for _, pu := range uploads { + if !pu.fileReceived || pu.destPath == "" { + return oapi.UploadFiles400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "each item must include file and dest_path"}}, nil + } + + // Ensure parent directory exists + if err := os.MkdirAll(filepath.Dir(pu.destPath), 0o755); err != nil { + log.Error("failed to create destination directories", "err", err, "path", pu.destPath) + return oapi.UploadFiles500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to create destination directories"}}, nil + } + + // Copy temp -> destination + src, err := os.Open(pu.tempPath) + if err != nil { + log.Error("failed to open temporary file", "err", err) + return oapi.UploadFiles500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "internal error"}}, nil + } + dst, err := os.OpenFile(pu.destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) + if err != nil { + src.Close() + if errors.Is(err, os.ErrNotExist) { + return oapi.UploadFiles404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "destination not found"}}, nil + } + log.Error("failed to open destination file", "err", err, "path", pu.destPath) + return oapi.UploadFiles500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to open destination file"}}, nil + } + if _, err := io.Copy(dst, src); err != nil { + src.Close() + dst.Close() + log.Error("failed to write destination file", "err", err) + return oapi.UploadFiles500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to write destination file"}}, nil + } + _ = src.Close() + if err := dst.Close(); err != nil { + log.Error("failed to close destination file", "err", err) + return oapi.UploadFiles500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "internal error"}}, nil + } + } + + return oapi.UploadFiles201Response{}, nil +} + +func (s *ApiService) DownloadDirZip(ctx context.Context, request oapi.DownloadDirZipRequestObject) (oapi.DownloadDirZipResponseObject, error) { + log := logger.FromContext(ctx) + path := request.Params.Path + if path == "" { + return oapi.DownloadDirZip400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "path cannot be empty"}}, nil + } + + info, err := os.Stat(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return oapi.DownloadDirZip404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "directory not found"}}, nil + } + log.Error("failed to stat path", "err", err, "path", path) + return oapi.DownloadDirZip500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to stat path"}}, nil + } + if !info.IsDir() { + return oapi.DownloadDirZip400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "path is not a directory"}}, nil + } + + // Build zip in-memory to provide a single streaming response + zipBytes, err := ziputil.ZipDir(path) + if err != nil { + // Add extra diagnostics for common failure causes + // Check if directory is readable and walkable + // We avoid heavy recursion here; just attempt to open directory and read one entry + var readErr error + f, oerr := os.Open(path) + if oerr != nil { + readErr = oerr + } else { + _, readErr = f.Readdir(1) + _ = f.Close() + } + log.Error("failed to create zip archive", "err", err, "path", path, "read_probe_err", readErr) + return oapi.DownloadDirZip500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to create zip"}}, nil + } + + body := io.NopCloser(bytes.NewReader(zipBytes)) + return oapi.DownloadDirZip200ApplicationzipResponse{Body: body, ContentLength: int64(len(zipBytes))}, nil +} diff --git a/server/cmd/api/api/fs_test.go b/server/cmd/api/api/fs_test.go index c4a8a70f..f8ddee44 100644 --- a/server/cmd/api/api/fs_test.go +++ b/server/cmd/api/api/fs_test.go @@ -1,15 +1,19 @@ package api import ( + "archive/zip" "bufio" + "bytes" "context" "io" + "mime/multipart" "os" "path/filepath" "strings" "testing" oapi "github.com/onkernel/kernel-images/server/lib/oapi" + "github.com/onkernel/kernel-images/server/lib/ziputil" ) // TestWriteReadFile verifies that files can be written and read back successfully. @@ -232,3 +236,330 @@ func TestFileDirOperations(t *testing.T) { t.Fatalf("unexpected DeleteDirectory resp: %T", resp) } } + +// helper to build multipart form for uploadFiles +func buildUploadMultipart(t *testing.T, parts map[string]string, files map[string]string) *multipart.Reader { + t.Helper() + pr, pw := io.Pipe() + mpw := multipart.NewWriter(pw) + + go func() { + // write fields + for name, val := range parts { + _ = mpw.WriteField(name, val) + } + // write files (string content) + for name, content := range files { + fw, _ := mpw.CreateFormField(name) + _, _ = io.Copy(fw, strings.NewReader(content)) + } + mpw.Close() + pw.Close() + }() + + return multipart.NewReader(pr, mpw.Boundary()) +} + +// helper to build multipart for UploadZip with binary zip bytes +func buildUploadZipMultipart(t *testing.T, destPath string, zipBytes []byte) *multipart.Reader { + t.Helper() + pr, pw := io.Pipe() + mpw := multipart.NewWriter(pw) + + go func() { + // dest_path field + if destPath != "" { + _ = mpw.WriteField("dest_path", destPath) + } + // binary zip part + if zipBytes != nil { + // Use form field named zip_file; file vs field does not matter for our handler + fw, _ := mpw.CreateFormFile("zip_file", "upload.zip") + _, _ = fw.Write(zipBytes) + } + mpw.Close() + pw.Close() + }() + + return multipart.NewReader(pr, mpw.Boundary()) +} + +func TestUploadFilesSingle(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := &ApiService{} + + tmp := t.TempDir() + dest := filepath.Join(tmp, "single.txt") + + // single-file shorthand: file + dest_path + reader := buildUploadMultipart(t, + map[string]string{"dest_path": dest}, + map[string]string{"file": "hello"}, + ) + + resp, err := svc.UploadFiles(ctx, oapi.UploadFilesRequestObject{Body: reader}) + if err != nil { + t.Fatalf("UploadFiles error: %v", err) + } + if _, ok := resp.(oapi.UploadFiles201Response); !ok { + t.Fatalf("unexpected response type: %T", resp) + } + data, err := os.ReadFile(dest) + if err != nil || string(data) != "hello" { + t.Fatalf("uploaded file mismatch: %v %q", err, string(data)) + } +} + +func TestUploadFilesMultipleAndOutOfOrder(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := &ApiService{} + + tmp := t.TempDir() + d1 := filepath.Join(tmp, "a.txt") + d2 := filepath.Join(tmp, "b.txt") + + // Use indexed fields with mixed ordering and bracket/dot styles + parts := map[string]string{ + "files[1][dest_path]": d2, + "files.0.dest_path": d1, + } + files := map[string]string{ + "files[1][file]": "two", + "files.0.file": "one", + } + reader := buildUploadMultipart(t, parts, files) + + resp, err := svc.UploadFiles(ctx, oapi.UploadFilesRequestObject{Body: reader}) + if err != nil { + t.Fatalf("UploadFiles error: %v", err) + } + if _, ok := resp.(oapi.UploadFiles201Response); !ok { + t.Fatalf("unexpected response type: %T", resp) + } + + if b, _ := os.ReadFile(d1); string(b) != "one" { + t.Fatalf("d1 mismatch: %q", string(b)) + } + if b, _ := os.ReadFile(d2); string(b) != "two" { + t.Fatalf("d2 mismatch: %q", string(b)) + } +} + +func TestUploadZipSuccess(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := &ApiService{} + + // Create a source directory with content + srcDir := t.TempDir() + nested := filepath.Join(srcDir, "dir", "sub") + if err := os.MkdirAll(nested, 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + filePath := filepath.Join(nested, "a.txt") + if err := os.WriteFile(filePath, []byte("hello-zip"), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + + // Zip the directory + zipBytes, err := ziputil.ZipDir(srcDir) + if err != nil { + t.Fatalf("ZipDir error: %v", err) + } + + // Destination directory for extraction + destDir := t.TempDir() + + reader := buildUploadZipMultipart(t, destDir, zipBytes) + resp, err := svc.UploadZip(ctx, oapi.UploadZipRequestObject{Body: reader}) + if err != nil { + t.Fatalf("UploadZip error: %v", err) + } + if _, ok := resp.(oapi.UploadZip201Response); !ok { + t.Fatalf("unexpected UploadZip resp type: %T", resp) + } + + // Verify extracted content exists + extracted := filepath.Join(destDir, "dir", "sub", "a.txt") + data, err := os.ReadFile(extracted) + if err != nil { + t.Fatalf("read extracted: %v", err) + } + if string(data) != "hello-zip" { + t.Fatalf("extracted content mismatch: %q", string(data)) + } +} + +func TestUploadZipTraversalBlocked(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := &ApiService{} + + // Build a malicious zip with a path traversal entry + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + fh, _ := zw.Create("../evil.txt") + _, _ = fh.Write([]byte("pwned")) + _ = zw.Close() + + destDir := t.TempDir() + reader := buildUploadZipMultipart(t, destDir, buf.Bytes()) + resp, err := svc.UploadZip(ctx, oapi.UploadZipRequestObject{Body: reader}) + if err != nil { + t.Fatalf("UploadZip error: %v", err) + } + if _, ok := resp.(oapi.UploadZip400JSONResponse); !ok { + t.Fatalf("expected 400 for traversal, got %T", resp) + } + if _, err := os.Stat(filepath.Join(destDir, "evil.txt")); err == nil { + t.Fatalf("traversal file unexpectedly created") + } +} + +func TestUploadZipValidationErrors(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := &ApiService{} + + // Missing dest_path + reader1 := func() *multipart.Reader { + pr, pw := io.Pipe() + mpw := multipart.NewWriter(pw) + go func() { + fw, _ := mpw.CreateFormFile("zip_file", "z.zip") + _, _ = fw.Write([]byte("not-a-zip")) + mpw.Close() + pw.Close() + }() + return multipart.NewReader(pr, mpw.Boundary()) + }() + resp1, err := svc.UploadZip(ctx, oapi.UploadZipRequestObject{Body: reader1}) + if err != nil { + t.Fatalf("UploadZip error: %v", err) + } + if _, ok := resp1.(oapi.UploadZip400JSONResponse); !ok { + t.Fatalf("expected 400 for missing dest_path, got %T", resp1) + } + + // Missing zip_file + destDir := t.TempDir() + reader2 := func() *multipart.Reader { + pr, pw := io.Pipe() + mpw := multipart.NewWriter(pw) + go func() { + _ = mpw.WriteField("dest_path", destDir) + mpw.Close() + pw.Close() + }() + return multipart.NewReader(pr, mpw.Boundary()) + }() + resp2, err := svc.UploadZip(ctx, oapi.UploadZipRequestObject{Body: reader2}) + if err != nil { + t.Fatalf("UploadZip error: %v", err) + } + if _, ok := resp2.(oapi.UploadZip400JSONResponse); !ok { + t.Fatalf("expected 400 for missing zip_file, got %T", resp2) + } +} + +func TestDownloadDirZipSuccess(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := &ApiService{} + + // Prepare a directory with nested content + root := t.TempDir() + nested := filepath.Join(root, "dir", "sub") + if err := os.MkdirAll(nested, 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + f1 := filepath.Join(root, "top.txt") + f2 := filepath.Join(nested, "a.txt") + if err := os.WriteFile(f1, []byte("top"), 0o644); err != nil { + t.Fatalf("write top: %v", err) + } + if err := os.WriteFile(f2, []byte("hello"), 0o644); err != nil { + t.Fatalf("write nested: %v", err) + } + + resp, err := svc.DownloadDirZip(ctx, oapi.DownloadDirZipRequestObject{Params: oapi.DownloadDirZipParams{Path: root}}) + if err != nil { + t.Fatalf("DownloadDirZip error: %v", err) + } + r200, ok := resp.(oapi.DownloadDirZip200ApplicationzipResponse) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + data, err := io.ReadAll(r200.Body) + if err != nil { + t.Fatalf("read body: %v", err) + } + + zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) + if err != nil { + t.Fatalf("open zip: %v", err) + } + + // Expect both files present with paths relative to root + want := map[string]string{ + "top.txt": "top", + "dir/sub/a.txt": "hello", + } + found := map[string]bool{} + for _, f := range zr.File { + if f.FileInfo().IsDir() { + continue + } + if _, ok := want[f.Name]; ok { + rc, err := f.Open() + if err != nil { + t.Fatalf("open zip entry: %v", err) + } + b, _ := io.ReadAll(rc) + rc.Close() + if string(b) != want[f.Name] { + t.Fatalf("content mismatch for %s: %q", f.Name, string(b)) + } + found[f.Name] = true + } + } + for k := range want { + if !found[k] { + t.Fatalf("missing zip entry: %s", k) + } + } +} + +func TestDownloadDirZipErrors(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := &ApiService{} + + // Empty path + if resp, err := svc.DownloadDirZip(ctx, oapi.DownloadDirZipRequestObject{Params: oapi.DownloadDirZipParams{Path: ""}}); err != nil { + t.Fatalf("unexpected error: %v", err) + } else if _, ok := resp.(oapi.DownloadDirZip400JSONResponse); !ok { + t.Fatalf("expected 400 for empty path, got %T", resp) + } + + // Non-existent path + missing := filepath.Join(t.TempDir(), "nope") + if resp, err := svc.DownloadDirZip(ctx, oapi.DownloadDirZipRequestObject{Params: oapi.DownloadDirZipParams{Path: missing}}); err != nil { + t.Fatalf("unexpected error: %v", err) + } else if _, ok := resp.(oapi.DownloadDirZip404JSONResponse); !ok { + t.Fatalf("expected 404 for missing dir, got %T", resp) + } + + // Path is a file, not a directory + tmp := filepath.Join(t.TempDir(), "file.txt") + if err := os.WriteFile(tmp, []byte("x"), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + if resp, err := svc.DownloadDirZip(ctx, oapi.DownloadDirZipRequestObject{Params: oapi.DownloadDirZipParams{Path: tmp}}); err != nil { + t.Fatalf("unexpected error: %v", err) + } else if _, ok := resp.(oapi.DownloadDirZip400JSONResponse); !ok { + t.Fatalf("expected 400 for file path, got %T", resp) + } +} diff --git a/server/cmd/api/api/logs.go b/server/cmd/api/api/logs.go new file mode 100644 index 00000000..42cdf085 --- /dev/null +++ b/server/cmd/api/api/logs.go @@ -0,0 +1,129 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "io" + "os" + "path/filepath" + "time" + + oapi "github.com/onkernel/kernel-images/server/lib/oapi" +) + +// LogsStream implements Server-Sent Events log streaming. +// (GET /logs/stream) +func (s *ApiService) LogsStream(ctx context.Context, request oapi.LogsStreamRequestObject) (oapi.LogsStreamResponseObject, error) { + // Only path-based streaming is implemented. Supervisor streaming can be added later. + src := string(request.Params.Source) + follow := true + if request.Params.Follow != nil { + follow = *request.Params.Follow + } + + var logPath string + if src == "path" { + if request.Params.Path != nil { + logPath = *request.Params.Path + } + } + + pr, pw := io.Pipe() + + go func() { + defer pw.Close() + + if logPath == "" || !filepath.IsAbs(logPath) { + _ = writeSSELogEvent(pw, oapi.LogEvent{Timestamp: time.Now(), Message: "logs source not available"}) + return + } + + f, err := os.Open(logPath) + if err != nil { + _ = writeSSELogEvent(pw, oapi.LogEvent{Timestamp: time.Now(), Message: "failed to open log path"}) + return + } + defer f.Close() + + var offset int64 = 0 + if follow { + if st, err := f.Stat(); err == nil { + offset = st.Size() + } + } + + var remainder []byte + buf := make([]byte, 16*1024) + + ticker := time.NewTicker(50 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + st, err := f.Stat() + if err != nil { + return + } + size := st.Size() + if size < offset { + offset = size + } + if size == offset { + continue + } + toRead := size - offset + for toRead > 0 { + if int64(len(buf)) > toRead { + buf = buf[:toRead] + } + n, err := f.ReadAt(buf, offset) + if n > 0 { + offset += int64(n) + toRead -= int64(n) + chunk := append(remainder, buf[:n]...) + for { + if i := bytes.IndexByte(chunk, '\n'); i >= 0 { + line := chunk[:i] + if len(line) > 0 { + _ = writeSSELogEvent(pw, oapi.LogEvent{Timestamp: time.Now(), Message: string(line)}) + } + chunk = chunk[i+1:] + continue + } + break + } + remainder = chunk + } + if err != nil { + break + } + } + } + } + }() + + headers := oapi.LogsStream200ResponseHeaders{XSSEContentType: "application/json"} + return oapi.LogsStream200TexteventStreamResponse{Body: pr, Headers: headers, ContentLength: 0}, nil +} + +func writeSSELogEvent(w io.Writer, ev oapi.LogEvent) error { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(ev); err != nil { + return err + } + line := bytes.TrimRight(buf.Bytes(), "\n") + if _, err := w.Write([]byte("data: ")); err != nil { + return err + } + if _, err := w.Write(line); err != nil { + return err + } + if _, err := w.Write([]byte("\n\n")); err != nil { + return err + } + return nil +} diff --git a/server/cmd/api/api/logs_test.go b/server/cmd/api/api/logs_test.go new file mode 100644 index 00000000..525b868d --- /dev/null +++ b/server/cmd/api/api/logs_test.go @@ -0,0 +1,65 @@ +package api + +import ( + "bufio" + "context" + "os" + "path/filepath" + "strings" + "testing" + "time" + + oapi "github.com/onkernel/kernel-images/server/lib/oapi" +) + +func TestLogsStream_PathFollow(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + svc := &ApiService{} + + // create temp log file + tmpDir := t.TempDir() + logPath := filepath.Join(tmpDir, "app.log") + if err := os.WriteFile(logPath, []byte("initial\n"), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + + // start streaming with follow=true from end of file + follow := true + resp, err := svc.LogsStream(ctx, oapi.LogsStreamRequestObject{Params: oapi.LogsStreamParams{Source: "path", Follow: &follow, Path: &logPath}}) + if err != nil { + t.Fatalf("LogsStream error: %v", err) + } + r200, ok := resp.(oapi.LogsStream200TexteventStreamResponse) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + + // write another line after starting + go func() { + time.Sleep(100 * time.Millisecond) + f, _ := os.OpenFile(logPath, os.O_APPEND|os.O_WRONLY, 0o644) + defer f.Close() + _, _ = f.WriteString("hello world\n") + }() + + reader := bufio.NewReader(r200.Body) + deadline := time.Now().Add(2 * time.Second) + for { + if time.Now().After(deadline) { + t.Fatalf("timed out waiting for SSE line") + } + line, err := reader.ReadString('\n') + if err != nil { + continue + } + if strings.HasPrefix(line, "data: ") { + payload := strings.TrimPrefix(line, "data: ") + if strings.Contains(payload, "hello world") { + break + } + } + } +} diff --git a/server/cmd/api/api/process.go b/server/cmd/api/api/process.go new file mode 100644 index 00000000..742728db --- /dev/null +++ b/server/cmd/api/api/process.go @@ -0,0 +1,444 @@ +package api + +import ( + "bufio" + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "io" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/google/uuid" + openapi_types "github.com/oapi-codegen/runtime/types" + "github.com/onkernel/kernel-images/server/lib/logger" + oapi "github.com/onkernel/kernel-images/server/lib/oapi" +) + +type processHandle struct { + id openapi_types.UUID + pid int + cmd *exec.Cmd + started time.Time + exitCode *int + stdin io.WriteCloser + stdout io.ReadCloser + stderr io.ReadCloser + outCh chan oapi.ProcessStreamEvent + doneCh chan struct{} + mu sync.RWMutex +} + +func (h *processHandle) state() string { + h.mu.RLock() + defer h.mu.RUnlock() + if h.exitCode != nil { + return "exited" + } + return "running" +} + +func (h *processHandle) setExited(code int) { + h.mu.Lock() + if h.exitCode == nil { + h.exitCode = &code + } + h.mu.Unlock() +} + +func buildCmd(body *oapi.ProcessExecRequest) (*exec.Cmd, error) { + if body == nil || body.Command == "" { + return nil, errors.New("command required") + } + var args []string + if body.Args != nil { + args = append(args, (*body.Args)...) + } + cmd := exec.Command(body.Command, args...) + if body.Cwd != nil && *body.Cwd != "" { + cmd.Dir = *body.Cwd + // Ensure absolute if provided + if !filepath.IsAbs(cmd.Dir) { + // make relative to current working directory + wd, _ := os.Getwd() + cmd.Dir = filepath.Join(wd, cmd.Dir) + } + } + // Build environment + envMap := map[string]string{} + for _, kv := range os.Environ() { + if i := strings.IndexByte(kv, '='); i > 0 { + envMap[kv[:i]] = kv[i+1:] + } + } + if body.Env != nil { + for k, v := range *body.Env { + envMap[k] = v + } + } + env := make([]string, 0, len(envMap)) + for k, v := range envMap { + env = append(env, k+"="+v) + } + cmd.Env = env + return cmd, nil +} + +// Execute a command synchronously (optional streaming) +// (POST /process/exec) +func (s *ApiService) ProcessExec(ctx context.Context, request oapi.ProcessExecRequestObject) (oapi.ProcessExecResponseObject, error) { + log := logger.FromContext(ctx) + if request.Body == nil { + return oapi.ProcessExec400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body required"}}, nil + } + // Streaming over this endpoint is not supported by the current API definition + if request.Body.Stream != nil && *request.Body.Stream { + return oapi.ProcessExec400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "streaming not supported for /process/exec"}}, nil + } + + cmd, err := buildCmd((*oapi.ProcessExecRequest)(request.Body)) + if err != nil { + return oapi.ProcessExec400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil + } + + var stdoutBuf, stderrBuf bytes.Buffer + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf + + // Handle timeout if provided + start := time.Now() + var cancel context.CancelFunc + if request.Body.TimeoutSec != nil && *request.Body.TimeoutSec > 0 { + ctx, cancel = context.WithTimeout(ctx, time.Duration(*request.Body.TimeoutSec)*time.Second) + defer cancel() + } + if err := cmd.Start(); err != nil { + log.Error("failed to start process", "err", err) + return oapi.ProcessExec500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start process"}}, nil + } + + done := make(chan error, 1) + go func() { done <- cmd.Wait() }() + select { + case <-ctx.Done(): + _ = cmd.Process.Kill() + <-done // ensure wait returns + return oapi.ProcessExec500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "process timed out"}}, nil + case err := <-done: + // proceed + _ = err + } + durationMs := int(time.Since(start) / time.Millisecond) + exitCode := 0 + if cmd.ProcessState != nil { + if status, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok { + exitCode = status.ExitStatus() + } + } + + resp := oapi.ProcessExec200JSONResponse{ + ExitCode: &exitCode, + StdoutB64: ptrOf(base64.StdEncoding.EncodeToString(stdoutBuf.Bytes())), + StderrB64: ptrOf(base64.StdEncoding.EncodeToString(stderrBuf.Bytes())), + DurationMs: &durationMs, + } + return resp, nil +} + +// Execute a command asynchronously +// (POST /process/spawn) +func (s *ApiService) ProcessSpawn(ctx context.Context, request oapi.ProcessSpawnRequestObject) (oapi.ProcessSpawnResponseObject, error) { + log := logger.FromContext(ctx) + if request.Body == nil { + return oapi.ProcessSpawn400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body required"}}, nil + } + // Build from ProcessExecRequest shape + execReq := oapi.ProcessExecRequest{ + Command: request.Body.Command, + Args: request.Body.Args, + Cwd: request.Body.Cwd, + Env: request.Body.Env, + AsUser: request.Body.AsUser, + AsRoot: request.Body.AsRoot, + TimeoutSec: request.Body.TimeoutSec, + } + cmd, err := buildCmd(&execReq) + if err != nil { + return oapi.ProcessSpawn400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to open stdout"}}, nil + } + stderr, err := cmd.StderrPipe() + if err != nil { + return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to open stderr"}}, nil + } + stdin, err := cmd.StdinPipe() + if err != nil { + return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to open stdin"}}, nil + } + if err := cmd.Start(); err != nil { + log.Error("failed to start process", "err", err) + return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start process"}}, nil + } + + id := openapi_types.UUID(uuid.New()) + h := &processHandle{ + id: id, + pid: cmd.Process.Pid, + cmd: cmd, + started: time.Now(), + stdin: stdin, + stdout: stdout, + stderr: stderr, + outCh: make(chan oapi.ProcessStreamEvent, 256), + doneCh: make(chan struct{}), + } + + // Store handle + s.procMu.Lock() + if s.procs == nil { + s.procs = make(map[string]*processHandle) + } + s.procs[id.String()] = h + s.procMu.Unlock() + + // Reader goroutines + go func() { + reader := bufio.NewReader(stdout) + buf := make([]byte, 4096) + for { + n, err := reader.Read(buf) + if n > 0 { + data := base64.StdEncoding.EncodeToString(buf[:n]) + stream := oapi.ProcessStreamEventStream("stdout") + h.outCh <- oapi.ProcessStreamEvent{Stream: &stream, DataB64: &data} + } + if err != nil { + break + } + } + }() + + go func() { + reader := bufio.NewReader(stderr) + buf := make([]byte, 4096) + for { + n, err := reader.Read(buf) + if n > 0 { + data := base64.StdEncoding.EncodeToString(buf[:n]) + stream := oapi.ProcessStreamEventStream("stderr") + h.outCh <- oapi.ProcessStreamEvent{Stream: &stream, DataB64: &data} + } + if err != nil { + break + } + } + }() + + // Waiter goroutine + go func() { + err := cmd.Wait() + code := 0 + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + code = status.ExitStatus() + } + } + } else if cmd.ProcessState != nil { + if status, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok { + code = status.ExitStatus() + } + } + h.setExited(code) + // Send exit event + evt := oapi.ProcessStreamEventEvent("exit") + h.outCh <- oapi.ProcessStreamEvent{Event: &evt, ExitCode: &code} + close(h.doneCh) + // Retain the handle for a short period so clients can observe the + // final "exited" status via ProcessStatus before it disappears. + // This avoids races where the process exits immediately after spawn + // and status polling returns 404. + retention := 10 * time.Second + go func(procID string) { + time.Sleep(retention) + s.procMu.Lock() + delete(s.procs, procID) + s.procMu.Unlock() + }(id.String()) + }() + + startedAt := h.started + pid := h.pid + return oapi.ProcessSpawn200JSONResponse{ + ProcessId: &id, + Pid: &pid, + StartedAt: &startedAt, + }, nil +} + +// Send signal to process +// (POST /process/{process_id}/kill) +func (s *ApiService) ProcessKill(ctx context.Context, request oapi.ProcessKillRequestObject) (oapi.ProcessKillResponseObject, error) { + log := logger.FromContext(ctx) + id := request.ProcessId.String() + s.procMu.RLock() + h, ok := s.procs[id] + s.procMu.RUnlock() + if !ok { + return oapi.ProcessKill404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "process not found"}}, nil + } + if request.Body == nil { + return oapi.ProcessKill400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body required"}}, nil + } + // Map signal + var sig syscall.Signal + switch request.Body.Signal { + case "TERM": + sig = syscall.SIGTERM + case "KILL": + sig = syscall.SIGKILL + case "INT": + sig = syscall.SIGINT + case "HUP": + sig = syscall.SIGHUP + default: + return oapi.ProcessKill400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "invalid signal"}}, nil + } + if h.cmd.Process == nil { + return oapi.ProcessKill404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "process not running"}}, nil + } + if err := h.cmd.Process.Signal(sig); err != nil { + log.Error("failed to signal process", "err", err) + return oapi.ProcessKill500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to signal process"}}, nil + } + return oapi.ProcessKill200JSONResponse(oapi.OkResponse{Ok: true}), nil +} + +// Get process status +// (GET /process/{process_id}/status) +func (s *ApiService) ProcessStatus(ctx context.Context, request oapi.ProcessStatusRequestObject) (oapi.ProcessStatusResponseObject, error) { + id := request.ProcessId.String() + s.procMu.RLock() + h, ok := s.procs[id] + s.procMu.RUnlock() + if !ok { + return oapi.ProcessStatus404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "process not found"}}, nil + } + stateStr := h.state() + state := oapi.ProcessStatusState(stateStr) + var exitCode *int + h.mu.RLock() + if h.exitCode != nil { + v := *h.exitCode + exitCode = &v + } + pid := h.pid + h.mu.RUnlock() + // Best-effort memory stats via /proc + var memBytes int + if stateStr == "running" && pid > 0 { + if b, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/status"); err == nil { + // Parse VmRSS: 123 kB + for _, line := range strings.Split(string(b), "\n") { + if strings.HasPrefix(line, "VmRSS:") { + fields := strings.Fields(line) + if len(fields) >= 2 { + if v, err := strconv.Atoi(fields[1]); err == nil { + // fields[2] is likely kB + memBytes = v * 1024 + } + } + break + } + } + } + } + cpuPct := float32(0) + resp := oapi.ProcessStatus200JSONResponse{State: &state, ExitCode: exitCode, CpuPct: &cpuPct} + if memBytes > 0 { + resp.MemBytes = ptrOf(memBytes) + } + return resp, nil +} + +// Write to process stdin +// (POST /process/{process_id}/stdin) +func (s *ApiService) ProcessStdin(ctx context.Context, request oapi.ProcessStdinRequestObject) (oapi.ProcessStdinResponseObject, error) { + id := request.ProcessId.String() + s.procMu.RLock() + h, ok := s.procs[id] + s.procMu.RUnlock() + if !ok { + return oapi.ProcessStdin404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "process not found"}}, nil + } + if request.Body == nil { + return oapi.ProcessStdin400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body required"}}, nil + } + data, err := base64.StdEncoding.DecodeString(request.Body.DataB64) + if err != nil { + return oapi.ProcessStdin400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "invalid base64"}}, nil + } + n, err := h.stdin.Write(data) + if err != nil { + return oapi.ProcessStdin500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to write to stdin"}}, nil + } + return oapi.ProcessStdin200JSONResponse{WrittenBytes: ptrOf(n)}, nil +} + +// Stream process stdout/stderr (SSE) +// (GET /process/{process_id}/stdout/stream) +func (s *ApiService) ProcessStdoutStream(ctx context.Context, request oapi.ProcessStdoutStreamRequestObject) (oapi.ProcessStdoutStreamResponseObject, error) { + log := logger.FromContext(ctx) + id := request.ProcessId.String() + s.procMu.RLock() + h, ok := s.procs[id] + s.procMu.RUnlock() + if !ok { + return oapi.ProcessStdoutStream404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "process not found"}}, nil + } + + pr, pw := io.Pipe() + go func() { + defer pw.Close() + for { + select { + case evt := <-h.outCh: + // Write SSE: data: \n\n + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(evt); err != nil { + log.Error("failed to marshal event", "err", err) + return + } + line := bytes.TrimRight(buf.Bytes(), "\n") + if _, err := pw.Write([]byte("data: ")); err != nil { + return + } + if _, err := pw.Write(line); err != nil { + return + } + if _, err := pw.Write([]byte("\n\n")); err != nil { + return + } + case <-h.doneCh: + return + } + } + }() + + headers := oapi.ProcessStdoutStream200ResponseHeaders{XSSEContentType: "application/json"} + return oapi.ProcessStdoutStream200TexteventStreamResponse{Body: pr, Headers: headers, ContentLength: 0}, nil +} + +func ptrOf[T any](v T) *T { return &v } diff --git a/server/cmd/api/api/process_test.go b/server/cmd/api/api/process_test.go new file mode 100644 index 00000000..e1cb330f --- /dev/null +++ b/server/cmd/api/api/process_test.go @@ -0,0 +1,237 @@ +package api + +import ( + "bufio" + "context" + "encoding/base64" + "encoding/json" + "io" + "strings" + "testing" + "time" + + "github.com/google/uuid" + openapi_types "github.com/oapi-codegen/runtime/types" + oapi "github.com/onkernel/kernel-images/server/lib/oapi" +) + +func TestProcessExec(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := &ApiService{procs: make(map[string]*processHandle)} + + cmd := "sh" + args := []string{"-c", "echo -n out; echo -n err 1>&2; exit 3"} + body := &oapi.ProcessExecRequest{Command: cmd, Args: &args} + resp, err := svc.ProcessExec(ctx, oapi.ProcessExecRequestObject{Body: body}) + if err != nil { + t.Fatalf("ProcessExec error: %v", err) + } + r200, ok := resp.(oapi.ProcessExec200JSONResponse) + if !ok { + t.Fatalf("unexpected resp type: %T", resp) + } + if r200.ExitCode == nil || *r200.ExitCode != 3 { + t.Fatalf("exit code mismatch: %+v", r200.ExitCode) + } + if r200.StdoutB64 == nil || r200.StderrB64 == nil { + t.Fatalf("missing stdout/stderr in response") + } + out, _ := base64.StdEncoding.DecodeString(*r200.StdoutB64) + errB, _ := base64.StdEncoding.DecodeString(*r200.StderrB64) + if string(out) != "out" || string(errB) != "err" { + t.Fatalf("stdout/stderr mismatch: %q %q", string(out), string(errB)) + } +} + +func TestProcessSpawnStatusAndStream(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := &ApiService{procs: make(map[string]*processHandle)} + + // Spawn a short-lived process that emits stdout and stderr then exits + cmd := "sh" + args := []string{"-c", "printf ABC; sleep 0.05; printf DEF 1>&2; sleep 0.05; exit 0"} + body := &oapi.ProcessSpawnRequest{Command: cmd, Args: &args} + spawnResp, err := svc.ProcessSpawn(ctx, oapi.ProcessSpawnRequestObject{Body: body}) + if err != nil { + t.Fatalf("ProcessSpawn error: %v", err) + } + s200, ok := spawnResp.(oapi.ProcessSpawn200JSONResponse) + if !ok || s200.ProcessId == nil || s200.Pid == nil { + t.Fatalf("unexpected spawn resp: %+v", spawnResp) + } + + // Status should be running initially (may race to exited; tolerate both by not asserting) + statusResp, err := svc.ProcessStatus(ctx, oapi.ProcessStatusRequestObject{ProcessId: *s200.ProcessId}) + if err != nil { + t.Fatalf("ProcessStatus error: %v", err) + } + if _, ok := statusResp.(oapi.ProcessStatus200JSONResponse); !ok { + t.Fatalf("unexpected status resp: %T", statusResp) + } + + // Start stream reader and collect at least two data events and one exit event + streamResp, err := svc.ProcessStdoutStream(ctx, oapi.ProcessStdoutStreamRequestObject{ProcessId: *s200.ProcessId}) + if err != nil { + t.Fatalf("StdoutStream error: %v", err) + } + st200, ok := streamResp.(oapi.ProcessStdoutStream200TexteventStreamResponse) + if !ok { + t.Fatalf("unexpected stream resp: %T", streamResp) + } + + reader := bufio.NewReader(st200.Body) + var gotStdout, gotStderr, gotExit bool + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) && !(gotStdout && gotStderr && gotExit) { + line, err := reader.ReadString('\n') + if err != nil { + if err == io.EOF { + break + } + t.Fatalf("read SSE line: %v", err) + } + if !strings.HasPrefix(line, "data: ") { + continue + } + payload := strings.TrimSpace(strings.TrimPrefix(line, "data: ")) + var evt oapi.ProcessStreamEvent + if err := json.Unmarshal([]byte(payload), &evt); err != nil { + t.Fatalf("unmarshal event: %v", err) + } + if evt.Stream != nil && *evt.Stream == "stdout" && evt.DataB64 != nil { + b, _ := base64.StdEncoding.DecodeString(*evt.DataB64) + if strings.Contains(string(b), "ABC") { + gotStdout = true + } + } + if evt.Stream != nil && *evt.Stream == "stderr" && evt.DataB64 != nil { + b, _ := base64.StdEncoding.DecodeString(*evt.DataB64) + if strings.Contains(string(b), "DEF") { + gotStderr = true + } + } + if evt.Event != nil && *evt.Event == "exit" { + gotExit = true + } + // consume blank line + _, _ = reader.ReadString('\n') + } + if !(gotStdout && gotStderr && gotExit) { + t.Fatalf("missing events: stdout=%v stderr=%v exit=%v", gotStdout, gotStderr, gotExit) + } +} + +func TestProcessStdinAndExit(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := &ApiService{procs: make(map[string]*processHandle)} + + // Spawn a process that reads exactly 3 bytes then exits + cmd := "sh" + args := []string{"-c", "dd of=/dev/null bs=1 count=3 status=none"} + body := &oapi.ProcessSpawnRequest{Command: cmd, Args: &args} + spawnResp, err := svc.ProcessSpawn(ctx, oapi.ProcessSpawnRequestObject{Body: body}) + if err != nil { + t.Fatalf("ProcessSpawn error: %v", err) + } + s200, ok := spawnResp.(oapi.ProcessSpawn200JSONResponse) + if !ok || s200.ProcessId == nil { + t.Fatalf("unexpected spawn resp: %T", spawnResp) + } + + // Write 3 bytes + data := base64.StdEncoding.EncodeToString([]byte("xyz")) + stdinResp, err := svc.ProcessStdin(ctx, oapi.ProcessStdinRequestObject{ProcessId: *s200.ProcessId, Body: &oapi.ProcessStdinRequest{DataB64: data}}) + if err != nil { + t.Fatalf("ProcessStdin error: %v", err) + } + st200, ok := stdinResp.(oapi.ProcessStdin200JSONResponse) + if !ok || st200.WrittenBytes == nil || *st200.WrittenBytes != 3 { + t.Fatalf("unexpected stdin resp: %+v", stdinResp) + } + + // Wait for exit via status polling + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + resp, err := svc.ProcessStatus(ctx, oapi.ProcessStatusRequestObject{ProcessId: *s200.ProcessId}) + if err != nil { + t.Fatalf("ProcessStatus error: %v", err) + } + sr, ok := resp.(oapi.ProcessStatus200JSONResponse) + if !ok { + t.Fatalf("unexpected status resp: %T", resp) + } + if sr.State != nil && *sr.State == "exited" { + return + } + time.Sleep(50 * time.Millisecond) + } + t.Fatalf("process did not exit in time") +} + +func TestProcessKill(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := &ApiService{procs: make(map[string]*processHandle)} + + cmd := "sh" + args := []string{"-c", "sleep 5"} + body := &oapi.ProcessSpawnRequest{Command: cmd, Args: &args} + spawnResp, err := svc.ProcessSpawn(ctx, oapi.ProcessSpawnRequestObject{Body: body}) + if err != nil { + t.Fatalf("ProcessSpawn error: %v", err) + } + s200, ok := spawnResp.(oapi.ProcessSpawn200JSONResponse) + if !ok || s200.ProcessId == nil { + t.Fatalf("unexpected spawn resp: %T", spawnResp) + } + + // Send KILL + killBody := &oapi.ProcessKillRequest{Signal: "KILL"} + killResp, err := svc.ProcessKill(ctx, oapi.ProcessKillRequestObject{ProcessId: *s200.ProcessId, Body: killBody}) + if err != nil { + t.Fatalf("ProcessKill error: %v", err) + } + if _, ok := killResp.(oapi.ProcessKill200JSONResponse); !ok { + t.Fatalf("unexpected kill resp: %T", killResp) + } + + // Verify exited + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + resp, err := svc.ProcessStatus(ctx, oapi.ProcessStatusRequestObject{ProcessId: *s200.ProcessId}) + if err != nil { + t.Fatalf("ProcessStatus error: %v", err) + } + sr, ok := resp.(oapi.ProcessStatus200JSONResponse) + if !ok { + t.Fatalf("unexpected status resp: %T", resp) + } + if sr.State != nil && *sr.State == "exited" { + return + } + time.Sleep(50 * time.Millisecond) + } + t.Fatalf("process not killed in time") +} + +func TestProcessNotFoundRoutes(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := &ApiService{procs: make(map[string]*processHandle)} + + // random id that will not exist + id := openapi_types.UUID(uuid.New()) + if resp, _ := svc.ProcessStatus(ctx, oapi.ProcessStatusRequestObject{ProcessId: id}); resp == nil { + t.Fatalf("expected a response") + } else if _, ok := resp.(oapi.ProcessStatus404JSONResponse); !ok { + t.Fatalf("expected 404, got %T", resp) + } + if resp, _ := svc.ProcessStdoutStream(ctx, oapi.ProcessStdoutStreamRequestObject{ProcessId: id}); resp == nil { + t.Fatalf("expected a response") + } else if _, ok := resp.(oapi.ProcessStdoutStream404JSONResponse); !ok { + t.Fatalf("expected 404, got %T", resp) + } +} diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index 50a56a14..57806754 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -2,9 +2,11 @@ package main import ( "context" + "encoding/json" "fmt" "log/slog" "net/http" + "net/url" "os" "os/exec" "os/signal" @@ -19,6 +21,7 @@ import ( serverpkg "github.com/onkernel/kernel-images/server" "github.com/onkernel/kernel-images/server/cmd/api/api" "github.com/onkernel/kernel-images/server/cmd/config" + "github.com/onkernel/kernel-images/server/lib/devtoolsproxy" "github.com/onkernel/kernel-images/server/lib/logger" oapi "github.com/onkernel/kernel-images/server/lib/oapi" "github.com/onkernel/kernel-images/server/lib/recorder" @@ -100,6 +103,52 @@ func main() { Handler: r, } + // DevTools WebSocket proxy setup: tail Chromium supervisord log and start WS server on :9222 only after upstream is found + const chromiumLogPath = "/var/log/supervisord/chromium" + upstreamMgr := devtoolsproxy.NewUpstreamManager(chromiumLogPath, slogger) + upstreamMgr.Start(ctx) + + // wait up to 10 seconds for initial upstream; exit nonzero if not found + if _, err := upstreamMgr.WaitForInitial(10 * time.Second); err != nil { + slogger.Error("devtools upstream not available", "err", err) + os.Exit(1) + } + + rDevtools := chi.NewRouter() + rDevtools.Use( + chiMiddleware.Logger, + chiMiddleware.Recoverer, + func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctxWithLogger := logger.AddToContext(r.Context(), slogger) + next.ServeHTTP(w, r.WithContext(ctxWithLogger)) + }) + }, + ) + // Expose a minimal /json/version endpoint so clients that attempt to + // resolve a browser websocket URL via HTTP can succeed. We map the + // upstream path onto this proxy's host:port so clients connect back to us. + rDevtools.Get("/json/version", func(w http.ResponseWriter, r *http.Request) { + current := upstreamMgr.Current() + if current == "" { + http.Error(w, "upstream not ready", http.StatusServiceUnavailable) + return + } + proxyWSURL := (&url.URL{Scheme: "ws", Host: r.Host}).String() + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{ + "webSocketDebuggerUrl": proxyWSURL, + }) + }) + rDevtools.Get("/*", func(w http.ResponseWriter, r *http.Request) { + devtoolsproxy.WebSocketProxyHandler(upstreamMgr, slogger).ServeHTTP(w, r) + }) + + srvDevtools := &http.Server{ + Addr: "0.0.0.0:9222", + Handler: rDevtools, + } + go func() { slogger.Info("http server starting", "addr", srv.Addr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { @@ -108,6 +157,14 @@ func main() { } }() + go func() { + slogger.Info("devtools websocket proxy starting", "addr", srvDevtools.Addr) + if err := srvDevtools.ListenAndServe(); err != nil && err != http.ErrServerClosed { + slogger.Error("devtools websocket proxy failed", "err", err) + stop() + } + }() + // graceful shutdown <-ctx.Done() slogger.Info("shutdown signal received") @@ -122,6 +179,10 @@ func main() { g.Go(func() error { return apiService.Shutdown(shutdownCtx) }) + g.Go(func() error { + upstreamMgr.Stop() + return srvDevtools.Shutdown(shutdownCtx) + }) if err := g.Wait(); err != nil { slogger.Error("server failed to shutdown", "err", err) diff --git a/server/e2e/cookie_debug.sh b/server/e2e/cookie_debug.sh new file mode 100755 index 00000000..585f3325 --- /dev/null +++ b/server/e2e/cookie_debug.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +echo "=== Container User Info ===" +echo "Current user: $(whoami)" +echo "User ID: $(id)" +echo "Home directory: $HOME" +echo "" + +echo "=== Chromium Process Info ===" +echo "Chromium processes:" +ps aux | grep -i chromium | grep -v grep || echo "No chromium processes found" +echo "" + +echo "=== User Data Directory Info ===" +USER_DATA_DIR="/home/kernel/user-data" +if [ -d "$USER_DATA_DIR" ]; then + echo "User data directory exists: $USER_DATA_DIR" + echo "Owner: $(ls -ld "$USER_DATA_DIR")" + echo "Contents:" + ls -la "$USER_DATA_DIR" || echo "Failed to list contents" + + COOKIES_DIR="$USER_DATA_DIR/Default" + if [ -d "$COOKIES_DIR" ]; then + echo "" + echo "Default directory exists: $COOKIES_DIR" + echo "Owner: $(ls -ld "$COOKIES_DIR")" + echo "Contents:" + ls -la "$COOKIES_DIR" || echo "Failed to list contents" + + COOKIES_FILE="$COOKIES_DIR/Cookies" + if [ -f "$COOKIES_FILE" ]; then + echo "" + echo "Cookies file exists: $COOKIES_FILE" + echo "Owner: $(ls -ld "$COOKIES_FILE")" + echo "Modification time: $(stat -c %y "$COOKIES_FILE")" + echo "File size: $(stat -c %s "$COOKIES_FILE") bytes" + if command -v file >/dev/null 2>&1; then + echo "File type: $(file "$COOKIES_FILE")" + else + echo "File type: unknown (file command not found)" + fi + + # Try to inspect the cookies database for specific cookies + echo "" + echo "=== Cookie Database Inspection ===" + if command -v sqlite3 >/dev/null 2>&1; then + echo "Checking for e2e_cookie:" + sqlite3 "$COOKIES_FILE" "SELECT name, value, host_key, path, expires_utc FROM cookies WHERE name='e2e_cookie';" 2>/dev/null || echo "Failed to query e2e_cookie" + echo "Checking for .x.com cookie:" + sqlite3 "$COOKIES_FILE" "SELECT name, value, host_key, path, expires_utc FROM cookies WHERE host_key='.x.com';" 2>/dev/null || echo "Failed to query .x.com cookie" + echo "All cookies in database:" + sqlite3 "$COOKIES_FILE" "SELECT name, value, host_key, path FROM cookies LIMIT 10;" 2>/dev/null || echo "Failed to query cookies" + else + echo "sqlite3 not available, cannot inspect cookie database" + fi + else + echo "" + echo "Cookies file does not exist: $COOKIES_FILE" + fi + else + echo "" + echo "Default directory does not exist: $COOKIES_DIR" + fi +else + echo "User data directory does not exist: $USER_DATA_DIR" +fi + +echo "" +echo "=== Supervisor Status ===" +supervisorctl -c /etc/supervisor/supervisord.conf status chromium || true + +echo "" +echo "=== Environment Variables ===" +echo "RUN_AS_ROOT: ${RUN_AS_ROOT:-not set}" +echo "USER: ${USER:-not set}" +echo "HOME: ${HOME:-not set}" +echo "PWD: $PWD" diff --git a/server/e2e/e2e_chromium_test.go b/server/e2e/e2e_chromium_test.go new file mode 100644 index 00000000..20e161bd --- /dev/null +++ b/server/e2e/e2e_chromium_test.go @@ -0,0 +1,1166 @@ +package e2e + +import ( + "archive/zip" + "bufio" + "bytes" + "context" + "crypto/rand" + "database/sql" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "log/slog" + "text/template" + + _ "github.com/glebarez/sqlite" + logctx "github.com/onkernel/kernel-images/server/lib/logger" + instanceoapi "github.com/onkernel/kernel-images/server/lib/oapi" +) + +const ( + defaultHeadfulImage = "onkernel/chromium-headful-test:latest" + defaultHeadlessImage = "onkernel/chromium-headless-test:latest" + containerName = "server-e2e-test" + // With host networking, the API listens on 10001 directly on the host + apiBaseURL = "http://127.0.0.1:10001" +) + +var ( + headfulImage = defaultHeadfulImage + headlessImage = defaultHeadlessImage +) + +func init() { + // Prefer fully-specified images if provided + if v := os.Getenv("E2E_CHROMIUM_HEADFUL_IMAGE"); v != "" { + headfulImage = v + } + if v := os.Getenv("E2E_CHROMIUM_HEADLESS_IMAGE"); v != "" { + headlessImage = v + } + // Otherwise, if a tag/sha is provided, use the CI-built images + tag := os.Getenv("E2E_IMAGE_TAG") + if tag == "" { + tag = os.Getenv("E2E_IMAGE_SHA") + } + if tag != "" { + if os.Getenv("E2E_CHROMIUM_HEADFUL_IMAGE") == "" { + headfulImage = "onkernel/chromium-headful:" + tag + } + if os.Getenv("E2E_CHROMIUM_HEADLESS_IMAGE") == "" { + headlessImage = "onkernel/chromium-headless:" + tag + } + } +} + +// getPlaywrightPath returns the path to the playwright script +func getPlaywrightPath() string { + return "./playwright" +} + +// ensurePlaywrightDeps ensures playwright dependencies are installed +func ensurePlaywrightDeps(t *testing.T) { + t.Helper() + nodeModulesPath := getPlaywrightPath() + "/node_modules" + if _, err := os.Stat(nodeModulesPath); os.IsNotExist(err) { + t.Log("Installing playwright dependencies...") + cmd := exec.Command("pnpm", "install") + cmd.Dir = getPlaywrightPath() + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Failed to install playwright dependencies: %v\nOutput: %s", err, string(output)) + } + t.Log("Playwright dependencies installed successfully") + } +} + +func TestChromiumHeadfulUserDataSaving(t *testing.T) { + ensurePlaywrightDeps(t) + runChromiumUserDataSavingFlow(t, headfulImage, containerName, true) +} + +func TestChromiumHeadlessPersistence(t *testing.T) { + ensurePlaywrightDeps(t) + runChromiumUserDataSavingFlow(t, headlessImage, containerName, true) +} + +func runChromiumUserDataSavingFlow(t *testing.T, image, containerName string, runAsRoot bool) { + t.Helper() + logger := slog.New(slog.NewTextHandler(t.Output(), &slog.HandlerOptions{ + Level: slog.LevelInfo, + AddSource: false, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + ts := a.Value.Time() + return slog.String(slog.TimeKey, ts.UTC().Format(time.RFC3339)) + } + return a + }, + })) + baseCtx := logctx.AddToContext(context.Background(), logger) + logger.Info("[e2e]", "action", "starting chromium cookie saving flow", "image", image, "name", containerName, "runAsRoot", runAsRoot) + if _, err := exec.LookPath("docker"); err != nil { + t.Fatalf("[precheck] docker not available: %v", err) + } + + // Setup Phase + layout := createTestTempLayout(t) + logger.Info("[setup]", "base", layout.BaseDir, "zips", layout.ZipsDir, "restored", layout.RestoreDir) + logger.Info("[setup]", "action", "ensuring container is not running", "container", containerName) + if err := stopContainer(baseCtx, containerName); err != nil { + t.Fatalf("[setup] failed to stop container %s: %v", containerName, err) + } + env := map[string]string{ + "WITH_KERNEL_IMAGES_API": "true", + "WITH_DOCKER": "true", + "RUN_AS_ROOT": fmt.Sprintf("%t", runAsRoot), + "USER": func() string { + if runAsRoot { + return "root" + } + return "kernel" + }(), + "WIDTH": "1024", + "HEIGHT": "768", + "ENABLE_WEBRTC": os.Getenv("ENABLE_WEBRTC"), + "NEKO_ICESERVERS": os.Getenv("NEKO_ICESERVERS"), + } + if strings.Contains(image, "headful") { + // headless image sets its own flags, so only do this for headful + env["CHROMIUM_FLAGS"] = "--no-sandbox --disable-dev-shm-usage --disable-gpu --start-maximized --disable-software-rasterizer --remote-allow-origins=* --no-zygote --password-store=basic --no-first-run" + } + logger.Info("[setup]", "action", "starting container", "image", image, "name", containerName) + _, exitCh, err := runContainer(baseCtx, image, containerName, env) + if err != nil { + t.Fatalf("[setup] failed to start container %s: %v", image, err) + } + defer stopContainer(baseCtx, containerName) + + ctx, cancel := context.WithTimeout(baseCtx, 3*time.Minute) + defer cancel() + logger.Info("[setup]", "action", "waiting for API", "url", apiBaseURL+"/spec.yaml") + if err := waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh); err != nil { + _ = dumpContainerDiagnostics(ctx, containerName) + t.Fatalf("[setup] api not ready: %v", err) + } + logger.Info("[setup]", "action", "waiting for DevTools WebSocket") + wsURL, err := waitDevtoolsWS(ctx) + if err != nil { + t.Fatalf("[setup] devtools not ready: %v", err) + } + + // Diagnostic Phase - Check file ownership and permissions before any navigations + logger.Info("[diagnostic]", "action", "checking file ownership and permissions") + if err := runCookieDebugScript(ctx, t); err != nil { + logger.Warn("[diagnostic]", "action", "cookie debug script failed", "error", err) + } else { + logger.Info("[diagnostic]", "action", "cookie debug script completed successfully") + } + + // Cookie Setting Phase + cookieName := "e2e_cookie" + randBytes := make([]byte, 16) + rand.Read(randBytes) + cookieValue := hex.EncodeToString(randBytes) + serverURL, stopServer := startCookieTestServer(t, cookieName, cookieValue) + defer stopServer() + + logger.Info("[cookies]", "action", "navigate set-cookie", "cookieName", cookieName, "cookieValue", cookieValue) + if err := navigateAndEnsureCookie(ctx, wsURL, serverURL+"/set-cookie", cookieName, cookieValue, "initial"); err != nil { + t.Fatalf("[cookies] failed to set/verify cookie: %v", err) + } + logger.Info("[cookies]", "action", "navigate get-cookies") + if err := navigateAndEnsureCookie(ctx, wsURL, serverURL+"/get-cookies", cookieName, cookieValue, "initial-get-page"); err != nil { + t.Fatalf("[cookies] failed to verify cookie on get-cookies: %v", err) + } + + // Local Storage Setting Phase + localStorageKey := "e2e_localstorage_key" + randBytes = make([]byte, 16) + rand.Read(randBytes) + localStorageValue := hex.EncodeToString(randBytes) + logger.Info("[localstorage]", "action", "set and verify localStorage") + if err := setAndVerifyLocalStorage(ctx, wsURL, serverURL+"/set-cookie", localStorageKey, localStorageValue, "initial"); err != nil { + t.Fatalf("[localstorage] failed to set/verify localStorage: %v", err) + } + + // x.com Cookie Generation Phase + logger.Info("[x-cookies]", "action", "navigate to x.com and verify guest_id cookie") + if err := navigateToXAndVerifyCookie(ctx, wsURL, "initial"); err != nil { + logger.Warn("[x-cookies]", "message", fmt.Sprintf("failed to navigate to x.com and verify cookie: %v", err)) + } + + // Restart & Persistence Testing Phase + logger.Info("[restart]", "action", "stop chromium via supervisorctl") + if err := stopChromiumViaSupervisord(ctx); err != nil { + t.Fatalf("[restart] failed to stop chromium via supervisorctl: %v", err) + } + + // Check file state after stopping + logger.Info("[restart]", "action", "checking file state after stop") + if err := runCookieDebugScript(ctx, t); err != nil { + logger.Warn("[restart]", "action", "post-stop debug script failed", "error", err) + } else { + logger.Info("[restart]", "action", "post-stop debug script completed") + } + + logger.Info("[snapshot]", "action", "download user-data zip") + zipBytes, err := downloadUserDataZip(ctx) + if err != nil { + t.Fatalf("[snapshot] download zip: %v", err) + } + if err := validateZip(zipBytes); err != nil { + t.Fatalf("[snapshot] invalid zip: %v", err) + } + zipPath := filepath.Join(layout.ZipsDir, "user-data-original.zip") + if err := os.WriteFile(zipPath, zipBytes, 0600); err != nil { + t.Fatalf("[snapshot] write zip: %v", err) + } + if err := unzipBytesToDir(zipBytes, layout.RestoreDir); err != nil { + t.Fatalf("[snapshot] unzip: %v", err) + } + + if err := verifyCookieInLocalSnapshot(ctx, layout.RestoreDir, cookieName, cookieValue); err != nil { + logger.Warn("[snapshot]", "message", fmt.Sprintf("verify cookie in sqlite: %v", err)) + } + if err := deleteLocalSingletonLockFiles(layout.RestoreDir); err != nil { + t.Fatalf("[snapshot] delete local singleton locks: %v", err) + } + cleanZipBytes, err := zipDirToBytes(layout.RestoreDir) + if err != nil { + t.Fatalf("[snapshot] zip cleaned snapshot: %v", err) + } + cleanZipPath := filepath.Join(layout.ZipsDir, "user-data-cleaned.zip") + if err := os.WriteFile(cleanZipPath, cleanZipBytes, 0600); err != nil { + t.Fatalf("[snapshot] write cleaned zip: %v", err) + } + logger.Info("[snapshot]", "action", "delete remote user-data") + if err := deleteDirectoryViaAPI(ctx, "/home/kernel/user-data"); err != nil { + t.Fatalf("[snapshot] delete remote user-data: %v", err) + } + logger.Info("[snapshot]", "action", "upload cleaned zip", "bytes", len(cleanZipBytes)) + if err := uploadUserDataZip(ctx, cleanZipBytes); err != nil { + t.Fatalf("[snapshot] upload cleaned zip: %v", err) + } + + // Verify that the cookie exists in the container's cookies database after upload + logger.Info("[snapshot]", "action", "verifying cookie in container database", "cookieName", cookieName) + if err := verifyCookieInContainerDB(ctx, cookieName); err != nil { + logger.Warn("[snapshot]", "message", fmt.Sprintf("cookie not found in container database: %v", err)) + } + + if err := startChromiumViaAPI(ctx); err != nil { + t.Fatalf("[restart] start chromium: %v", err) + } + logger.Info("[restart]", "action", "wait for DevTools") + wsURL, err = waitDevtoolsWS(ctx) + if err != nil { + t.Fatalf("[restart] devtools not ready: %v", err) + } + logger.Info("[restart]", "action", "sleep to init", "seconds", 5) + time.Sleep(5 * time.Second) + + if err := navigateAndEnsureCookie(ctx, wsURL, serverURL+"/get-cookies", cookieName, cookieValue, "after-restart"); err != nil { + t.Fatalf("[final] cookie not persisted after restart: %v", err) + } + logger.Info("[final]", "result", "cookie verified after restart") + + // Verify Local Storage persistence + logger.Info("[final]", "action", "verifying localStorage persistence") + if err := verifyLocalStorage(ctx, wsURL, serverURL+"/set-cookie", localStorageKey, localStorageValue, "after-restart"); err != nil { + t.Fatalf("[final] localStorage not persisted after restart: %v", err) + } + logger.Info("[final]", "result", "localStorage verified after restart") + + logger.Info("[final]", "result", "all persistence mechanisms verified after restart") +} + +func runContainer(ctx context.Context, image, name string, env map[string]string) (*exec.Cmd, <-chan error, error) { + logger := logctx.FromContext(ctx) + args := []string{ + "run", + "--name", name, + "--privileged", + "--network=host", + "--tmpfs", "/dev/shm:size=2g", + } + for k, v := range env { + args = append(args, "-e", fmt.Sprintf("%s=%s", k, v)) + } + args = append(args, image) + + logger.Info("[docker]", "action", "run", "args", strings.Join(args, " ")) + cmd := exec.Command("docker", args...) + if err := cmd.Start(); err != nil { + return nil, nil, err + } + + exitCh := make(chan error, 1) + go func() { + exitCh <- cmd.Wait() + }() + + return cmd, exitCh, nil +} + +func stopContainer(ctx context.Context, name string) error { + _ = exec.CommandContext(ctx, "docker", "kill", name).Run() + _ = exec.CommandContext(ctx, "docker", "rm", "-f", name).Run() + + // Wait loop to ensure the container is actually gone + const maxWait = 10 * time.Second + const pollInterval = 200 * time.Millisecond + deadline := time.Now().Add(maxWait) + var lastCheckErr error + for { + cmd := exec.CommandContext(ctx, "docker", "ps", "-a", "--filter", fmt.Sprintf("name=%s", name), "--format", "{{.Names}}") + out, err := cmd.Output() + if err != nil { + // If docker itself fails, break out (maybe docker is gone) + lastCheckErr = err + break + } + names := strings.Fields(string(out)) + found := false + for _, n := range names { + if n == name { + found = true + break + } + } + if !found { + break // container is gone + } + if time.Now().After(deadline) { + lastCheckErr = fmt.Errorf("timeout waiting for container %s to be removed", name) + break // give up after maxWait + } + time.Sleep(pollInterval) + } + + if lastCheckErr != nil { + return lastCheckErr + } + return nil +} + +// dumpContainerDiagnostics prints container logs and inspect to structured logger for debugging startup failures +func dumpContainerDiagnostics(ctx context.Context, name string) error { + logger := logctx.FromContext(ctx) + logger.Info("[docker]", "action", "collecting logs", "name", name) + logsCmd := exec.CommandContext(ctx, "docker", "logs", name) + logsOut, _ := logsCmd.CombinedOutput() + if len(logsOut) > 0 { + scanner := bufio.NewScanner(bytes.NewReader(logsOut)) + for scanner.Scan() { + logger.Info("[docker]", "action", "diag logs", "line", scanner.Text()) + } + } + logger.Info("[docker]", "action", "inspect", "name", name) + inspectCmd := exec.CommandContext(ctx, "docker", "inspect", name) + inspectOut, _ := inspectCmd.CombinedOutput() + if len(inspectOut) > 0 { + // Trim to a reasonable size + const max = 64 * 1024 + if len(inspectOut) > max { + inspectOut = inspectOut[:max] + } + scanner := bufio.NewScanner(bytes.NewReader(inspectOut)) + for scanner.Scan() { + logger.Info("[docker]", "action", "diag inspect", "line", scanner.Text()) + } + } + return nil +} + +func waitHTTPOrExit(ctx context.Context, url string, exitCh <-chan error) error { + client := &http.Client{Timeout: 5 * time.Second} + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + for { + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + resp, err := client.Do(req) + if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 500 { + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + return nil + } + if resp != nil && resp.Body != nil { + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + } + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-exitCh: + if err != nil { + return fmt.Errorf("container exited while waiting for %s: %w", url, err) + } + return fmt.Errorf("container exited while waiting for %s", url) + case <-ticker.C: + } + } +} + +func waitTCP(ctx context.Context, hostport string) error { + d := net.Dialer{Timeout: 2 * time.Second} + ticker := time.NewTicker(300 * time.Millisecond) + defer ticker.Stop() + for { + conn, err := d.DialContext(ctx, "tcp", hostport) + if err == nil { + conn.Close() + return nil + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + } + } +} + +func waitDevtoolsWS(ctx context.Context) (string, error) { + if err := waitTCP(ctx, "127.0.0.1:9222"); err != nil { + return "", err + } + return "ws://127.0.0.1:9222/", nil +} + +// startCookieTestServer starts an HTTP server listening on 0.0.0.0 +// It serves two pages: +// - /set-cookie: sets a deterministic (passed in to server initialization) cookie if not present and displays cookie state +// - /get-cookies: just displays existing cookies without setting anything +func startCookieTestServer(t *testing.T, cookieName, cookieValue string) (url string, stop func()) { + mux := http.NewServeMux() + nameJS, err := json.Marshal(cookieName) + if err != nil { + t.Fatalf("failed to marshal cookieName: %v", err) + } + valueJS, err := json.Marshal(cookieValue) + if err != nil { + t.Fatalf("failed to marshal cookieValue: %v", err) + } + + // Template for setting cookies + const setCookieHTML = ` + +Set Cookie Test + +

Set Cookie Page

+ + +` + + // Template for getting cookies only + const getCookiesHTML = ` + +Get Cookies Test + +

Get Cookies Page

+

This page only displays cookies, it does not set any.

+

+ + +` + + setCookieTmpl := template.Must(template.New("set_cookie_page").Parse(setCookieHTML)) + var setCookieBuf bytes.Buffer + if err := setCookieTmpl.Execute(&setCookieBuf, map[string]interface{}{ + "NameJS": string(nameJS), + "ValueJS": string(valueJS), + }); err != nil { + t.Fatalf("failed to execute set cookie page template: %v", err) + } + + // Route that sets the cookie + mux.HandleFunc("/set-cookie", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = io.WriteString(w, setCookieBuf.String()) + }) + + // Route that only displays cookies + mux.HandleFunc("/get-cookies", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = io.WriteString(w, getCookiesHTML) + }) + + ln, err := net.Listen("tcp", "0.0.0.0:0") + if err != nil { + t.Fatalf("failed to start cookie test server: %v", err) + } + srv := &http.Server{Handler: mux} + go func() { _ = srv.Serve(ln) }() + + // figure out the random port assigned + _, port, _ := net.SplitHostPort(ln.Addr().String()) + url = "http://127.0.0.1:" + port + stop = func() { + _ = srv.Shutdown(context.Background()) + } + return url, stop +} + +// navigateAndEnsureCookie opens the given URL and asserts that the page's #cookies +// element contains name=value. It is idempotent and used before/after restarts. +func navigateAndEnsureCookie(ctx context.Context, wsURL, url, cookieName, cookieValue string, label string) error { + logger := logctx.FromContext(ctx) + + // Run playwright script + cmd := exec.CommandContext(ctx, "pnpm", "exec", "tsx", "index.ts", + "navigate-and-ensure-cookie", + "--url", url, + "--cookie-name", cookieName, + "--cookie-value", cookieValue, + "--label", label, + "--ws-url", wsURL, + "--timeout", "45000", + ) + cmd.Dir = getPlaywrightPath() + + output, err := cmd.CombinedOutput() + if err != nil { + logger.Info("[playwright]", "action", "navigate-and-ensure-cookie failed", "output", string(output)) + return fmt.Errorf("playwright navigate-and-ensure-cookie failed: %w, output: %s", err, string(output)) + } + + logger.Info("[playwright]", "action", "navigate-and-ensure-cookie success", "output", string(output)) + return nil +} + +// setAndVerifyLocalStorage sets a localStorage key-value pair and verifies it was set correctly +func setAndVerifyLocalStorage(ctx context.Context, wsURL, url, key, value, label string) error { + logger := logctx.FromContext(ctx) + + // Run playwright script + cmd := exec.CommandContext(ctx, "pnpm", "exec", "tsx", "index.ts", + "set-localstorage", + "--url", url, + "--key", key, + "--value", value, + "--label", label, + "--ws-url", wsURL, + "--timeout", "45000", + ) + cmd.Dir = getPlaywrightPath() + + output, err := cmd.CombinedOutput() + if err != nil { + logger.Info("[playwright]", "action", "set-localstorage failed", "output", string(output)) + return fmt.Errorf("playwright set-localstorage failed: %w, output: %s", err, string(output)) + } + + logger.Info("[playwright]", "action", "set-localstorage success", "output", string(output)) + return nil +} + +// verifyLocalStorage verifies that a localStorage key-value pair exists +func verifyLocalStorage(ctx context.Context, wsURL, url, key, value, label string) error { + logger := logctx.FromContext(ctx) + + // Run playwright script + cmd := exec.CommandContext(ctx, "pnpm", "exec", "tsx", "index.ts", + "verify-localstorage", + "--url", url, + "--key", key, + "--value", value, + "--label", label, + "--ws-url", wsURL, + "--timeout", "45000", + ) + cmd.Dir = getPlaywrightPath() + + output, err := cmd.CombinedOutput() + if err != nil { + logger.Info("[playwright]", "action", "verify-localstorage failed", "output", string(output)) + return fmt.Errorf("playwright verify-localstorage failed: %w, output: %s", err, string(output)) + } + + logger.Info("[playwright]", "action", "verify-localstorage success", "output", string(output)) + return nil +} + +// navigateToXAndVerifyCookie navigates to x.com and then to news.ycombinator.com to generate cookies, +// then verifies that the guest_id cookie was created for .x.com +func navigateToXAndVerifyCookie(ctx context.Context, wsURL string, label string) error { + logger := logctx.FromContext(ctx) + + // Run playwright script to navigate to x.com and back + cmd := exec.CommandContext(ctx, "pnpm", "exec", "tsx", "index.ts", + "navigate-to-x-and-back", + "--label", label, + "--ws-url", wsURL, + "--timeout", "45000", + ) + cmd.Dir = getPlaywrightPath() + + output, err := cmd.CombinedOutput() + if err != nil { + logger.Info("[playwright]", "action", "navigate-to-x-and-back failed", "output", string(output)) + return fmt.Errorf("playwright navigate-to-x-and-back failed: %w, output: %s", err, string(output)) + } + + logger.Info("[playwright]", "action", "navigate-to-x-and-back success", "output", string(output)) + + // Now verify the cookie was created by querying the database + logger.Info("[cookie-verify]", "action", "verifying guest_id cookie for .x.com") + + // wild: it takes about 10 seconds for cookies to flush to disk, and a supervisorctl stop / sigterm does not force it either. So we sleep + time.Sleep(15 * time.Second) + + // Execute SQLite query to check for the cookie + sqlQuery := `SELECT creation_utc,host_key,name,value,encrypted_value,last_update_utc FROM cookies WHERE host_key=".x.com" AND name="guest_id";` + + // Find the Cookies database file path + cookiesDBPath := "/home/kernel/user-data/Default/Cookies" + + stdout, err := execCombinedOutput(ctx, "sqlite3", []string{cookiesDBPath, "-header", "-column", sqlQuery}) + if err != nil { + return fmt.Errorf("failed to execute sqlite3 query on primary path: %w, output: %s", err, stdout) + } + + // Log the raw output for debugging + logger.Info("[cookie-verify]", "action", "sqlite3 output", "stdout", stdout) + + // Check if the output contains the expected cookie + if !strings.Contains(stdout, ".x.com") || !strings.Contains(stdout, "guest_id") { + logger.Error("[cookie-verify]", "action", "guest_id cookie not found", "output", stdout) + return fmt.Errorf("guest_id cookie for .x.com not found in database output: %s", stdout) + } + + logger.Info("[cookie-verify]", "action", "guest_id cookie verified successfully", "output", stdout) + return nil +} + +func apiClient() (*instanceoapi.ClientWithResponses, error) { + return instanceoapi.NewClientWithResponses(apiBaseURL, instanceoapi.WithHTTPClient(http.DefaultClient)) +} + +// RemoteExecError represents a non-zero exit from a remote exec, exposing exit code and combined output +type RemoteExecError struct { + Command string + Args []string + ExitCode int + Output string +} + +func (e *RemoteExecError) Error() string { + return fmt.Sprintf("remote exec %s exited with code %d", e.Command, e.ExitCode) +} + +// execCombinedOutput runs a command via the remote API and returns combined stdout+stderr and an error if exit code != 0 +func execCombinedOutput(ctx context.Context, command string, args []string) (string, error) { + client, err := apiClient() + if err != nil { + return "", err + } + + req := instanceoapi.ProcessExecJSONRequestBody{ + Command: command, + Args: &args, + } + + rsp, err := client.ProcessExecWithResponse(ctx, req) + if err != nil { + return "", err + } + if rsp.JSON200 == nil { + return "", fmt.Errorf("remote exec failed: %s body=%s", rsp.Status(), string(rsp.Body)) + } + + var stdout, stderr string + if rsp.JSON200.StdoutB64 != nil && *rsp.JSON200.StdoutB64 != "" { + if b, decErr := base64.StdEncoding.DecodeString(*rsp.JSON200.StdoutB64); decErr == nil { + stdout = string(b) + } + } + if rsp.JSON200.StderrB64 != nil && *rsp.JSON200.StderrB64 != "" { + if b, decErr := base64.StdEncoding.DecodeString(*rsp.JSON200.StderrB64); decErr == nil { + stderr = string(b) + } + } + combined := stdout + stderr + + exitCode := 0 + if rsp.JSON200.ExitCode != nil { + exitCode = *rsp.JSON200.ExitCode + } + if exitCode != 0 { + return combined, &RemoteExecError{Command: command, Args: args, ExitCode: exitCode, Output: combined} + } + return combined, nil +} + +func downloadUserDataZip(ctx context.Context) ([]byte, error) { + client, err := apiClient() + if err != nil { + return nil, err + } + params := &instanceoapi.DownloadDirZipParams{Path: "/home/kernel/user-data"} + rsp, err := client.DownloadDirZipWithResponse(ctx, params) + if err != nil { + return nil, err + } + if rsp.StatusCode() != http.StatusOK { + return nil, fmt.Errorf("unexpected status downloading zip: %s body=%s", rsp.Status(), string(rsp.Body)) + } + return rsp.Body, nil +} + +func uploadUserDataZip(ctx context.Context, zipBytes []byte) error { + client, err := apiClient() + if err != nil { + return err + } + var body bytes.Buffer + w := multipart.NewWriter(&body) + fw, err := w.CreateFormFile("zip_file", "user-data.zip") + if err != nil { + return err + } + if _, err := io.Copy(fw, bytes.NewReader(zipBytes)); err != nil { + return err + } + if err := w.WriteField("dest_path", "/home/kernel/user-data"); err != nil { + return err + } + if err := w.Close(); err != nil { + return err + } + _, err = client.UploadZipWithBodyWithResponse(ctx, w.FormDataContentType(), &body) + return err +} + +func startChromiumViaAPI(ctx context.Context) error { + if out, err := execCombinedOutput(ctx, "supervisorctl", []string{"-c", "/etc/supervisor/supervisord.conf", "start", "chromium"}); err != nil { + return fmt.Errorf("failed to start chromium: %w, output: %s", err, out) + } + // Ensure process fully running before proceeding + if err := waitForProgramStates(ctx, "chromium", []string{"RUNNING"}, 10*time.Second); err != nil { + return err + } + return nil +} + +func deleteDirectoryViaAPI(ctx context.Context, path string) error { + client, err := apiClient() + if err != nil { + return err + } + body := instanceoapi.DeleteDirectoryJSONRequestBody{Path: path} + rsp, err := client.DeleteDirectoryWithResponse(ctx, body) + if err != nil { + return err + } + if rsp.StatusCode() != http.StatusOK { + return fmt.Errorf("unexpected status deleting directory: %s body=%s", rsp.Status(), string(rsp.Body)) + } + return nil +} + +func validateZip(b []byte) error { + r, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) + if err != nil { + return err + } + // Ensure at least one file + if len(r.File) == 0 { + return fmt.Errorf("empty zip") + } + // Try opening first file header to sanity-check + f := r.File[0] + rc, err := f.Open() + if err != nil { + return err + } + _, _ = io.Copy(io.Discard, rc) + rc.Close() + return nil +} + +type testLayout struct { + BaseDir string + ZipsDir string + RestoreDir string +} + +// createTestTempLayout creates .tmp/userdata-test-{timestamp}/ with subdirs for zips and the restored userdata directory (i.e. after saving and preparing for reuse) +func createTestTempLayout(t *testing.T) testLayout { + // Base under repo local .tmp + base := filepath.Join(".tmp", fmt.Sprintf("userdata-test-%d", time.Now().UnixNano())) + paths := []string{ + base, + filepath.Join(base, "zips"), + filepath.Join(base, "restored"), + } + for _, p := range paths { + if err := os.MkdirAll(p, 0700); err != nil { + t.Fatalf("create temp dir %s: %v", p, err) + } + } + return testLayout{ + BaseDir: base, + ZipsDir: filepath.Join(base, "zips"), + RestoreDir: filepath.Join(base, "restored"), + } +} + +// unzipBytesToDir extracts a zip archive (in-memory) into destDir +func unzipBytesToDir(b []byte, destDir string) error { + r, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) + if err != nil { + return err + } + for _, f := range r.File { + // Sanitize name + name := filepath.Clean(f.Name) + if strings.HasPrefix(name, "..") { + return fmt.Errorf("invalid zip path: %s", f.Name) + } + abs := filepath.Join(destDir, name) + if f.FileInfo().IsDir() { + if err := os.MkdirAll(abs, 0755); err != nil { + return err + } + continue + } + if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil { + return err + } + rc, err := f.Open() + if err != nil { + return err + } + w, err := os.OpenFile(abs, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode()) + if err != nil { + rc.Close() + return err + } + if _, err := io.Copy(w, rc); err != nil { + w.Close() + rc.Close() + return err + } + w.Close() + rc.Close() + } + return nil +} + +// zipDirToBytes zips the contents of dir (no extra top-level folder) to bytes +func zipDirToBytes(dir string) ([]byte, error) { + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + defer zw.Close() + + // Walk dir + root := filepath.Clean(dir) + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if path == root { + return nil + } + rel, err := filepath.Rel(root, path) + if err != nil { + return err + } + rel = filepath.ToSlash(rel) + if info.IsDir() { + _, err := zw.Create(rel + "/") + return err + } + fh, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + fh.Name = rel + fh.Method = zip.Deflate + w, err := zw.CreateHeader(fh) + if err != nil { + return err + } + f, err := os.Open(path) + if err != nil { + return err + } + _, copyErr := io.Copy(w, f) + f.Close() + return copyErr + }) + if err != nil { + return nil, err + } + if err := zw.Close(); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// verifyCookieInLocalSnapshot verifies cookie presence in local unzipped snapshot +func verifyCookieInLocalSnapshot(ctx context.Context, root string, cookieName, wantValue string) error { + logger := logctx.FromContext(ctx) + candidates := []string{ + filepath.Join(root, "Default", "Network", "Cookies"), + filepath.Join(root, "Default", "Cookies"), + } + + logger.Info("[verify]", "action", "checking cookie database", "cookieName", cookieName, "wantValue", wantValue) + + for _, p := range candidates { + logger.Info("[verify]", "action", "checking database", "path", p) + ok, err := inspectLocalCookiesDB(ctx, p, cookieName, wantValue) + if err == nil && ok { + logger.Info("[verify]", "action", "cookie found", "path", p) + return nil + } + if err != nil { + logger.Warn("[verify]", "action", "database check failed", "path", p, "error", err) + } + } + return fmt.Errorf("cookie %q not found in local snapshot", cookieName) +} + +func inspectLocalCookiesDB(ctx context.Context, dbPath, cookieName, wantValue string) (bool, error) { + logger := logctx.FromContext(ctx) + + // If db does not exist, skip + if _, err := os.Stat(dbPath); err != nil { + logger.Info("[inspect]", "action", "database file not found", "path", dbPath, "error", err) + return false, nil + } + + logger.Info("[inspect]", "action", "opening database", "path", dbPath) + db, err := sql.Open("sqlite", dbPath+"?_pragma=query_only(1)&_pragma=journal_mode(wal)") + if err != nil { + logger.Warn("[inspect]", "action", "failed to open database", "path", dbPath, "error", err) + return false, err + } + defer db.Close() + + rows, err := db.QueryContext(ctx, "SELECT name, value, length(encrypted_value) FROM cookies") + if err != nil { + logger.Warn("[inspect]", "action", "failed to query cookies", "path", dbPath, "error", err) + return false, err + } + defer rows.Close() + + logger.Info("[inspect]", "action", "scanning cookies from database", "path", dbPath) + cookieCount := 0 + for rows.Next() { + var name, value string + var encLen int64 + if err := rows.Scan(&name, &value, &encLen); err != nil { + logger.Warn("[inspect]", "action", "failed to scan cookie row", "error", err) + continue + } + cookieCount++ + logger.Info("[inspect]", "action", "found cookie", "name", name, "value", value, "encrypted", encLen > 0) + + if name == cookieName { + if value == wantValue || encLen > 0 { + logger.Info("[inspect]", "action", "target cookie found", "name", name, "value", value, "encrypted", encLen > 0) + return true, nil + } + } + } + + logger.Info("[inspect]", "action", "database scan complete", "path", dbPath, "totalCookies", cookieCount) + return false, rows.Err() +} + +// deleteLocalSingletonLockFiles removes Chromium singleton files in a local snapshot +func deleteLocalSingletonLockFiles(root string) error { + for _, name := range []string{"SingletonLock", "SingletonCookie", "SingletonSocket", "RunningChromeVersion"} { + p := filepath.Join(root, name) + _ = os.Remove(p) + } + return nil +} + +// execCommandWithResponse is a helper function that executes a command via the remote API +// and handles the response parsing consistently across all callers +// Deprecated: use execCombinedOutput instead +func execCommandWithResponse(ctx context.Context, command string, args []string) (*instanceoapi.ProcessExecResponse, error) { + client, err := apiClient() + if err != nil { + return nil, err + } + + req := instanceoapi.ProcessExecJSONRequestBody{ + Command: command, + Args: &args, + } + + return client.ProcessExecWithResponse(ctx, req) +} + +// stopChromiumViaSupervisord stops chromium using supervisord via the remote API +func stopChromiumViaSupervisord(ctx context.Context) error { + logger := logctx.FromContext(ctx) + + // Wait a bit for any pending I/O to complete + logger.Info("[stop]", "action", "waiting for I/O flush", "seconds", 3) + time.Sleep(3 * time.Second) + + // Now use supervisorctl to ensure it's fully stopped + logger.Info("[stop]", "action", "stopping via supervisorctl") + if out, stopErr := execCombinedOutput(ctx, "supervisorctl", []string{"-c", "/etc/supervisor/supervisord.conf", "stop", "chromium"}); stopErr != nil { + return fmt.Errorf("failed to stop chromium via supervisorctl: %w, output: %s", stopErr, out) + } + + // Accept either STOPPED or EXITED as terminal stopped states + desiredStates := []string{"STOPPED", "EXITED"} + if waitErr := waitForProgramStates(ctx, "chromium", desiredStates, 5*time.Second); waitErr != nil { + return fmt.Errorf("chromium did not reach a stopped state: %w", waitErr) + } + + // Allow a short grace period for I/O flush + time.Sleep(1 * time.Second) + return nil +} + +// getProgramState returns the current supervisor state (e.g. RUNNING, STOPPED, EXITED) for the given program. +// It parses the output of `supervisorctl status` even if the command exits with a non-zero status code, which +// supervisorctl does when the target program is not in the RUNNING state. +func getProgramState(ctx context.Context, programName string) (string, error) { + stdout, err := execCombinedOutput(ctx, "supervisorctl", []string{"-c", "/etc/supervisor/supervisord.conf", "status", programName}) + if err != nil { + if execErr, ok := err.(*RemoteExecError); ok && execErr.ExitCode == 3 { + stdout = execErr.Output + } else { + return "", err + } + } + + // Expected output example: + // "chromium STOPPED Sep 21 10:05 AM" + // "chromium EXITED Sep 21 10:05 AM (exit status 0)" + fields := strings.Fields(stdout) + if len(fields) < 2 { + return "", fmt.Errorf("unexpected supervisorctl status output: %s", stdout) + } + return fields[1], nil +} + +// waitForProgramStates polls supervisorctl status until the program reaches any of the desired states +// or the timeout expires. +func waitForProgramStates(ctx context.Context, programName string, desiredStates []string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + + contains := func(list []string, s string) bool { + for _, v := range list { + if v == s { + return true + } + } + return false + } + + for { + state, err := getProgramState(ctx, programName) + if err == nil && contains(desiredStates, state) { + return nil + } + + if time.Now().After(deadline) { + if err != nil { + return err + } + return fmt.Errorf("timeout waiting for %s to reach states %v (last state %s)", programName, desiredStates, state) + } + time.Sleep(500 * time.Millisecond) + } +} + +// runCookieDebugScript executes the cookie debug script in the container to check file ownership and permissions +func runCookieDebugScript(ctx context.Context, t *testing.T) error { + logger := logctx.FromContext(ctx) + + // Read the debug script content + scriptContent, err := os.ReadFile("cookie_debug.sh") + if err != nil { + return fmt.Errorf("failed to read debug script: %w", err) + } + + // Execute the script content directly via bash + args := []string{"-c", string(scriptContent)} + stdout, err := execCombinedOutput(ctx, "bash", args) + if err != nil { + return fmt.Errorf("failed to execute debug script: %w, output: %s", err, stdout) + } + + logger.Info("[diagnostic]", "action", "debug script output") + fmt.Fprint(t.Output(), stdout) + return nil +} + +// verifyCookieInContainerDB checks that the specified cookie exists in the cookies database on the container +func verifyCookieInContainerDB(ctx context.Context, cookieName string) error { + logger := logctx.FromContext(ctx) + + // Execute SQLite query to check for the cookie + sqlQuery := fmt.Sprintf(`SELECT creation_utc,host_key,name,value,encrypted_value,last_update_utc FROM cookies WHERE name="%s";`, cookieName) + + // Find the Cookies database file path + cookiesDBPath := "/home/kernel/user-data/Default/Cookies" + + stdout, err := execCombinedOutput(ctx, "sqlite3", []string{cookiesDBPath, "-header", "-column", sqlQuery}) + if err != nil { + return fmt.Errorf("failed to execute sqlite3 query: %w, output: %s", err, stdout) + } + + // Log the raw output for debugging + logger.Info("[container-cookie-verify]", "action", "sqlite3 output", "stdout", stdout) + + // Check if the output contains the expected cookie + if !strings.Contains(stdout, cookieName) { + logger.Error("[container-cookie-verify]", "action", "cookie not found", "cookieName", cookieName, "output", stdout) + return fmt.Errorf("cookie %q not found in container database output: %s", cookieName, stdout) + } + + logger.Info("[container-cookie-verify]", "action", "cookie verified successfully", "cookieName", cookieName, "output", stdout) + return nil +} diff --git a/server/e2e/playwright/.gitignore b/server/e2e/playwright/.gitignore new file mode 100644 index 00000000..390dc31b --- /dev/null +++ b/server/e2e/playwright/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +*.log +.pnpm-debug.log* +cookie-verify-*.png +*.png diff --git a/server/e2e/playwright/README.md b/server/e2e/playwright/README.md new file mode 100644 index 00000000..99af9a0c --- /dev/null +++ b/server/e2e/playwright/README.md @@ -0,0 +1,74 @@ +# Playwright CDP Integration + +This directory contains a Playwright-based script that replaces the chromedp functionality in the e2e tests. + +## Installation + +```bash +pnpm install +``` + +## Usage + +The script connects to an existing Chrome browser instance via CDP (Chrome DevTools Protocol) and performs various browser automation tasks. + +### Commands + +#### navigate-and-ensure-cookie + +Navigates to a URL and ensures a specific cookie exists with the expected value: + +```bash +pnpm exec tsx index.ts navigate-and-ensure-cookie \ + --url "http://localhost:8080/set-cookie" \ + --cookie-name "session_id" \ + --cookie-value "abc123" \ + --label "test-label" \ + --ws-url "ws://127.0.0.1:9222/" \ + --timeout 45000 +``` + +Options: +- `--url` (required): The URL to navigate to +- `--cookie-name` (required): The name of the cookie to check +- `--cookie-value` (required): The expected value of the cookie +- `--label` (optional): Label for screenshot filenames on failure +- `--ws-url` (optional): WebSocket URL for CDP connection (default: ws://127.0.0.1:9222/) +- `--timeout` (optional): Timeout in milliseconds (default: 45000) + +#### capture-screenshot + +Takes a full-page screenshot: + +```bash +pnpm exec tsx index.ts capture-screenshot \ + --filename "screenshot.png" \ + --ws-url "ws://127.0.0.1:9222/" +``` + +Options: +- `--filename` (required): Output filename for the screenshot +- `--ws-url` (optional): WebSocket URL for CDP connection (default: ws://127.0.0.1:9222/) + +## Integration with Go Tests + +The Go e2e tests execute this script via `exec.Command` to perform browser automation tasks. The script: + +1. Connects to an existing Chrome instance (not launching a new one) +2. Reuses existing browser contexts and pages when possible +3. Returns appropriate exit codes (0 for success, 1 for failure) +4. Outputs logs to stdout/stderr for debugging + +## Development + +To test the script locally: + +1. Start a Chrome instance with remote debugging enabled: + ```bash + chromium --remote-debugging-port=9222 + ``` + +2. Run the script with desired arguments: + ```bash + pnpm exec tsx index.ts [options] + ``` diff --git a/server/e2e/playwright/index.ts b/server/e2e/playwright/index.ts new file mode 100644 index 00000000..559cbe1a --- /dev/null +++ b/server/e2e/playwright/index.ts @@ -0,0 +1,408 @@ +#!/usr/bin/env tsx + +import { writeFileSync } from 'fs'; +import { Browser, BrowserContext, chromium, Page } from 'playwright-core'; + +interface CommandOptions { + wsURL?: string; + timeout?: number; +} + +interface NavigateCookieOptions extends CommandOptions { + url: string; + cookieName: string; + cookieValue: string; + label?: string; +} + +interface NavigateCookieFormOptions extends CommandOptions { + url: string; + cookieName: string; + cookieValue: string; + label?: string; +} + +interface LocalStorageOptions extends CommandOptions { + url: string; + key: string; + value: string; + label?: string; +} + +interface HistoryOptions extends CommandOptions { + urls: string[]; + label?: string; +} + +interface NavigateXAndBackOptions extends CommandOptions { + label?: string; +} + +interface ScreenshotOptions extends CommandOptions { + filename: string; +} + +class CDPClient { + private browser?: Browser; + private context?: BrowserContext; + private page?: Page; + + async connect(wsURL: string = 'ws://127.0.0.1:9222/'): Promise { + try { + // Connect to existing browser via CDP + this.browser = await chromium.connectOverCDP(wsURL); + + // Get the default context (or first available context) + const contexts = this.browser.contexts(); + if (contexts.length > 0) { + this.context = contexts[0]; + } else { + // This shouldn't happen with an existing browser, but just in case + this.context = await this.browser.newContext(); + } + + // Get existing page or create new one + const pages = this.context.pages(); + if (pages.length > 0) { + this.page = pages[0]; + } else { + this.page = await this.context.newPage(); + } + } catch (error) { + console.error('Failed to connect to browser:', error); + throw error; + } + } + + async navigateAndEnsureCookie(options: NavigateCookieOptions): Promise { + if (!this.page) throw new Error('Not connected to browser'); + + const { url, cookieName, cookieValue, label = 'check', timeout = 45000 } = options; + + // Array to collect browser console logs + const browserLogs: string[] = []; + // Handler to push logs from browser console + const consoleListener = (msg: any) => { + // Only log 'log', 'warn', 'error', 'info' types + if (['log', 'warn', 'error', 'info'].includes(msg.type())) { + // Join all arguments as string + const text = msg.text(); + browserLogs.push(`[browser][${msg.type()}] ${text}`); + } + }; + + try { + console.log(`[cdp] action: navigate-cookie, url: ${url}, label: ${label}`); + + // Attach console listener + this.page.on('console', consoleListener); + + // Set timeout for this operation + this.page.setDefaultTimeout(timeout); + + // Navigate to the URL + await this.page.goto(url, { waitUntil: 'domcontentloaded' }); + + // Wait for #cookies element to be visible + await this.page.waitForSelector('#cookies', { state: 'visible', timeout: 5000 }); + + // Get the text content of #cookies element + const cookiesText = await this.page.textContent('#cookies'); + + // Echo browser console logs + if (browserLogs.length > 0) { + for (const log of browserLogs) { + console.log(log); + } + } + + if (!cookiesText) { + throw new Error('#cookies element has no text content'); + } + + // Check if the cookie exists with the expected value + const expectedCookie = `${cookieName}=${cookieValue}`; + if (!cookiesText.includes(expectedCookie)) { + // Take a screenshot on failure + const screenshotPath = `cookie-verify-miss-${label}.png`; + await this.captureScreenshot({ filename: screenshotPath }); + throw new Error(`Expected document.cookie to contain "${expectedCookie}", got "${cookiesText}"`); + } + + console.log(`Cookie verified successfully: ${cookieName}=${cookieValue}`); + + } catch (error) { + // Echo browser console logs on error as well + if (browserLogs.length > 0) { + for (const log of browserLogs) { + console.log(log); + } + } + // Take a screenshot on any error + const screenshotPath = `cookie-verify-${label}.png`; + await this.captureScreenshot({ filename: screenshotPath }).catch(console.error); + throw error; + } finally { + // Remove the console listener to avoid leaks + if (this.page) { + this.page.off('console', consoleListener); + } + } + } + + async setAndVerifyLocalStorage(options: LocalStorageOptions): Promise { + if (!this.page) throw new Error('Not connected to browser'); + + const { url, key, value, label = 'localstorage', timeout = 45000 } = options; + + try { + console.log(`[cdp] action: set-localstorage, url: ${url}, key: ${key}, value: ${value}, label: ${label}`); + + // Set timeout for this operation + this.page.setDefaultTimeout(timeout); + + // Navigate to the URL + await this.page.goto(url, { waitUntil: 'domcontentloaded' }); + + // Set localStorage value + await this.page.evaluate(({ k, v }: { k: string; v: string }) => { + (globalThis as any).localStorage.setItem(k, v); + console.log(`[localStorage] Set ${k}=${v}`); + }, { k: key, v: value }); + + // Verify localStorage value + const storedValue = await this.page.evaluate(({ k }: { k: string }) => { + return (globalThis as any).localStorage.getItem(k); + }, { k: key }); + + if (storedValue !== value) { + const screenshotPath = `localstorage-verify-miss-${label}.png`; + await this.captureScreenshot({ filename: screenshotPath }); + throw new Error(`Expected localStorage["${key}"] to be "${value}", got "${storedValue}"`); + } + + console.log(`LocalStorage verified successfully: ${key}=${value}`); + + // Navigate to google.com to potentially force a flush + console.log('[cdp] action: navigating to google.com to force localStorage flush'); + try { + await this.page.goto('https://www.google.com', { waitUntil: 'domcontentloaded' }); + console.log('[cdp] action: google.com navigation completed'); + } catch (navError) { + console.warn('[cdp] action: google.com navigation failed, continuing anyway:', navError); + } + } catch (error) { + const screenshotPath = `localstorage-verify-${label}.png`; + await this.captureScreenshot({ filename: screenshotPath }).catch(console.error); + throw error; + } + } + + async verifyLocalStorage(options: LocalStorageOptions): Promise { + if (!this.page) throw new Error('Not connected to browser'); + + const { url, key, value, label = 'localstorage-verify', timeout = 45000 } = options; + + try { + console.log(`[cdp] action: verify-localstorage, url: ${url}, key: ${key}, expected: ${value}, label: ${label}`); + + // Set timeout for this operation + this.page.setDefaultTimeout(timeout); + + // Navigate to the URL + await this.page.goto(url, { waitUntil: 'domcontentloaded' }); + + // Get localStorage value + const storedValue = await this.page.evaluate(({ k }: { k: string }) => { + return (globalThis as any).localStorage.getItem(k); + }, { k: key }); + + if (storedValue !== value) { + const screenshotPath = `localstorage-verify-fail-${label}.png`; + await this.captureScreenshot({ filename: screenshotPath }); + throw new Error(`Expected localStorage["${key}"] to be "${value}", got "${storedValue}"`); + } + + console.log(`LocalStorage verification successful: ${key}=${value}`); + } catch (error) { + const screenshotPath = `localstorage-verify-fail-${label}.png`; + await this.captureScreenshot({ filename: screenshotPath }).catch(console.error); + throw error; + } + } + + async navigateToXAndBack(options: NavigateXAndBackOptions): Promise { + if (!this.page) throw new Error('Not connected to browser'); + + const { label = 'x-navigation', timeout = 45000 } = options; + + try { + console.log(`[cdp] action: navigate-to-x-and-back, label: ${label}`); + + // Set timeout for this operation + this.page.setDefaultTimeout(timeout); + + // Do the navigation to x.com and back twice in a loop + for (let i = 0; i < 2; i++) { + console.log(`[cdp] action: [${i + 1}/2] navigating to x.com`); + await this.page.goto('https://x.com', { waitUntil: 'domcontentloaded' }); + + // Wait a bit to ensure cookies are set + await this.page.waitForTimeout(2000); + + console.log(`[cdp] action: [${i + 1}/2] navigating to news.ycombinator.com`); + await this.page.goto('https://news.ycombinator.com', { waitUntil: 'domcontentloaded' }); + + // Wait a bit to ensure the navigation is recorded + await this.page.waitForTimeout(2000); + } + + console.log('X.com navigation and return completed successfully'); + + } catch (error) { + const screenshotPath = `x-navigation-${label}.png`; + await this.captureScreenshot({ filename: screenshotPath }).catch(console.error); + throw error; + } + } + + async captureScreenshot(options: ScreenshotOptions): Promise { + if (!this.page) throw new Error('Not connected to browser'); + + const { filename } = options; + + try { + // Take a full page screenshot + const screenshot = await this.page.screenshot({ + fullPage: true, + type: 'png', + }); + + // Write to file + writeFileSync(filename, screenshot); + console.log(`Screenshot saved to: ${filename}`); + } catch (error) { + console.error('Failed to capture screenshot:', error); + throw error; + } + } + + async disconnect(): Promise { + // Note: We don't close the browser since it's an existing instance + // We just disconnect from it + if (this.browser) { + await this.browser.close().catch(() => { + // Ignore errors when disconnecting + }); + } + } +} + +async function main(): Promise { + const args = process.argv.slice(2); + + if (args.length === 0) { + console.error('Usage: tsx index.ts [options]'); + console.error('Commands:'); + console.error(' navigate-and-ensure-cookie --url --cookie-name --cookie-value [--label