mirror of
https://github.com/nestriness/nestri.git
synced 2026-05-01 10:53:08 +03:00
feat: Init
This commit is contained in:
576
build/usr/bin/nestri-entry
Normal file
576
build/usr/bin/nestri-entry
Normal file
@@ -0,0 +1,576 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# ============================================================
|
||||
# nestri-entry
|
||||
#
|
||||
# Entry point that runs inside the bwrap sandbox.
|
||||
# Launched by nestri-runner.sh as the top-level process.
|
||||
#
|
||||
# Responsibilities:
|
||||
# 1. Start PipeWire (sandbox-local audio server)
|
||||
# 2. Start GStreamer pipeline (Wayland compositor + encoding + streaming)
|
||||
# 3. Launch muvm (microVM that runs the actual games)
|
||||
#
|
||||
# The GStreamer pipeline IS the Wayland compositor:
|
||||
# - waylanddisplaysrc creates $XDG_RUNTIME_DIR/wayland-1
|
||||
# - VM guest connects via wl-cross-domain-proxy (virtio-gpu)
|
||||
# - Video frames captured as DMA-BUF, encoded, streamed via QUIC
|
||||
# - Audio captured from PipeWire, encoded as Opus, muxed with video
|
||||
#
|
||||
# Environment variables (set by nestri-runner.sh):
|
||||
# NESTRI_WIDTH Stream width (default: 1920)
|
||||
# NESTRI_HEIGHT Stream height (default: 1080)
|
||||
# NESTRI_FPS Stream framerate (default: 60)
|
||||
# NESTRI_BITRATE Encoder bitrate in kbps (default: 8000)
|
||||
# NESTRI_RENDER_NODE GPU render node (default: /dev/dri/renderD128)
|
||||
# NESTRI_BROADCAST Stream broadcast name (default: live)
|
||||
# NESTRI_GPU GPU type: amd, intel, nvidia (default: amd)
|
||||
# NESTRI_CODEC Video codec: h264, h265, av1 (default: h264)
|
||||
# MICROVM_UID User ID inside sandbox/VM
|
||||
# MICROVM_GID Group ID inside sandbox/VM
|
||||
#
|
||||
# muvm-guest binary symlinks (in /opt/bin/):
|
||||
# muvm-remote — session runner (runs user command)
|
||||
# muvm-configure-network — network setup for VM
|
||||
# muvm-pwbridge — PipeWire bridge (host ↔ guest audio)
|
||||
# ============================================================
|
||||
|
||||
# ============================================================
|
||||
# Configuration (from environment or defaults)
|
||||
# ============================================================
|
||||
|
||||
NESTRI_WIDTH="${NESTRI_WIDTH:-1920}"
|
||||
NESTRI_HEIGHT="${NESTRI_HEIGHT:-1080}"
|
||||
NESTRI_FPS="${NESTRI_FPS:-60}"
|
||||
NESTRI_BITRATE="${NESTRI_BITRATE:-8000}"
|
||||
NESTRI_RENDER_NODE="${NESTRI_RENDER_NODE:-/dev/dri/renderD128}"
|
||||
NESTRI_BROADCAST="${NESTRI_BROADCAST:-live}"
|
||||
MICROVM_UID="${MICROVM_UID:-1000}"
|
||||
|
||||
# GPU and codec selection
|
||||
# GPU options: amd, intel, nvidia
|
||||
# Codec options: h264, h265, av1
|
||||
NESTRI_GPU="${NESTRI_GPU:-amd}"
|
||||
NESTRI_CODEC="${NESTRI_CODEC:-h264}"
|
||||
|
||||
# Setup XDG_RUNTIME_DIR
|
||||
mkdir -p "$XDG_RUNTIME_DIR"
|
||||
|
||||
# Track background PIDs for cleanup
|
||||
PIDS=()
|
||||
|
||||
cleanup() {
|
||||
echo "nestri-entry: shutting down..." >&2
|
||||
|
||||
# Kill all tracked background processes
|
||||
for pid in "${PIDS[@]}"; do
|
||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
kill -TERM "$pid" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
# Wait briefly for graceful shutdown
|
||||
sleep 0.5
|
||||
|
||||
# Force kill anything still alive
|
||||
for pid in "${PIDS[@]}"; do
|
||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
kill -KILL "$pid" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
# dbus-launch sets this separately
|
||||
if [ -n "${DBUS_SESSION_BUS_PID:-}" ]; then
|
||||
kill -TERM "$DBUS_SESSION_BUS_PID" 2>/dev/null
|
||||
fi
|
||||
|
||||
exit
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# ============================================================
|
||||
# 1. D-Bus Session Bus + PipeWire (sandbox-local audio)
|
||||
#
|
||||
# WirePlumber requires a D-Bus session bus — it's a hard
|
||||
# dependency. PipeWire itself can start without D-Bus, but
|
||||
# WirePlumber (the session manager that handles routing,
|
||||
# device enumeration, policy) will abort without one.
|
||||
#
|
||||
# Inside the VM, systemd provides the session bus via
|
||||
# nestri-session-bus.service. Here in bwrap, we have no
|
||||
# systemd, so we launch dbus-daemon manually.
|
||||
#
|
||||
# Audio flow:
|
||||
# Game (in VM) → PipeWire (guest) → muvm-pwbridge → PipeWire (here)
|
||||
# PipeWire (here) → pipewiresrc → GStreamer → Opus encode → QUIC
|
||||
# ============================================================
|
||||
|
||||
# --- D-Bus session bus ---
|
||||
echo "nestri-entry: starting dbus session bus..." >&2
|
||||
|
||||
# Create dbus directory
|
||||
mkdir -p "$XDG_RUNTIME_DIR/dbus"
|
||||
|
||||
# Launch a session bus and capture its address
|
||||
eval "$(dbus-launch --sh-syntax)"
|
||||
|
||||
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
|
||||
echo "nestri-entry: WARNING — dbus-launch failed, trying manual start..." >&2
|
||||
|
||||
DBUS_SOCKET="$XDG_RUNTIME_DIR/dbus/session-bus"
|
||||
dbus-daemon \
|
||||
--session \
|
||||
--address="unix:path=$DBUS_SOCKET" \
|
||||
--nofork \
|
||||
--print-address \
|
||||
&>/dev/null &
|
||||
DBUS_PID=$!
|
||||
PIDS+=($DBUS_PID)
|
||||
|
||||
# Wait for socket
|
||||
for i in $(seq 1 30); do
|
||||
[ -S "$DBUS_SOCKET" ] && break
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if [ -S "$DBUS_SOCKET" ]; then
|
||||
export DBUS_SESSION_BUS_ADDRESS="unix:path=$DBUS_SOCKET"
|
||||
echo "nestri-entry: dbus ready at $DBUS_SOCKET" >&2
|
||||
else
|
||||
echo "nestri-entry: WARNING — dbus-daemon failed to start" >&2
|
||||
echo "nestri-entry: wireplumber will likely fail" >&2
|
||||
fi
|
||||
else
|
||||
# dbus-launch succeeded, track its PID for cleanup
|
||||
PIDS+=("${DBUS_SESSION_BUS_PID:-}")
|
||||
echo "nestri-entry: dbus ready (via dbus-launch) PID=${DBUS_SESSION_BUS_PID:-}" >&2
|
||||
fi
|
||||
|
||||
export DBUS_SESSION_BUS_ADDRESS
|
||||
|
||||
# --- PipeWire ---
|
||||
echo "nestri-entry: starting pipewire..." >&2
|
||||
pipewire &
|
||||
PIDS+=($!)
|
||||
|
||||
# Wait for PipeWire socket
|
||||
for i in $(seq 1 50); do
|
||||
[ -S "$XDG_RUNTIME_DIR/pipewire-0" ] && break
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if [ -S "$XDG_RUNTIME_DIR/pipewire-0" ]; then
|
||||
echo "nestri-entry: pipewire ready" >&2
|
||||
|
||||
# WirePlumber needs both PipeWire AND D-Bus
|
||||
if [ -n "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
|
||||
wireplumber &
|
||||
PIDS+=($!)
|
||||
sleep 0.5
|
||||
echo "nestri-entry: wireplumber ready" >&2
|
||||
else
|
||||
echo "nestri-entry: WARNING — skipping wireplumber (no dbus)" >&2
|
||||
echo "nestri-entry: audio routing may not work correctly" >&2
|
||||
fi
|
||||
else
|
||||
echo "nestri-entry: WARNING — pipewire not ready, continuing without audio" >&2
|
||||
fi
|
||||
|
||||
# Wait for WirePlumber to register the loopback node
|
||||
for i in $(seq 1 50); do
|
||||
if pw-cli ls Node 2>/dev/null | grep -q "nestri-source"; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if pw-cli ls Node 2>/dev/null | grep -q "nestri-source"; then
|
||||
echo "nestri-entry: wireplumber ready, loopback node available" >&2
|
||||
else
|
||||
echo "nestri-entry: WARNING — loopback node not found after 5s" >&2
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 2. GStreamer Pipeline — Encoder Detection & Configuration
|
||||
#
|
||||
# VAAPI encoders come in two variants:
|
||||
# - vaXenc — standard encoder
|
||||
# - vaXlpenc — low-power variant (sometimes the only option)
|
||||
#
|
||||
# We probe for available encoders and use what's present.
|
||||
# ============================================================
|
||||
|
||||
# Check if a GStreamer element exists
|
||||
gst_element_exists() {
|
||||
gst-inspect-1.0 "$1" &>/dev/null
|
||||
}
|
||||
|
||||
# Find the best available VAAPI encoder for a codec
|
||||
find_vaapi_encoder() {
|
||||
local codec="$1"
|
||||
local standard_enc="va${codec}enc"
|
||||
local lowpower_enc="va${codec}lpenc"
|
||||
|
||||
# Prefer standard encoder, fall back to low-power
|
||||
if gst_element_exists "$standard_enc"; then
|
||||
echo "$standard_enc"
|
||||
elif gst_element_exists "$lowpower_enc"; then
|
||||
echo "$lowpower_enc"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 2. GStreamer Pipeline — Encoding Configuration
|
||||
#
|
||||
# GPU-specific encoding paths:
|
||||
#
|
||||
# NVIDIA (NVENC):
|
||||
# Memory: video/x-raw(memory:CUDAMemory)
|
||||
# H.264: nvh264enc (p1 preset, ultra-low-latency tune)
|
||||
# H.265: nvh265enc (p1 preset, ultra-low-latency tune)
|
||||
# AV1: nvav1enc (p1 preset, ultra-low-latency tune)
|
||||
#
|
||||
# Modern NVENC presets (p1-p7):
|
||||
# p1 = fastest (lowest quality)
|
||||
# p7 = slowest (highest quality)
|
||||
# Tunes: ultra-low-latency, low-latency, high-quality
|
||||
#
|
||||
#
|
||||
# Intel (QSV):
|
||||
# Memory: video/x-raw(memory:DMABuf) → vapostproc → video/x-raw(memory:VAMemory)
|
||||
# H.264: qsvh264enc
|
||||
# H.265: qsvh265enc
|
||||
# AV1: qsvav1enc
|
||||
#
|
||||
# target-usage is 7 for all (speed) with low-latency set to true
|
||||
#
|
||||
#
|
||||
# AMD/Intel/.. (VAAPI):
|
||||
# Memory: video/x-raw(memory:DMABuf) → vapostproc → video/x-raw(memory:VAMemory)
|
||||
# H.264: vah264enc or vah264lpenc (low-power variant)
|
||||
# H.265: vah265enc or vah265lpenc (low-power variant)
|
||||
# AV1: vaav1enc or vaav1lpenc (low-power variant)
|
||||
#
|
||||
# target-usage: 1 (quality) to 7 (speed)
|
||||
# For streaming, we use 7 (maximum speed/minimum latency)
|
||||
#
|
||||
# The vapostproc element converts DMA-BUF to VA-API memory and
|
||||
# transforms to NV12 format required by the VA-API encoders.
|
||||
# ============================================================
|
||||
|
||||
# Calculate GOP size (keyframe interval)
|
||||
# For low latency, we use 1-2 seconds worth of frames
|
||||
GOP_SIZE=$((NESTRI_FPS * 2))
|
||||
|
||||
setup_vaapi_encoder() {
|
||||
# AMD/Intel VAAPI path — uses DMA-BUF → VA-API memory
|
||||
# vapostproc handles colorspace conversion to NV12
|
||||
GST_MEM_CAPS="video/x-raw(memory:DMABuf),width=${NESTRI_WIDTH},height=${NESTRI_HEIGHT},framerate=${NESTRI_FPS}/1 ! vapostproc ! video/x-raw(memory:VAMemory),format=NV12"
|
||||
|
||||
case "$NESTRI_CODEC" in
|
||||
av1)
|
||||
VAAPI_ENC=$(find_vaapi_encoder "av1")
|
||||
if [ -z "$VAAPI_ENC" ]; then
|
||||
echo "nestri-entry: FATAL — no VAAPI AV1 encoder found" >&2
|
||||
echo "nestri-entry: tried: vaav1enc, vaav1penc" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "nestri-entry: using VAAPI encoder: $VAAPI_ENC" >&2
|
||||
GST_ENC="$VAAPI_ENC"
|
||||
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
|
||||
GST_ENC="$GST_ENC key-int-max=${GOP_SIZE}"
|
||||
GST_ENC="$GST_ENC target-usage=7"
|
||||
;;
|
||||
h265)
|
||||
VAAPI_ENC=$(find_vaapi_encoder "h265")
|
||||
if [ -z "$VAAPI_ENC" ]; then
|
||||
echo "nestri-entry: FATAL — no VAAPI H.265 encoder found" >&2
|
||||
echo "nestri-entry: tried: vah265enc, vah265lpenc" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "nestri-entry: using VAAPI encoder: $VAAPI_ENC" >&2
|
||||
GST_ENC="$VAAPI_ENC"
|
||||
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
|
||||
GST_ENC="$GST_ENC key-int-max=${GOP_SIZE}"
|
||||
GST_ENC="$GST_ENC target-usage=7"
|
||||
;;
|
||||
h264|*)
|
||||
VAAPI_ENC=$(find_vaapi_encoder "h264")
|
||||
if [ -z "$VAAPI_ENC" ]; then
|
||||
echo "nestri-entry: FATAL — no VAAPI H.264 encoder found" >&2
|
||||
echo "nestri-entry: tried: vah264enc, vah264lpenc" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "nestri-entry: using VAAPI encoder: $VAAPI_ENC" >&2
|
||||
GST_ENC="$VAAPI_ENC"
|
||||
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
|
||||
GST_ENC="$GST_ENC key-int-max=${GOP_SIZE}"
|
||||
GST_ENC="$GST_ENC target-usage=7"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Select memory caps and encoder based on GPU type
|
||||
case "$NESTRI_GPU" in
|
||||
nvidia)
|
||||
# NVIDIA NVENC path — uses CUDA memory
|
||||
# Modern presets: p1 (fastest) to p7 (highest quality)
|
||||
# Tunes: ultra-low-latency, low-latency, high-quality
|
||||
GST_MEM_CAPS="video/x-raw(memory:CUDAMemory),width=${NESTRI_WIDTH},height=${NESTRI_HEIGHT},framerate=${NESTRI_FPS}/1"
|
||||
|
||||
case "$NESTRI_CODEC" in
|
||||
av1)
|
||||
GST_ENC="nvav1enc"
|
||||
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
|
||||
GST_ENC="$GST_ENC gop-size=${GOP_SIZE}"
|
||||
GST_ENC="$GST_ENC preset=p1"
|
||||
GST_ENC="$GST_ENC tune=ultra-low-latency"
|
||||
GST_ENC="$GST_ENC multi-pass=disabled"
|
||||
GST_ENC="$GST_ENC zerolatency=true"
|
||||
;;
|
||||
h265)
|
||||
GST_ENC="nvh265enc"
|
||||
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
|
||||
GST_ENC="$GST_ENC gop-size=${GOP_SIZE}"
|
||||
GST_ENC="$GST_ENC preset=p1"
|
||||
GST_ENC="$GST_ENC tune=ultra-low-latency"
|
||||
GST_ENC="$GST_ENC multi-pass=disabled"
|
||||
GST_ENC="$GST_ENC zerolatency=true"
|
||||
;;
|
||||
h264|*)
|
||||
GST_ENC="nvh264enc"
|
||||
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
|
||||
GST_ENC="$GST_ENC gop-size=${GOP_SIZE}"
|
||||
GST_ENC="$GST_ENC preset=p1"
|
||||
GST_ENC="$GST_ENC tune=ultra-low-latency"
|
||||
GST_ENC="$GST_ENC multi-pass=disabled"
|
||||
GST_ENC="$GST_ENC zerolatency=true"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
|
||||
intel)
|
||||
# Check for QSV element plugin
|
||||
if gst_element_exists "qsv"; then
|
||||
# Intel QSV path — uses DMA-BUF → VA-API memory
|
||||
# vapostproc handles colorspace conversion to NV12
|
||||
GST_MEM_CAPS="video/x-raw(memory:DMABuf),width=${NESTRI_WIDTH},height=${NESTRI_HEIGHT},framerate=${NESTRI_FPS}/1 ! vapostproc ! video/x-raw(memory:VAMemory),format=NV12"
|
||||
|
||||
case "$NESTRI_CODEC" in
|
||||
av1)
|
||||
GST_ENC="qsvav1enc"
|
||||
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
|
||||
GST_ENC="$GST_ENC gop-size=${GOP_SIZE}"
|
||||
GST_ENC="$GST_ENC target-usage=7"
|
||||
GST_ENC="$GST_ENC low-latency=true"
|
||||
;;
|
||||
h265)
|
||||
GST_ENC="qsvh265enc"
|
||||
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
|
||||
GST_ENC="$GST_ENC gop-size=${GOP_SIZE}"
|
||||
GST_ENC="$GST_ENC target-usage=7"
|
||||
GST_ENC="$GST_ENC low-latency=true"
|
||||
;;
|
||||
h264|*)
|
||||
GST_ENC="qsvh264enc"
|
||||
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
|
||||
GST_ENC="$GST_ENC gop-size=${GOP_SIZE}"
|
||||
GST_ENC="$GST_ENC target-usage=7"
|
||||
GST_ENC="$GST_ENC low-latency=true"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# Fallback to VAAPI if no QSV available
|
||||
setup_vaapi_encoder
|
||||
fi
|
||||
;;
|
||||
|
||||
amd|*)
|
||||
setup_vaapi_encoder
|
||||
;;
|
||||
esac
|
||||
|
||||
# ============================================================
|
||||
# 2. GStreamer Pipeline — Launch
|
||||
#
|
||||
# Pipeline structure:
|
||||
#
|
||||
# Video path:
|
||||
# waylanddisplaysrc (compositor + capture)
|
||||
# → queue (buffer management)
|
||||
# → [GPU-specific caps + encoder]
|
||||
# → [codec parser]
|
||||
#
|
||||
# Audio path:
|
||||
# pipewiresrc (capture from PipeWire)
|
||||
# → audioconvert/audiorate/audioresample
|
||||
# → opusenc (Opus encoding @ 128kbps)
|
||||
#
|
||||
# Output:
|
||||
# nestrisink → QUIC/iroh-moq streaming
|
||||
#
|
||||
# waylanddisplaysrc IS the Wayland compositor:
|
||||
# - Creates socket at $XDG_RUNTIME_DIR/wayland-1
|
||||
# - Captures client buffers as DMA-BUF (zero-copy)
|
||||
# - Input events arrive via nestrisink (bidirectional)
|
||||
# ============================================================
|
||||
|
||||
echo "nestri-entry: starting gstreamer pipeline..." >&2
|
||||
echo "nestri-entry: resolution: ${NESTRI_WIDTH}x${NESTRI_HEIGHT}@${NESTRI_FPS}fps" >&2
|
||||
echo "nestri-entry: bitrate: ${NESTRI_BITRATE}kbps" >&2
|
||||
echo "nestri-entry: gop-size: ${GOP_SIZE} frames" >&2
|
||||
echo "nestri-entry: render: ${NESTRI_RENDER_NODE}" >&2
|
||||
echo "nestri-entry: gpu: ${NESTRI_GPU}" >&2
|
||||
echo "nestri-entry: codec: ${NESTRI_CODEC}" >&2
|
||||
echo "nestri-entry: encoder: ${GST_ENC%% *}" >&2
|
||||
echo "nestri-entry: broadcast: ${NESTRI_BROADCAST}" >&2
|
||||
|
||||
# Note: GST_MEM_CAPS and GST_ENC are intentionally unquoted
|
||||
# to allow bash word splitting for GStreamer's '!' element separators.
|
||||
|
||||
gst-launch-1.0 --gst-debug-no-color -e \
|
||||
nestrisink name=sink broadcast="$NESTRI_BROADCAST" \
|
||||
waylanddisplaysrc render-node="$NESTRI_RENDER_NODE" ! \
|
||||
queue max-size-buffers=2 max-size-time=0 max-size-bytes=0 ! \
|
||||
$GST_MEM_CAPS ! \
|
||||
$GST_ENC ! \
|
||||
sink. \
|
||||
pipewiresrc target-object="nestri-source" use-bufferpool=false do-timestamp=true ! \
|
||||
queue max-size-buffers=2 max-size-time=0 max-size-bytes=0 ! \
|
||||
"audio/x-raw,format=S16LE,channels=2,rate=48000" ! \
|
||||
audioconvert ! \
|
||||
audiorate ! \
|
||||
audioresample ! \
|
||||
opusenc bitrate=128000 frame-size=10 ! \
|
||||
sink. \
|
||||
>> /tmp/gstreamer.log 2>&1 &
|
||||
|
||||
GST_PID=$!
|
||||
PIDS+=($GST_PID)
|
||||
|
||||
# Wait for compositor socket
|
||||
for i in $(seq 1 100); do
|
||||
[ -S "$XDG_RUNTIME_DIR/wayland-1" ] && break
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if [ ! -S "$XDG_RUNTIME_DIR/wayland-1" ]; then
|
||||
echo "nestri-entry: FATAL — compositor socket not created" >&2
|
||||
echo "nestri-entry: check waylanddisplaysrc plugin and GPU access" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "nestri-entry: wayland compositor ready at $XDG_RUNTIME_DIR/wayland-1" >&2
|
||||
|
||||
# X_DISPLAY_NUM="12"
|
||||
|
||||
# X_SOCKET="/tmp/.X11-unix/X$X_DISPLAY_NUM"
|
||||
|
||||
# # Launch the XWayland satellite process to handle X11 clients in the VM.
|
||||
# xwayland-satellite ":$X_DISPLAY_NUM" >> /tmp/x11.log 2>&1 &
|
||||
# PIDS+=($!)
|
||||
|
||||
# # Wait for X11 socket
|
||||
# for i in $(seq 1 100); do
|
||||
# [ -S "$X_SOCKET" ] && break
|
||||
# sleep 0.1
|
||||
# done
|
||||
|
||||
# if [ ! -S "$X_SOCKET" ]; then
|
||||
# echo "nestri-entry: FATAL — X11 socket $X_SOCKET not created" >&2
|
||||
# echo "nestri-entry: check xwayland-satellite logs for startup errors" >&2
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
# echo "nestri-entry: X11 server ready at $X_SOCKET" >&2
|
||||
|
||||
# export DISPLAY=":$X_DISPLAY_NUM"
|
||||
# ============================================================
|
||||
# DNS Setup
|
||||
#
|
||||
# passt (muvm's network backend) reads /etc/resolv.conf from
|
||||
# the bwrap namespace to get DNS servers for the VM's DHCP.
|
||||
# The rootfs may have a stale or symlinked resolv.conf.
|
||||
# We overwrite it here with the real DNS from the host.
|
||||
#
|
||||
# nestri-init also tries to copy /run/nestri/resolv.conf into
|
||||
# the VM's /etc/resolv.conf, but that often fails because the
|
||||
# VM filesystem may be read-only. Fixing it here in the bwrap
|
||||
# layer ensures passt gets the right servers.
|
||||
# ============================================================
|
||||
|
||||
if [ -f /run/nestri/resolv.conf ]; then
|
||||
# Remove symlink if present, then write directly
|
||||
rm -f /etc/resolv.conf 2>/dev/null || true
|
||||
cp /run/nestri/resolv.conf /etc/resolv.conf 2>/dev/null || {
|
||||
# If /etc is somehow read-only, try writing in-place
|
||||
cat /run/nestri/resolv.conf > /etc/resolv.conf 2>/dev/null || true
|
||||
}
|
||||
echo "nestri-entry: DNS configured:" >&2
|
||||
cat /etc/resolv.conf >&2
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 3. muvm — MicroVM Launch
|
||||
#
|
||||
# The VM boots from this same filesystem via virtiofs.
|
||||
# nestri-init wraps systemd, setting up machine-id, dns, etc.
|
||||
#
|
||||
# Guest systemd services (started automatically):
|
||||
# nestri-network.service — muvm-configure-network (networking)
|
||||
# nestri-wayland-proxy.* — wl-cross-domain-proxy (socket-activated)
|
||||
# nestri-pwbridge.* — muvm-pwbridge (socket-activated audio)
|
||||
# nestri-session-bus.* — dbus-daemon (socket-activated)
|
||||
# nestri-remote.service — muvm-remote (runs user command)
|
||||
#
|
||||
# Cross-domain Wayland proxy:
|
||||
# The VM guest runs wl-cross-domain-proxy which connects to
|
||||
# the host's wayland-1 socket via virtio-gpu cross-domain context.
|
||||
# This allows the guest gamescope to render to the host compositor.
|
||||
#
|
||||
# Environment passed to guest:
|
||||
# WAYLAND_DISPLAY=wayland-1 — compositor socket name
|
||||
# XDG_RUNTIME_DIR=/run/vm-user — guest runtime directory
|
||||
# XDG_SESSION_TYPE=wayland — session type hint
|
||||
# SDL_VIDEODRIVER=wayland — force SDL wayland backend
|
||||
# SDL_AUDIO_DRIVER=pipewire — force SDL pipewire audio
|
||||
# NESTRI_* — stream configuration
|
||||
# ============================================================
|
||||
echo "nestri-entry: starting muvm..." >&2
|
||||
|
||||
# -e "SDL_VIDEODRIVER=wayland" \
|
||||
# -e "ELECTRON_OZONE_PLATFORM_HINT=wayland" \
|
||||
# -e "_JAVA_AWT_WM_NONREPARENTING=1" \
|
||||
# -e "QT_QPA_PLATFORM=wayland" \
|
||||
|
||||
muvm \
|
||||
--custom-init-cmdline "nestri-init /sbin/init --echo-target=console" \
|
||||
-e "container=muvm" \
|
||||
-e "XDG_SESSION_TYPE=wayland" \
|
||||
-e "XDG_RUNTIME_DIR=/run/vm-user" \
|
||||
-e "SDL_AUDIO_DRIVER=pipewire" \
|
||||
-e "WAYLAND_DISPLAY=wayland-1" \
|
||||
-e "VK_DRIVER_FILES=$VK_DRIVER_FILES" \
|
||||
-e "NESTRI_WIDTH=$NESTRI_WIDTH" \
|
||||
-e "NESTRI_HEIGHT=$NESTRI_HEIGHT" \
|
||||
-e "NESTRI_FPS=$NESTRI_FPS" \
|
||||
-e "MICROVM_UID=$MICROVM_UID" \
|
||||
-e "MICROVM_GID=${MICROVM_GID:-$MICROVM_UID}" \
|
||||
-e "BOOT_TIME_OFFSET=${BOOT_TIME_OFFSET:-}" \
|
||||
-i -t \
|
||||
"$@"
|
||||
|
||||
MUVM_EXIT=$?
|
||||
|
||||
echo "nestri-entry: muvm exited with code $MUVM_EXIT" >&2
|
||||
|
||||
exit $MUVM_EXIT
|
||||
# /usr/bin/steam -gamepadui -cef-force-gpu &
|
||||
# STEAM_PID=$!
|
||||
# PIDS+=($STEAM_PID)
|
||||
|
||||
# wait $STEAM_PID
|
||||
93
build/usr/bin/nestri-init
Normal file
93
build/usr/bin/nestri-init
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# ============================================================
|
||||
# 1. Save files from old /run
|
||||
# ============================================================
|
||||
_resolv=""
|
||||
_localtime=""
|
||||
[ -f /run/nestri/resolv.conf ] && _resolv=$(cat /run/nestri/resolv.conf)
|
||||
[ -f /run/nestri/localtime ] && _localtime=$(base64 /run/nestri/localtime 2>/dev/null) || true
|
||||
|
||||
# ============================================================
|
||||
# 2. Fresh tmpfs on /run
|
||||
# ============================================================
|
||||
mount -t tmpfs tmpfs /run -o nosuid,nodev,strictatime
|
||||
|
||||
# ============================================================
|
||||
# 3. Restore saved files
|
||||
# ============================================================
|
||||
if [ -n "$_resolv" ]; then
|
||||
echo "$_resolv" > /run/resolv.conf
|
||||
echo "$_resolv" > /etc/resolv.conf 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "$_localtime" ]; then
|
||||
echo "$_localtime" | base64 -d > /run/localtime
|
||||
echo "$_localtime" | base64 -d > /etc/localtime 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 4. Machine ID
|
||||
# ============================================================
|
||||
# FIXME(wanjohiryan): Use the same machine-id as the host? For Steam, i believe it does check machine id and stuff like that for SteamGuard
|
||||
if [ ! -s /etc/machine-id ]; then
|
||||
head -c 16 /dev/urandom | od -A n -t x1 | tr -d ' \n' > /etc/machine-id
|
||||
echo "" >> /etc/machine-id
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 5. Journald
|
||||
# ============================================================
|
||||
mkdir -p /run/systemd/journal /run/log/journal
|
||||
|
||||
mkdir -p /etc/systemd/system/systemd-journald.service.d
|
||||
cat > /etc/systemd/system/systemd-journald.service.d/override.conf << 'EOF'
|
||||
[Service]
|
||||
RuntimeDirectory=
|
||||
LogsDirectory=
|
||||
StateDirectory=
|
||||
ExecStartPre=/bin/mkdir -p /run/systemd/journal /run/log/journal
|
||||
EOF
|
||||
|
||||
# ============================================================
|
||||
# 6. Hide container FILE markers (ro rootfs)
|
||||
# But DO NOT strip the `container` env var!
|
||||
# systemd needs it for exit.target (clean shutdown).
|
||||
# ============================================================
|
||||
for marker in /.dockerenv /run/.containerenv; do
|
||||
if [ -e "$marker" ]; then
|
||||
mount --bind /dev/null "$marker" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# ============================================================
|
||||
# 8. Isolate X11 socket directory
|
||||
# ============================================================
|
||||
if [ -d /tmp/.X11-unix ]; then
|
||||
mount -t tmpfs tmpfs /tmp/.X11-unix -o noexec,nosuid,relatime
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 9. Remount /dev/shm as virtiofs with DAX
|
||||
# (required for cross-domain shared memory, DRI3 fences)
|
||||
# ============================================================
|
||||
umount /dev/shm
|
||||
mount -t virtiofs devshm /dev/shm -o noexec,nosuid,dax
|
||||
|
||||
# ============================================================
|
||||
# 10. Boot time offset
|
||||
# ============================================================
|
||||
if [ -n "$BOOT_TIME_OFFSET" ]; then
|
||||
if unshare --time true 2>/dev/null; then
|
||||
exec unshare --time -- bash -c '
|
||||
echo "monotonic '"$BOOT_TIME_OFFSET"'" > /proc/self/timens_offsets 2>/dev/null
|
||||
echo "boottime '"$BOOT_TIME_OFFSET"'" >> /proc/self/timens_offsets 2>/dev/null
|
||||
exec "$@"
|
||||
' -- "$@"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 11. Hand off to systemd
|
||||
# ============================================================
|
||||
exec "$@"
|
||||
BIN
build/usr/bin/steam-token.exe
Executable file
BIN
build/usr/bin/steam-token.exe
Executable file
Binary file not shown.
Reference in New Issue
Block a user