Skip to content

Commit 2c9d6bf

Browse files
authored
Integrate api server (#19)
### Description In #17 we built a new API server. In this PR we integrate it into the container + unikernel image builds. It's off by default and can be turned + configured with env vars ### Testing - [x] Built docker container - [x] Ran docker container locally without the server - [x] Ran docker container locally with `WITH_KERNEL_IMAGES_API=true`. Confirmed the server started up and was able to perform the basic start, stop, down workflow successfully - [x] Built unikernel container - [x] Ran unikernel in the cloud without the server - [x] Ran unikernel in the cloud with `WITH_KERNEL_IMAGES_API=true`. Confirmed the server started up and was able to perform the basic start, stop, download workflow successfully
1 parent e0f4d09 commit 2c9d6bf

File tree

12 files changed

+187
-11
lines changed

12 files changed

+187
-11
lines changed

containers/docker/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bin/
2+
recordings/

containers/docker/Dockerfile

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ FROM ghcr.io/anthropics/anthropic-quickstarts:computer-use-demo-latest
22

33
USER root
44
RUN add-apt-repository -y ppa:xtradeb/apps
5-
RUN apt update -y && apt install -y chromium ncat
5+
RUN apt update -y && apt install -y chromium ncat ffmpeg
66

77
# Switch to computeruse user
88
USER computeruse
99

10+
# copy the kernel-images API binary built by build.sh
11+
COPY bin/kernel-images-api /usr/local/bin/kernel-images-api
12+
ENV WITH_KERNEL_IMAGES_API=false
13+
1014
# Modify entrypoint script
1115
# The original can be found here: https://github.com/anthropics/anthropic-quickstarts/blob/main/computer-use-demo/image/entrypoint.sh
1216
COPY --chmod=0755 <<'EOL' /home/computeruse/entrypoint.sh
@@ -21,10 +25,15 @@ cleanup () {
2125
echo "Cleaning up..."
2226
kill -TERM $pid
2327
kill -TERM $pid2
28+
# Kill API pid if set
29+
if [[ -n "${pid3:-}" ]]; then
30+
kill -TERM $pid3 || true
31+
fi
2432
}
2533
trap cleanup TERM INT
2634
pid=
2735
pid2=
36+
pid3=
2837
INTERNAL_PORT=9223
2938
CHROME_PORT=9222 # External port mapped in Docker
3039
echo "Starting Chromium on internal port $INTERNAL_PORT"
@@ -40,6 +49,25 @@ ncat \
4049

4150
./novnc_startup.sh >&2
4251

52+
if [[ "${WITH_KERNEL_IMAGES_API:-}" == "true" ]]; then
53+
echo "✨ Starting kernel-images API."
54+
55+
API_PORT="${KERNEL_IMAGES_API_PORT:-10001}"
56+
API_FRAME_RATE="${KERNEL_IMAGES_API_FRAME_RATE:-10}"
57+
API_DISPLAY_NUM="${KERNEL_IMAGES_API_DISPLAY_NUM:-1}"
58+
API_MAX_SIZE_MB="${KERNEL_IMAGES_API_MAX_SIZE_MB:-500}"
59+
API_OUTPUT_DIR="${KERNEL_IMAGES_API_OUTPUT_DIR:-/recordings}"
60+
61+
mkdir -p "$API_OUTPUT_DIR"
62+
63+
PORT="$API_PORT" \
64+
FRAME_RATE="$API_FRAME_RATE" \
65+
DISPLAY_NUM="$API_DISPLAY_NUM" \
66+
MAX_SIZE_MB="$API_MAX_SIZE_MB" \
67+
OUTPUT_DIR="$API_OUTPUT_DIR" \
68+
/usr/local/bin/kernel-images-api & pid3=$!
69+
fi
70+
4371
python http_server.py >&2 &
4472

4573
STREAMLIT_SERVER_PORT=8501 python -m streamlit run computer_use_demo/streamlit.py >&2

containers/docker/build.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env bash
2+
set -ex -o pipefail
3+
4+
# Move to the script's directory so relative paths work regardless of the caller CWD
5+
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
6+
cd "$SCRIPT_DIR"
7+
8+
IMAGE="${IMAGE:-onkernel/kernel-cu-test:latest}"
9+
10+
source ../../shared/start-buildkit.sh
11+
12+
# Build the kernel-images API binary and place it into ./bin for Docker build context
13+
source ../../shared/build-server.sh "$(pwd)/bin"
14+
15+
# Build (and optionally push) the Docker image.
16+
docker build -t "$IMAGE" .

containers/docker/run.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env bash
2+
set -ex -o pipefail
3+
4+
# Move to the script's directory so relative paths work regardless of the caller CWD
5+
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
6+
cd "$SCRIPT_DIR"
7+
8+
IMAGE="${IMAGE:-onkernel/kernel-cu-test:latest}"
9+
NAME="${NAME:-kernel-cu-test}"
10+
11+
# Directory on host where recordings will be saved
12+
HOST_RECORDINGS_DIR="$SCRIPT_DIR/recordings"
13+
mkdir -p "$HOST_RECORDINGS_DIR"
14+
15+
# Build docker run argument list
16+
RUN_ARGS=(
17+
--name "$NAME"
18+
--privileged
19+
--tmpfs /dev/shm:size=2g
20+
-v "$HOST_RECORDINGS_DIR:/recordings"
21+
--memory 8192m
22+
-p 9222:9222 \
23+
-e DISPLAY_NUM=1 \
24+
-e HEIGHT=768 \
25+
-e WIDTH=1024 \
26+
-e CHROMIUM_FLAGS="--no-sandbox --disable-dev-shm-usage --disable-gpu --start-maximized --disable-software-rasterizer --remote-allow-origins=* --no-zygote"
27+
)
28+
29+
if [[ "${WITH_KERNEL_IMAGES_API:-}" == "true" ]]; then
30+
RUN_ARGS+=( -p 444:10001 )
31+
RUN_ARGS+=( -e WITH_KERNEL_IMAGES_API=true )
32+
fi
33+
34+
# noVNC vs WebRTC port mapping
35+
if [[ "${ENABLE_WEBRTC:-}" == "true" ]]; then
36+
echo "Running container with WebRTC"
37+
RUN_ARGS+=( -p 443:8080 )
38+
RUN_ARGS+=( -e ENABLE_WEBRTC=true )
39+
[[ -n "${NEKO_ICESERVERS:-}" ]] && RUN_ARGS+=( -e NEKO_ICESERVERS="$NEKO_ICESERVERS" )
40+
else
41+
echo "Running container with noVNC"
42+
RUN_ARGS+=( -p 443:6080 )
43+
fi
44+
45+
docker rm -f "$NAME" 2>/dev/null || true
46+
docker run -d "${RUN_ARGS[@]}" "$IMAGE"

server/cmd/api/main.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os/exec"
1010
"os/signal"
1111
"syscall"
12+
"time"
1213

1314
"github.com/ghodss/yaml"
1415
"github.com/go-chi/chi/v5"
@@ -101,13 +102,15 @@ func main() {
101102
<-ctx.Done()
102103
slogger.Info("shutdown signal received")
103104

104-
g, _ := errgroup.WithContext(ctx)
105+
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
106+
defer shutdownCancel()
107+
g, _ := errgroup.WithContext(shutdownCtx)
105108

106109
g.Go(func() error {
107-
return srv.Shutdown(context.Background())
110+
return srv.Shutdown(shutdownCtx)
108111
})
109112
g.Go(func() error {
110-
return apiService.Shutdown(ctx)
113+
return apiService.Shutdown(shutdownCtx)
111114
})
112115

113116
if err := g.Wait(); err != nil {

server/lib/recorder/ffmpeg.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ func (fr *FFmpegRecorder) Start(ctx context.Context) error {
153153
go fr.waitForCommand(ctx)
154154

155155
// Check for startup errors before returning
156-
if err := waitForChan(ctx, 500*time.Millisecond, fr.exited); err == nil {
156+
if err := waitForChan(ctx, 250*time.Millisecond, fr.exited); err == nil {
157157
fr.mu.Lock()
158158
defer fr.mu.Unlock()
159159
return fmt.Errorf("failed to start ffmpeg process: %w", fr.ffmpegErr)
@@ -165,16 +165,17 @@ func (fr *FFmpegRecorder) Start(ctx context.Context) error {
165165
// Stop gracefully stops the recording using a multi-phase shutdown process.
166166
func (fr *FFmpegRecorder) Stop(ctx context.Context) error {
167167
return fr.shutdownInPhases(ctx, []shutdownPhase{
168-
{"interrupt", []syscall.Signal{syscall.SIGCONT, syscall.SIGINT}, 5 * time.Second, "graceful stop"},
169-
{"terminate", []syscall.Signal{syscall.SIGTERM}, 2 * time.Second, "forceful termination"},
170-
{"kill", []syscall.Signal{syscall.SIGKILL}, 1 * time.Second, "immediate kill"},
168+
{"wake_and_interrupt", []syscall.Signal{syscall.SIGCONT, syscall.SIGINT}, 5 * time.Second, "graceful stop"},
169+
{"retry_interrupt", []syscall.Signal{syscall.SIGINT}, 3 * time.Second, "retry graceful stop"},
170+
{"terminate", []syscall.Signal{syscall.SIGTERM}, 250 * time.Millisecond, "forceful termination"},
171+
{"kill", []syscall.Signal{syscall.SIGKILL}, 100 * time.Millisecond, "immediate kill"},
171172
})
172173
}
173174

174175
// ForceStop immediately terminates the recording process.
175176
func (fr *FFmpegRecorder) ForceStop(ctx context.Context) error {
176177
return fr.shutdownInPhases(ctx, []shutdownPhase{
177-
{"kill", []syscall.Signal{syscall.SIGKILL}, 1 * time.Second, "immediate kill"},
178+
{"kill", []syscall.Signal{syscall.SIGKILL}, 100 * time.Millisecond, "immediate kill"},
178179
})
179180
}
180181

@@ -321,6 +322,7 @@ func (fr *FFmpegRecorder) shutdownInPhases(ctx context.Context, phases []shutdow
321322

322323
pgid := -cmd.Process.Pid // negative PGID targets the whole group
323324
for _, phase := range phases {
325+
phaseStartTime := time.Now()
324326
// short circuit: the process exited before this phase started.
325327
select {
326328
case <-done:
@@ -331,12 +333,16 @@ func (fr *FFmpegRecorder) shutdownInPhases(ctx context.Context, phases []shutdow
331333
log.Info("ffmpeg shutdown phase", "phase", phase.name, "desc", phase.desc)
332334

333335
// Send the phase's signals in order.
334-
for _, sig := range phase.signals {
336+
for idx, sig := range phase.signals {
335337
_ = syscall.Kill(pgid, sig) // ignore error; process may have gone away
338+
// arbitrary delay between signals, but not after the last signal
339+
if idx < len(phase.signals)-1 {
340+
time.Sleep(100 * time.Millisecond)
341+
}
336342
}
337343

338344
// Wait for exit or timeout
339-
if err := waitForChan(ctx, phase.timeout, done); err == nil {
345+
if err := waitForChan(ctx, phase.timeout-time.Since(phaseStartTime), done); err == nil {
340346
log.Info("ffmpeg shutdown successful", "phase", phase.name)
341347
return nil
342348
}

shared/build-server.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env bash
2+
3+
# build-server.sh
4+
# -------------------------
5+
# Usage (source or execute):
6+
# build-recording-server.sh [dest-dir] [goos] [goarch]
7+
#
8+
# dest-dir (optional) Directory to place the resulting binary. Defaults to ./bin
9+
# goos (optional) Target GOOS for cross-compilation. Defaults to linux
10+
# goarch (optional) Target GOARCH for cross-compilation. Defaults to amd64
11+
#
12+
# Examples
13+
# source ../../shared/build-recording-server.sh # → ./bin, linux/amd64
14+
# ../../shared/build-recording-server.sh ./bin arm64 # → linux/arm64
15+
# ../../shared/build-recording-server.sh ./out darwin arm64 # → darwin/arm64
16+
set -euo pipefail
17+
18+
DEST_DIR="${1:-./bin}"
19+
# Optional os/arch parameters
20+
TARGET_OS="${2:-linux}"
21+
TARGET_ARCH="${3:-amd64}"
22+
23+
# Resolve repo root as the parent directory of this script's directory
24+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
26+
27+
# 1. Build the binary in the server module
28+
pushd "$REPO_ROOT/server" >/dev/null
29+
GOOS="$TARGET_OS" GOARCH="$TARGET_ARCH" CGO_ENABLED=0 make build
30+
popd >/dev/null
31+
32+
# 2. Copy to destination
33+
mkdir -p "$DEST_DIR"
34+
cp "$REPO_ROOT/server/bin/api" "$DEST_DIR/kernel-images-api"
35+
36+
echo "✅ kernel-images-api binary copied to $DEST_DIR/kernel-images-api"

unikernels/unikraft-cu/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bin/

unikernels/unikraft-cu/Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ RUN apt-get update && \
5050
sudo \
5151
mutter \
5252
x11vnc \
53+
# Recording tools
54+
ffmpeg \
5355
# Python/pyenv reqs
5456
build-essential \
5557
libssl-dev \
@@ -150,5 +152,9 @@ COPY --from=xorg-deps /usr/local/lib/xorg/modules/input/neko_drv.so /usr/lib/xor
150152
COPY image-chromium/ /
151153
COPY ./wrapper.sh /wrapper.sh
152154

155+
# copy the kernel-images API binary built by build.sh
156+
COPY bin/kernel-images-api /usr/local/bin/kernel-images-api
157+
ENV WITH_KERNEL_IMAGES_API=false
158+
153159
ENTRYPOINT [ "/wrapper.sh" ]
154160

unikernels/unikraft-cu/build.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ if [ -z "$UKC_TOKEN" ] || [ -z "$UKC_METRO" ]; then
1010
fi
1111
source ../../shared/start-buildkit.sh
1212

13+
# Build the API binary
14+
source ../../shared/build-server.sh "$(pwd)/bin"
15+
1316
kraft pkg \
1417
--name index.unikraft.io/$image \
1518
--plat kraftcloud --arch x86_64 \

0 commit comments

Comments
 (0)