Compare commits

...
10 Commits
21 changed files with 555 additions and 183 deletions
+10 -14
View File
@@ -7,6 +7,7 @@ ARG TARGETARCH
ARG TARGETPLATFORM
ARG VERSION_ARG="0.0"
ARG VERSION_WSD="0.4.2"
ARG VERSION_CSTRUCT="4.7"
ARG VERSION_PASST="2026_07_28"
@@ -29,10 +30,6 @@ RUN <<EOF
procps \
ipcalc \
ethtool \
python3 \
python3-pip \
python3-msgpack \
python3-pysodium \
xz-utils \
iptables \
iproute2 \
@@ -43,30 +40,29 @@ RUN <<EOF
e2fsprogs \
diffutils \
qemu-utils \
websocketd \
iputils-ping \
inotify-tools \
ca-certificates \
netcat-openbsd \
qemu-system-x86
qemu-system-x86 \
python3 \
python3-pip \
python3-msgpack \
python3-pysodium
# Install Passt package
wget "https://github.com/qemus/passt/releases/download/v${VERSION_PASST}/passt_${VERSION_PASST}_${TARGETARCH}.deb" -O /tmp/passt.deb -q --timeout=10
dpkg -i /tmp/passt.deb
# Install Websocketd package
wget "https://github.com/qemus/websocketd/releases/download/v${VERSION_WSD}/websocketd-${VERSION_WSD}_${TARGETARCH}.deb" -O /tmp/wsd.deb -q --timeout=10
dpkg -i /tmp/wsd.deb
apt-get clean
# Install Python dependencies
pip3 install --no-cache-dir --break-system-packages --root-user-action=ignore "dissect.cstruct==$VERSION_CSTRUCT"
# Configure QEMU
mkdir -p /etc/qemu
echo "allow br0" > /etc/qemu/bridge.conf
# Configure nginx
unlink /etc/nginx/sites-enabled/default
sed -i 's/^worker_processes.*/worker_processes 1;/' /etc/nginx/nginx.conf
# Set version file
echo "$VERSION_ARG" > /etc/version
+2 -2
View File
@@ -68,8 +68,8 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
- Docker or Podman on a Linux host with KVM support.
- Docker Desktop or Podman (Desktop) on Windows 11 with nested virtualization enabled.
- At least 2 GB of available RAM.
- At least 32 GB of free disk space.
- At least 1 GB of available RAM.
- At least 16 GB of free disk space.
> [!NOTE]
> Docker Desktop on Linux, macOS, and Windows 10 does not currently provide KVM access to containers and is therefore not supported.
+4
View File
@@ -15,12 +15,16 @@ file="/run/shm/dsm.url"
address="/run/shm/qemu.ip"
gateway="/run/shm/qemu.gw"
# dsm.url is written only after the guest agent reports both the DSM
# address and its configured HTTP port.
[ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 0
location=$(<"$file")
if ! curl -m 20 -ILfSs "http://$location/" > /dev/null; then
# In DHCP mode the firewall must allow the container address; with port
# forwarding it must allow the internal gateway used to reach the guest.
if enabled "$DHCP"; then
ip=$(<"$address")
echo "Failed to reach DSM at http://$location"
+13 -2
View File
@@ -6,6 +6,8 @@ DEV_OPTS=""
configureProcessor() {
# Expose one thread per core in a single socket; DSM licensing and topology
# reporting are more predictable with this fixed layout.
CPU_OPTS="-cpu $CPU_FLAGS"
CPU_OPTS+=" -smp $CPU_CORES,sockets=1,dies=1,cores=$CPU_CORES,threads=1"
@@ -29,6 +31,8 @@ configureMonitor() {
configureMachine() {
# Disable firmware and chipset features that Virtual DSM does not use and
# that can introduce extra devices or timing differences.
MAC_OPTS="-machine type=$MACHINE,smm=off,usb=off"
MAC_OPTS+=",vmport=off,dump-guest-core=off,hpet=off${KVM_OPTS}"
@@ -37,9 +41,12 @@ configureMachine() {
configureVirtioDevices() {
DEV_OPTS="-device virtio-balloon-pci,id=balloon0,bus=pcie.0,addr=0x4"
local bus
bus=$(getPciBus)
DEV_OPTS="-device virtio-balloon-pci,id=balloon0,bus=$bus,addr=0x4"
DEV_OPTS+=" -object rng-random,id=objrng0,filename=/dev/urandom"
DEV_OPTS+=" -device virtio-rng-pci,rng=objrng0,id=rng0,bus=pcie.0,addr=0x1c"
DEV_OPTS+=" -device virtio-rng-pci,rng=objrng0,id=rng0,bus=$bus,addr=0x1c"
return 0
}
@@ -47,11 +54,15 @@ configureVirtioDevices() {
buildArguments() {
ARGS="$DEF_OPTS $CPU_OPTS $RAM_OPTS $MAC_OPTS $DISPLAY_OPTS $MON_OPTS $SERIAL_OPTS $NET_OPTS $DISK_OPTS $DEV_OPTS $ARGUMENTS"
# Collapse whitespace after optional argument groups are assembled so empty
# features cannot leave malformed spacing in the final QEMU command.
ARGS=$(echo "$ARGS" | sed 's/\t/ /g' | tr -s ' ')
return 0
}
finalizeMemory
configureMemory
configureMonitor
configureMachine
+26 -2
View File
@@ -25,6 +25,8 @@ DISK_ROTATION=$(strip "$DISK_ROTATION")
BOOT="$STORAGE/$BASE.boot.img"
SYSTEM="$STORAGE/$BASE.system.img"
# The boot and system images are installation artifacts, not optional data
# disks, and must exist before any user storage is attached.
[ ! -s "$BOOT" ] && error "Virtual DSM boot-image does not exist ($BOOT)" && exit 81
[ ! -s "$SYSTEM" ] && error "Virtual DSM system-image does not exist ($SYSTEM)" && exit 82
@@ -127,6 +129,8 @@ allocateRaw() {
return $?
fi
# Prefer real allocation, retry with zero-range allocation where supported,
# and fall back to a sparse file when the host filesystem rejects both.
fallocate -l "$dataSize" "$diskFile" &>/dev/null && return 0
fallocate -l -x "$dataSize" "$diskFile" && return 0
truncate -s "$dataSize" "$diskFile" || return 1
@@ -159,6 +163,8 @@ normalizeSize() {
local free dataSize
local spare=1073741824
# Dynamic sizes are resolved once from current free space. max reserves one
# GiB for host metadata and container activity; half uses half the space.
if [[ "${diskSpace,,}" == "max" || "${diskSpace,,}" == "half" ]]; then
free=$(df --output=avail -B 1 "$dir" | tail -n 1)
@@ -421,6 +427,8 @@ convertDisk() {
if [[ "$destinationFmt" == "raw" ]]; then
if ! disabled "$ALLOCATE"; then
# qemu-img may leave converted raw output sparse despite requested
# preallocation, so allocate its final length explicitly afterward.
# Work around qemu-img bug
if ! currentSize=$(stat -c%s "$tmpFile"); then
error "Failed to determine converted image size: $tmpFile"
@@ -435,6 +443,8 @@ convertDisk() {
fi
fi
# Publish the converted image before deleting the original so a failed
# conversion or rename never destroys the only usable disk.
if ! mv "$tmpFile" "$destinationFile"; then
error "Failed to move converted $diskDesc image to $destinationFile."
exit 79
@@ -479,6 +489,8 @@ checkFS () {
warn "the filesystem of $base is FUSE, this extra layer will negatively affect performance!"
fi
# Filesystems without O_DIRECT support require threaded I/O and writeback
# caching; native AIO with cache=none would fail at runtime.
if ! supportsDirect "$fs"; then
warn "the filesystem of $base is $fs, which does not support O_DIRECT mode, adjusting settings..."
fi
@@ -506,9 +518,9 @@ createDevice () {
local diskCache="$7"
local diskSerial="$8"
local diskSectors="$9"
local bus="${PCI_BUS:-pcie.0}"
[[ -z "${PCI_BUS:-}" && ( "${MACHINE,,}" == pc || "${MACHINE,,}" == pc-i440fx* ) ]] && bus="pci.0"
local bus
bus=$(getPciBus)
local options=""
[ -n "$DISK_OPTIONS" ] && options=",${DISK_OPTIONS#,}"
@@ -559,6 +571,8 @@ finishDisks () {
case "${DISK_TYPE,,}" in
"blk" | "scsi" | "virtio-blk" | "virtio-scsi" )
# VirtIO block and SCSI devices share one dedicated I/O thread, which
# must be declared exactly once regardless of disk count.
[[ "$DISK_OPTS" != *" -object iothread,id=io2"* ]] && DISK_OPTS+=" -object iothread,id=io2" ;;
esac
@@ -613,6 +627,8 @@ addDisk () {
previousExt=$(fmt2ext "$previousFmt")
# Treat a disk in the other supported format as the same logical disk and
# convert it automatically instead of creating an empty replacement.
if [ -f "$diskBase.$previousExt" ] &&
[ -s "$diskBase.$previousExt" ]; then
convertDisk "$diskBase.$previousExt" "$previousFmt" "$diskFile" "$diskFmt" "$diskBase" "$diskDesc" "$fs" || exit $?
@@ -645,6 +661,8 @@ addDisk () {
fi
# Sparse disks can promise more guest capacity than the host can currently
# satisfy, so report the future shortfall without blocking startup.
if [ -f "$diskFile" ] && disabled "$ALLOCATE"; then
currentSize=$(getSize "$diskFile") || exit 73
@@ -699,6 +717,8 @@ addDevice () {
[ -z "$diskDev" ] && return 0
[ ! -b "$diskDev" ] && error "Device $diskDev cannot be found! Please add it to the 'devices' section of your compose file." && exit 55
# DSM may reject whole-disk passthrough when QEMU is given explicit sector
# geometry; partitions need it to preserve non-512-byte host geometry.
# Only detect and apply sector sizes for partitions, not whole disks.
# Whole disk passthrough with explicit sector sizes causes DSM not to recognize the disk.
if [[ "$devType" == "part" ]]; then
@@ -793,6 +813,8 @@ else
DISK_ALLOC="preallocation=falloc"
fi
# Reserve the first two boot indexes and PCI addresses for the managed boot
# and system images; user disks begin at index 3.
DISK_OPTS+=$(createDevice "$BOOT" "$DISK_TYPE" "1" "0xa" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
DISK_OPTS+=$(createDevice "$SYSTEM" "$DISK_TYPE" "2" "0xb" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
@@ -832,6 +854,8 @@ DISK_DEVICES=( "$DEVICE" "$DEVICE2" "$DEVICE3" "$DEVICE4" )
DISK_INDEXES=( "3" "4" "5" "6" )
DISK_ADDRESSES=( "0xc" "0xd" "0xe" "0xf" )
# A passed-through block device takes precedence over the image-file slot
# with the same number.
for i in "${!DISK_FILES[@]}"; do
if [ -n "${DISK_DEVICES[i]}" ]; then
+8
View File
@@ -17,8 +17,12 @@ RENDERNODE=$(strip "$RENDERNODE")
CPU_VENDOR=$(lscpu | awk '/Vendor ID/{print $3}')
# The accelerated Intel render-node path is restricted to x86 Intel hosts;
# other platforms retain the normal QEMU display backend.
if ! enabled "$GPU" || isAmdCpu || [[ "$ARCH" != "amd64" ]]; then
# A disabled frontend also removes the emulated VGA device to keep the guest
# hardware layout headless.
[[ "${DISPLAY,,}" == "none" ]] && VGA="none"
if enabled "$LOSSY" && [[ "${DISPLAY,,}" == vnc=* ]]; then
@@ -40,6 +44,8 @@ DISPLAY_OPTS+=" -vga $VGA"
[ ! -d /dev/dri ] && mkdir -m 755 /dev/dri
# Extract the card number from the render node
# Linux renderD128 corresponds to card0; derive both device minors because
# container device bindings may expose only the render node.
CARD_NUMBER=$(echo "$RENDERNODE" | grep -oP '(?<=renderD)\d+')
CARD_DEVICE="/dev/dri/card$((CARD_NUMBER - 128))"
@@ -59,6 +65,8 @@ if [ ! -c "$RENDERNODE" ] || [ ! -r "$RENDERNODE" ] || [ ! -w "$RENDERNODE" ]; t
warn "render device '${RENDERNODE}' is unavailable or inaccessible."
fi
# Install acceleration packages lazily so non-GPU deployments keep the base
# image small and do not require OpenGL modules.
addPackage "xserver-xorg-video-intel" "Intel GPU drivers"
addPackage "qemu-system-modules-opengl" "OpenGL module"
+2 -2
View File
@@ -9,7 +9,8 @@ cd /run
. start.sh # Startup hook
. utils.sh # Load functions
. reset.sh # Initialize system
. init.sh # Initialize system
. memory.sh # Check memory
. server.sh # Start webserver
. install.sh # Run installation
. disk.sh # Initialize disks
@@ -18,7 +19,6 @@ cd /run
. proc.sh # Initialize processor
. serial.sh # Initialize serialport
. power.sh # Configure shutdown
. memory.sh # Check available memory
. config.sh # Configure arguments
. finish.sh # Finish initialization
+3
View File
@@ -5,4 +5,7 @@ if enabled "$DEBUG"; then
printf "QEMU arguments:\n\n %s\n\n" "${ARGS// -/$'\n -'}"
fi
# Must always remain the very last command
enableTrap
return 0
+37 -96
View File
@@ -43,6 +43,8 @@ detectRootless() {
local uid_map
# A full identity UID map indicates a rootful container; any remapping is
# treated as rootless even though the process itself runs as UID 0.
uid_map=$(awk '{$1=$1; print}' /proc/self/uid_map 2>/dev/null || true)
if [[ "$uid_map" == "0 0 4294967295" ]]; then
@@ -66,6 +68,8 @@ checkPrivileged() {
last_cap=$(cat /proc/sys/kernel/cap_last_cap)
# Calculate the maximum capability value
# Compare the bounding set with every capability supported by this kernel;
# checking only a few known capabilities would misclassify newer kernels.
local max_cap=$(((1 << (last_cap + 1)) - 1))
if [ "$cap_bnd" -eq "$max_cap" ]; then
@@ -75,15 +79,15 @@ checkPrivileged() {
return 0
}
normalizeCpuCores() {
checkCores() {
CPU_CORES=$(strip "$CPU_CORES")
[ -z "$CPU_CORES" ] && CPU_CORES=2
[[ "${CPU_CORES,,}" == "max" ]] && CPU_CORES="$CORES"
[[ "${CPU_CORES,,}" == "half" ]] && CPU_CORES=$(( CORES / 2 ))
[ -z "${CPU_CORES##*[!0-9]*}" ] && error "Invalid amount of CPU_CORES: $CPU_CORES" && exit 15
[ "$CPU_CORES" -lt "1" ] && CPU_CORES=1
if [ "$CPU_CORES" -gt "$CORES" ]; then
warn "The amount for CPU_CORES (${CPU_CORES}) exceeds the amount of logical cores available (${CORES}) and will be limited."
CPU_CORES="$CORES"
@@ -92,6 +96,20 @@ normalizeCpuCores() {
return 0
}
checkSockets() {
local lscpu_out
lscpu_out=$(lscpu 2>/dev/null || true)
if grep -qi "socket(s)" <<< "$lscpu_out"; then
SOCKETS=$(grep -m 1 -i 'socket(s)' <<< "$lscpu_out" | awk '{print $2}')
[ -z "${SOCKETS##*[!0-9]*}" ] && SOCKETS=1
[ "$SOCKETS" -lt "1" ] && SOCKETS=1
fi
return 0
}
checkStorage() {
# Check system
@@ -101,6 +119,8 @@ checkStorage() {
if [ ! -d "/dev/shm" ]; then
error "Directory /dev/shm not found!" && exit 14
else
# Keep runtime sockets and PID files on shared memory even on images where
# /run/shm is absent but /dev/shm is available.
[ ! -d "$QEMU_DIR" ] && ln -s /dev/shm "$QEMU_DIR"
fi
@@ -109,7 +129,9 @@ checkStorage() {
# Check folder
if [[ "${STORAGE,,}" != "/storage" ]]; then
mkdir -p "$STORAGE"
if ! mkdir -p -- "$STORAGE"; then
error "Cannot create storage folder ($STORAGE)!" && exit 13
fi
fi
if [ ! -d "$STORAGE" ]; then
@@ -125,11 +147,9 @@ checkStorage() {
return 0
}
checkFilesystem() {
checkHost() {
# Check filesystem
FS=$(stat -f -c %T "$STORAGE")
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then
DISK_IO="threads"
DISK_CACHE="writeback"
@@ -138,86 +158,6 @@ checkFilesystem() {
return 0
}
finiteMemoryLimit() {
local limit="$1"
local sentinel="4611686018427387904"
local i
[[ "$limit" =~ ^[0-9]+$ ]] || return 1
(( ${#limit} < ${#sentinel} )) && return 0
(( ${#limit} > ${#sentinel} )) && return 1
for (( i=0; i<${#sentinel}; i++ )); do
local left="${limit:i:1}"
local right="${sentinel:i:1}"
(( left < right )) && return 0
(( left > right )) && return 1
done
return 1
}
getMemoryInfo() {
local limit="" current=""
local host_total host_avail
host_total=$(free -b | awk '/^Mem:/ {print $2; exit}')
host_avail=$(free -b | awk '/^Mem:/ {print $7; exit}')
RAM_TOTAL="$host_total"
RAM_AVAIL="$host_avail"
if [ -r /sys/fs/cgroup/memory.max ] && [ -r /sys/fs/cgroup/memory.current ]; then
limit=$(< /sys/fs/cgroup/memory.max)
current=$(< /sys/fs/cgroup/memory.current)
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ] && [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then
limit=$(< /sys/fs/cgroup/memory/memory.limit_in_bytes)
current=$(< /sys/fs/cgroup/memory/memory.usage_in_bytes)
fi
if finiteMemoryLimit "$limit" && [[ "$current" =~ ^[0-9]+$ ]]; then
(( limit < RAM_TOTAL )) && RAM_TOTAL="$limit"
local available=$(( limit - current ))
(( available < 0 )) && available=0
(( available < RAM_AVAIL )) && RAM_AVAIL="$available"
fi
return 0
}
normalizeRamSize() {
# Read host and container memory limits.
getMemoryInfo
RAM_SPARE=500000000
RAM_MINIMUM=136314880
RAM_SIZE=$(strip "$RAM_SIZE")
RAM_SIZE="${RAM_SIZE// /}"
[ -z "$RAM_SIZE" ] && RAM_SIZE="2G"
if [[ "${RAM_SIZE,,}" != "max" && "${RAM_SIZE,,}" != "half" ]]; then
if [ -z "${RAM_SIZE//[0-9. ]}" ]; then
[ "${RAM_SIZE%%.*}" -lt "130" ] && RAM_SIZE="${RAM_SIZE}G" || RAM_SIZE="${RAM_SIZE}M"
fi
RAM_SIZE=$(echo "${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
numfmt --from=iec "$RAM_SIZE" &>/dev/null || { error "Invalid RAM_SIZE: $RAM_SIZE" && exit 16; }
wanted=$(numfmt --from=iec "$RAM_SIZE")
[ "$wanted" -lt "$RAM_MINIMUM" ] && error "RAM_SIZE is too low: $RAM_SIZE" && exit 16
fi
return 0
}
checkKvm() {
# Check KVM support
@@ -231,6 +171,8 @@ checkKvm() {
if disabled "$KVM"; then
warn "KVM acceleration is disabled, this will cause the machine to run about 10 times slower!"
else
# KVM accelerates only matching host and guest instruction sets; cross-
# architecture execution must fall back to software emulation.
if [[ "${ARCH,,}" != "$TARGET" ]]; then
KVM="N"
warn "your CPU architecture is ${ARCH^^} and cannot provide KVM acceleration for ${PLATFORM^^} instructions, so the machine will run about 10 times slower."
@@ -288,6 +230,7 @@ checkKvm() {
TZ=$(strip "$TZ")
STORAGE=$(strip "$STORAGE")
COUNTRY=$(strip "$COUNTRY")
MACHINE=$(strip "${MACHINE,,}")
DISK_SIZE=$(strip "$DISK_SIZE")
# Helper variables
@@ -315,22 +258,17 @@ SOCKETS=1
CPU=$(cpu)
SYS=$(uname -r)
ARCH=$(dpkg --print-architecture)
IFS=. read -r KERNEL MINOR _ <<< "$SYS"
CORES=$(grep -c '^processor' /proc/cpuinfo)
IFS=. read -r KERNEL MINOR _ <<< "$SYS"
if grep -qi "socket(s)" <<< "$(lscpu)"; then
SOCKETS=$(lscpu | grep -m 1 -i 'socket(s)' | awk '{print $2}')
[ -z "${SOCKETS##*[!0-9]*}" ] && SOCKETS=1
[ "$SOCKETS" -lt "1" ] && SOCKETS=1
fi
normalizeCpuCores
checkSockets
checkCores
checkStorage
checkFilesystem
normalizeRamSize
getMemoryInfo
# Print system info
SYS="${SYS/-generic/}"
FS=$(stat -f -c %T "$STORAGE")
FS="${FS/UNKNOWN //}"
FS="${FS/ext2\/ext3/ext4}"
FS=$(echo "$FS" | sed 's/[)(]//g')
@@ -342,8 +280,11 @@ TOTAL_MEM=$(formatBytes "$RAM_TOTAL" "up")
echo " CPU: ${CPU} | RAM: ${AVAIL_MEM/ GB/}/$TOTAL_MEM | DISK: $SPACE_GB (${FS}) | KERNEL: ${SYS}"
echo
checkHost
checkKvm
# Runtime state is intentionally discarded at each container start; persistent
# machine and disk identity lives under STORAGE instead.
# Cleanup files
rm -f "$QEMU_DIR"/dsm.url
rm -f "$QEMU_DIR"/{qemu.*,*.{pid,sock,pipe}}
+24
View File
@@ -3,6 +3,8 @@ set -Eeuo pipefail
: "${URL:=""}" # URL of the PAT file to be downloaded.
# Persist the exact PAT base name so future starts reopen the matching boot,
# system, and cached installation files.
if [ -f "$STORAGE/dsm.ver" ]; then
BASE=$(<"$STORAGE/dsm.ver")
BASE="${BASE//[![:print:]]/}"
@@ -16,6 +18,8 @@ FN="boot.pat"
DIR=$(find / -maxdepth 1 -type d -iname "$FN" -print -quit)
[ ! -d "$DIR" ] && DIR=$(find "$STORAGE" -maxdepth 1 -type d -iname "$FN" -print -quit)
# A boot.pat directory bind represents already extracted boot and system
# images and therefore takes precedence over PAT file or URL discovery.
if [ -d "$DIR" ]; then
BASE="DSM_VirtualDSM" && URL="file://$DIR"
if [[ ! -s "$STORAGE/$BASE.boot.img" || ! -s "$STORAGE/$BASE.system.img" ]]; then
@@ -29,6 +33,8 @@ FILE=$(find / -maxdepth 1 -type f -iname "$FN" -print -quit)
URL=$(strip "$URL")
# Derive a filesystem-safe identity from the URL only when no local boot.pat
# source was supplied; preserve an existing system image identity if present.
if [ -n "$URL" ] && [ ! -s "$FILE" ] && [ ! -d "$DIR" ]; then
BASE=$(basename "$URL" .pat)
if [ ! -s "$STORAGE/$BASE.system.img" ]; then
@@ -42,6 +48,8 @@ if [ -n "$URL" ] && [ ! -s "$FILE" ] && [ ! -d "$DIR" ]; then
fi
fi
# A complete matching image pair is the installation marker; the cached PAT
# itself is optional after installation.
if [[ -s "$STORAGE/$BASE.boot.img" && -s "$STORAGE/$BASE.system.img" ]]; then
return 0 # Previous installation found
fi
@@ -55,6 +63,8 @@ DL_GLOBAL="https://global.synologydownload.com/download/DSM"
[[ "${URL,,}" == *"cndl.synology"* ]] && DL="$DL_CHINA"
[[ "${URL,,}" == *"global.synology"* ]] && DL="$DL_GLOBAL"
# Honor an explicitly selected Synology mirror first, otherwise choose the
# China or global endpoint from the detected country.
if [ -z "$DL" ]; then
[ -z "$COUNTRY" ] && setCountry
[ -z "$COUNTRY" ] && info "Warning: could not detect country to select mirror!"
@@ -98,6 +108,8 @@ if [[ "${FS,,}" == "fat"* || "${FS,,}" == "vfat"* || "${FS,,}" == "msdos"* ]]; t
error "Unable to install on $FS filesystems, please use a different filesystem for /storage." && exit 61
fi
# Extract beside storage on Unix filesystems to avoid container-space limits;
# use /tmp for filesystems that cannot safely host the installer workspace.
if [[ "${FS,,}" != "exfat"* && "${FS,,}" != "ntfs"* && "${FS,,}" != "unknown"* ]]; then
TMP="$STORAGE/tmp"
rm -rf "$TMP"
@@ -206,6 +218,8 @@ fi
SIZE=$(stat -c%s "$PAT")
# Full Virtual DSM PAT files are substantially larger than update packs;
# reject undersized inputs before attempting destructive image preparation.
if ((SIZE<250000000)); then
error "The specified PAT file is probably an update pack as it's too small." && exit 62
fi
@@ -213,6 +227,8 @@ fi
MSG="Extracting installation image..."
info "Install: $MSG" && html "$MSG"
# Newer PAT files are normal tar archives; older encrypted/proprietary forms
# require the bundled extractor as a compatibility fallback.
if { tar tf "$PAT"; } >/dev/null 2>&1; then
tar xpf "$PAT" -C "$TMP/."
@@ -231,6 +247,8 @@ fi
MSG="Preparing system partition..."
info "Install: $MSG" && html "$MSG"
# The PAT boot archive becomes the persistent QEMU boot disk after its
# companion system partition has been assembled.
BOOT=$(find "$TMP" -name "*.bin.zip" -print -quit)
[ -z "$BOOT" ] && error "The PAT file contains no boot image." && exit 67
[ ! -s "$BOOT" ] && error "The PAT boot image archive is empty." && exit 67
@@ -273,6 +291,8 @@ if ! fallocate -l "$SYSTEM_SIZE" "$SYSTEM" &>/dev/null; then
fi
fi
# Recreate Synology's expected DOS partition layout inside the fixed 10 GiB
# system image before populating the ext4 root partition.
PART="$TMP/partition.fdisk"
{
@@ -320,6 +340,8 @@ OFFSET="1048576" # 2048 * 512
NUMBLOCKS="2097152" # (16777216 * 512) / 4096
MSG="Installing system partition..."
# Build the ext4 filesystem directly from the extracted tree under fakeroot,
# preserving archive ownership without mounting a loop device.
fakeroot -- bash -c "set -Eeu;\
[ -s $HDP.txz ] && tar xpfJ $HDP.txz --absolute-names -C $MOUNT/;\
[ -s $IDB.txz ] && tar xpfJ $IDB.txz --absolute-names -C $INDEX_DB/;\
@@ -331,6 +353,8 @@ rm -rf "$MOUNT"
echo "$BASE" > "$STORAGE/dsm.ver"
setOwner "$STORAGE/dsm.ver" || warn "failed to set the owner for \"$STORAGE/dsm.ver\" !"
# Do not keep a second copy when the source PAT already lives in storage;
# downloaded or externally mounted sources are cached for later reuse.
if [[ "$URL" == "file://$STORAGE/$BASE.pat" ]]; then
rm -f "$PAT"
else
+152 -29
View File
@@ -1,78 +1,144 @@
#!/usr/bin/env bash
set -Eeuo pipefail
msg="Checking memory..."
enabled "$DEBUG" && echo "$msg"
normalizeMemory() {
local wanted
RAM_SPARE=500000000
RAM_MINIMUM="${RAM_MINIMUM:-1073741824}"
RAM_MINIMUM=$(strip "$RAM_MINIMUM")
RAM_MINIMUM="${RAM_MINIMUM// /}"
RAM_MINIMUM=$(echo "${RAM_MINIMUM^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
numfmt --from=iec "$RAM_MINIMUM" &>/dev/null || {
error "Invalid RAM_MINIMUM: $RAM_MINIMUM"
exit 16
}
RAM_MINIMUM=$(numfmt --from=iec "$RAM_MINIMUM")
RAM_SIZE=$(strip "$RAM_SIZE")
RAM_SIZE="${RAM_SIZE// /}"
[ -z "$RAM_SIZE" ] && RAM_SIZE="2G"
if [[ "${RAM_SIZE,,}" != "max" && "${RAM_SIZE,,}" != "half" ]]; then
# Bare values below 130 are interpreted as GiB for convenience; larger bare
# values are treated as MiB to preserve historical configurations.
if [ -z "${RAM_SIZE//[0-9. ]}" ]; then
[ "${RAM_SIZE%%.*}" -lt "130" ] && RAM_SIZE="${RAM_SIZE}G" || RAM_SIZE="${RAM_SIZE}M"
fi
RAM_SIZE=$(echo "${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
numfmt --from=iec "$RAM_SIZE" &>/dev/null || {
error "Invalid RAM_SIZE: $RAM_SIZE"
exit 16
}
wanted=$(numfmt --from=iec "$RAM_SIZE")
if [ "$wanted" -lt "$RAM_MINIMUM" ]; then
error "$(app) requires at least $(formatBytes "$RAM_MINIMUM") of RAM, but RAM_SIZE is set to $(formatBytes "$wanted")."
exit 16
fi
# QEMU requires a whole-number memory value, so convert decimal sizes to MiB.
if [[ "$RAM_SIZE" == *.* ]]; then
RAM_SIZE="$(( wanted / 1048576 ))M"
fi
fi
app() {
echo "Virtual DSM"
return 0
}
checkConfiguredMemory() {
local final="$1"
if disabled "$RAM_CHECK" || [[ "${RAM_SIZE,,}" == "max" || "${RAM_SIZE,,}" == "half" ]]; then
return 0
fi
local wanted msg avail_mem
local wanted avail_mem
wanted=$(numfmt --from=iec "$RAM_SIZE")
avail_mem=$(formatBytes "$RAM_AVAIL")
if (( (wanted + RAM_SPARE) > RAM_AVAIL )); then
msg="Your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is too high for the $avail_mem of free memory available,"
local msg="Your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is too high for the $avail_mem of free memory available,"
# ZFS ARC can release cached memory under pressure, so this free-memory
# heuristic remains informational instead of rewriting RAM_SIZE.
if [[ "${FS,,}" == "zfs" ]]; then
info "$msg but since ZFS is active this will be ignored."
enabled "$final" && info "$msg but since ZFS is active this will be ignored."
else
RAM_SIZE="max"
warn "$msg it will automatically be adjusted to a lower amount."
RAM_WARNING="$msg it will automatically be adjusted to a lower amount."
fi
else
if (( (wanted + (RAM_SPARE * 3)) > RAM_AVAIL )); then
msg="your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is very close to the $avail_mem of free memory available,"
local msg="your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is very close to the $avail_mem of free memory available,"
if [[ "${FS,,}" == "zfs" ]]; then
info "$msg but since ZFS is active this will be ignored."
enabled "$final" && info "$msg but since ZFS is active this will be ignored."
else
warn "$msg please consider a lower amount."
enabled "$final" && warn "$msg please consider a lower amount."
fi
fi
fi
return 0
}
configureHalfMemory() {
local wanted
if [[ "${RAM_SIZE,,}" != "half" ]]; then
return 0
fi
if (( (RAM_AVAIL / 2) > RAM_SPARE )); then
wanted=$(( (RAM_AVAIL / 2) / 1048577 ))
RAM_SIZE="${wanted}M"
info "Allocated $wanted MB of RAM for $(app)."
local wanted=$(( RAM_AVAIL / 2 ))
# Divide by one byte more than a MiB to round down
local target=$(( wanted / 1048577 ))
RAM_SIZE="${target}M"
RAM_ALLOCATION="$wanted"
else
RAM_SIZE="max"
fi
return 0
}
configureMaxMemory() {
local wanted
if [[ "${RAM_SIZE,,}" != "max" ]]; then
return 0
fi
# max keeps a host reserve when possible, but on very small systems falls back
# to half the available memory to avoid starving the container.
if (( RAM_AVAIL < (RAM_SPARE * 2) )); then
wanted=$(( RAM_AVAIL / 2 ))
local wanted=$(( RAM_AVAIL / 2 ))
else
wanted=$(( RAM_AVAIL - (RAM_SPARE * 3) ))
local wanted=$(( RAM_AVAIL - (RAM_SPARE * 3) ))
if (( wanted < (RAM_SPARE * 6) )); then
wanted=$(( RAM_AVAIL - RAM_SPARE ))
@@ -80,33 +146,90 @@ configureMaxMemory() {
fi
wanted=$(( wanted / 1048577 ))
RAM_SIZE="${wanted}M"
# Divide by one byte more than a MiB to round down
local target=$(( wanted / 1048577 ))
RAM_SIZE="${target}M"
RAM_ALLOCATION="$wanted"
info "Allocated $wanted MB of RAM for $(app)."
return 0
}
showMemoryLimitHint() {
local kernel
kernel=$(uname -r)
if [[ "${kernel,,}" == *-wsl2* ]]; then
echo
info "Docker Desktop (WSL2) is detected, follow these instructions:"
info ""
info "Increase the memory limit in \"%UserProfile%\\.wslconfig\" by setting \"memory=<size>\" under \"[wsl2]\"."
info "Then run \"wsl --shutdown\" in PowerShell and restart Docker Desktop for the new limit to take effect."
echo
fi
return 0
}
checkMinimumMemory() {
local wanted
local wanted
wanted=$(numfmt --from=iec "$RAM_SIZE")
if [ "$wanted" -lt "$RAM_MINIMUM" ]; then
wanted=$(( wanted / 1048577 ))
error "Not enough memory available, there is only $wanted MB left!"
error "$(app) requires at least $(formatBytes "$RAM_MINIMUM") of RAM, but only $(formatBytes "$wanted") can be allocated."
showMemoryLimitHint
exit 16
fi
return 0
}
getMemoryInfo
checkMemoryAllocation() {
checkConfiguredMemory
configureHalfMemory
configureMaxMemory
checkMinimumMemory
local final="${1:-N}"
local configured
normalizeMemory
configured="$RAM_SIZE"
RAM_WARNING=""
RAM_ALLOCATION=""
getMemoryInfo
checkConfiguredMemory "$final"
configureHalfMemory
configureMaxMemory
checkMinimumMemory
if enabled "$final"; then
[ -n "$RAM_WARNING" ] && warn "$RAM_WARNING"
[ -n "$RAM_ALLOCATION" ] && info "Allocated $(formatBytes "$RAM_ALLOCATION") of RAM for $(app)."
else
RAM_SIZE="$configured"
fi
return 0
}
checkMemoryRequirement() {
checkMemoryAllocation "N"
return 0
}
finalizeMemory() {
checkMemoryAllocation "Y"
return 0
}
checkMemoryRequirement
return 0
+55 -9
View File
@@ -307,6 +307,27 @@ containerID() {
return 0
}
canBindPrivilegedPort() {
local port="$1"
local proto="${2:-tcp}"
local start="1024"
local rc=1
[ -r /proc/sys/net/ipv4/ip_unprivileged_port_start ] &&
start=$(< /proc/sys/net/ipv4/ip_unprivileged_port_start)
(( port >= start )) && return 0
if [[ "$proto" == "udp" ]]; then
{ timeout 0.1 nc -4 -n -d -u -l 127.0.0.1 "$port" > /dev/null 2>&1; rc=$?; } || :
else
{ timeout 0.1 nc -4 -n -d -l 127.0.0.1 "$port" > /dev/null 2>&1; rc=$?; } || :
fi
(( rc == 124 ))
}
disableIPv6() {
local dev="$1"
@@ -371,6 +392,8 @@ natGuestIP() {
local start="30"
fi
# Scan adjacent 172.30/31 through 172.254 subnets to avoid Docker routes
# while retaining the original third octet and guest host number.
for (( second=start; second<=254; second++ )); do
guest=$(guestIP "172.$second.$third.0" 2)
subnet=$(networkCIDR "$guest") || return 1
@@ -564,12 +587,8 @@ getReservedPorts() {
list+="53/tcp,53/udp,"
fi
# WEB_PORT and WSD_PORT are intentionally not reserved. In non-DHCP modes,
# closeWeb() releases both before NAT or user-mode forwarding is configured,
# allowing WEB_PORT (normally 5000) to be handed over to DSM.
normalizePorts "$list" "$mode"
return $?
return $?
}
getCustomHostPorts() {
@@ -587,7 +606,7 @@ getCustomHostPorts() {
done
normalizePorts "$ports" "$mode"
return 0
return $?
}
getHostPorts() {
@@ -600,12 +619,13 @@ getHostPorts() {
reserved=$(getReservedPorts "all")
custom=$(getCustomHostPorts "all")
normalizePorts "$reserved,$custom" "$mode"
return 0
return $?
}
getUserPorts() {
# User-mode networking forwards DSM management and SSH ports by default;
# internal container reservations and HOST_PORTS are removed below.
local defaults="22/tcp,5000/tcp,5001/tcp"
local list="$defaults,${USER_PORTS// /},"
@@ -646,8 +666,14 @@ getUserPorts() {
done
[ -n "$num" ] && ports+="$num/$proto,"
[ -z "$num" ] && continue
if ! canBindPrivilegedPort "$num" "$proto"; then
warn "Could not assign port $num/$proto to \"USER_PORTS\" because it cannot be bound by the current user!"
continue
fi
ports+="$num/$proto,"
done
# Remove duplicates
@@ -904,6 +930,7 @@ configurePasst() {
PASST_OPTS+=" -H $HOST"
PASST_OPTS+=" -M $GATEWAY_MAC"
PASST_OPTS+=" --runas $EUID:$(id -g)"
PASST_OPTS+=" -P $PASST_PID"
PASST_OPTS+=" -s $PASST_SOCKET"
PASST_OPTS+=" -l $log"
@@ -965,6 +992,17 @@ configurePasst() {
return 0
}
configureBridge() {
local file="/etc/qemu/bridge.conf"
[ -e "$file" ] && return 0
mkdir -p "${file%/*}" || return 0
echo "allow br0" > "$file" || return 0
return 0
}
createBridge() {
local gateway="$1"
@@ -2134,6 +2172,7 @@ showHostInfo() {
echo " DNS: $nameservers"
fi
enabled "$DEBUG" && echo
return 0
}
@@ -2188,6 +2227,7 @@ initializeNetwork() {
configureMTU
configureMAC
configureBridge
showHostInfo
@@ -2241,6 +2281,8 @@ else
if ! configureNAT; then
closeInterfaces
# NAT setup failure is recoverable: tear down partial interfaces and
# continue with the default user-mode backend.
NETWORK="user"
if ! enabled "$ROOTLESS" || enabled "$DEBUG"; then
@@ -2288,6 +2330,8 @@ else
fi
# Suppress the adapter option ROM because firmware network boot is unused and
# would otherwise alter boot order and startup timing.
NET_OPTS+=" -device $ADAPTER,id=net0,netdev=hostnet0,romfile=,mac=$MAC"
if [[ "$GUEST_MTU" != "0" && "$GUEST_MTU" != "1500" ]]; then
@@ -2298,6 +2342,8 @@ if [[ "$GUEST_MTU" != "0" && "$GUEST_MTU" != "1500" ]]; then
fi
fi
# Publish the container address and detected driver for the healthcheck and
# post-boot login-message helper.
if ! echo "$UPLINK" > "$QEMU_DIR"/qemu.ip; then
error "Failed to write QEMU IP file!"
exit 24
+33 -10
View File
@@ -67,6 +67,8 @@ displayReason() {
readQemuPid() {
# Interactive startup uses a wrapper-created PID file before QEMU writes its
# own pidfile, so accept either during startup and shutdown races.
readPidFile "$1" "$QEMU_START_PID" && return 0
readPidFile "$1" "$QEMU_PID"
}
@@ -184,6 +186,8 @@ startQemu() {
rm -f -- "$QEMU_START_PID"
# Launch QEMU in a separate session while recording the real child PID;
# setsid's wrapper PID is not suitable for guest shutdown or forced cleanup.
(
trap '' INT QUIT
@@ -238,6 +242,8 @@ sendGuestShutdown() {
local pid="$1"
local response
# Virtual DSM ignores ACPI powerdown, so graceful shutdown must go through
# the qemu-host guest API exposed on the Unix socket.
# Don't send the powerdown signal because vDSM ignores ACPI signals
# nc -q 1 -w 1 -U "$QEMU_DIR/monitor.sock" &> /dev/null <<<'system_powerdown' || :
@@ -265,6 +271,8 @@ sendGuestShutdown() {
normalizeTimeout() {
# Divide the remaining timeout into guest wait, SIGTERM grace, and final
# cleanup instead of allowing the API call to consume the entire budget.
local term_grace=3 # seconds before loop ends to send SIGTERM
local cleanup_grace=3 # seconds reserved after the loop for cleanup
@@ -307,6 +315,8 @@ waitForShutdown() {
# Stop waiting if the process has exited
isAlive "$pid" || break
# The process state is authoritative, but disappearance of both pidfiles
# also ends the wait when a wrapper exits before process reaping completes.
# Workaround for stale/zombie QEMU pid file
[ ! -s "$QEMU_START_PID" ] && [ ! -s "$QEMU_PID" ] && break
@@ -325,7 +335,7 @@ waitForShutdown() {
return 0
}
graceful_shutdown() {
gracefulShutdown() {
local sig="$1"
local pid code
@@ -334,8 +344,10 @@ graceful_shutdown() {
code=$(signalCode "$sig")
if [ -f "$QEMU_END" ]; then
if (( SHUTDOWN_SIGNAL != 0 )); then
# A second Ctrl-C is the explicit user request to skip the remaining
# graceful-shutdown wait and proceed to forced cleanup.
if (( code == 130 && SHUTDOWN_SIGNAL == code )); then
SHUTDOWN_SKIP=1
echo && info "Received SIGINT again, forcing shutdown..."
@@ -346,11 +358,14 @@ graceful_shutdown() {
return
fi
set +e
start=$SECONDS
SHUTDOWN_SIGNAL=$code
# Shutdown handlers must continue through missing processes and failed cleanup
# commands instead of being aborted by errexit.
set +e
touch "$QEMU_END"
echo && info "Received $sig signal, sending shutdown command..."
if ! readQemuPid pid; then
@@ -372,13 +387,21 @@ graceful_shutdown() {
finish "$code"
}
enabled "$SHUTDOWN" || return 0
enableTrap() {
enabled "$SHUTDOWN" || return 0
# Keep Ctrl-C available to interactive users without installing an unnecessary
# SIGINT handler for background/container execution.
if interactive; then
_trap gracefulShutdown SIGINT
fi
_trap gracefulShutdown SIGTERM SIGHUP SIGABRT SIGQUIT
return 0
}
[ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT"
if interactive; then
_trap graceful_shutdown SIGINT
fi
_trap graceful_shutdown SIGTERM SIGHUP SIGABRT SIGQUIT
return 0
+6
View File
@@ -37,6 +37,8 @@ exitIfShuttingDown() {
queryGuest() {
# Query DSM through the qemu-host sidecar rather than the guest network,
# which may not be configured yet.
{ json=$(curl --unix-socket "$socket" -m 20 -sk "$url"); local rc=$?; } || :
exitIfShuttingDown
@@ -116,6 +118,8 @@ writeDsmLocation() {
pollGuestLocation() {
# Keep polling until the guest reports a usable address, but stop promptly
# when container shutdown begins.
while [ ! -s "$file" ]; do
# Check if not shutting down
@@ -183,6 +187,8 @@ buildStaticMessage() {
ip=$(<"$address")
local port="${location##*:}"
# NAT and user-mode networking are reached through a forwarded host port;
# macvlan exposes DSM directly on the container-facing LAN address.
if [[ "${nic,,}" != "macvlan" ]]; then
msg="port $port"
else
+10
View File
@@ -66,6 +66,8 @@ trimSpaces() {
removeCpuArgument() {
# CPU configuration has dedicated variables. Remove raw -cpu arguments so
# option ordering cannot silently override the validated model and flags.
local args=" ${ARGUMENTS:-} "
while [[ "$args" =~ [[:space:]]-cpu([[:space:]][^[:space:]]+|=[^[:space:]]+)? ]]; do
@@ -94,6 +96,8 @@ configureKvmCpuModel() {
appendKvmInvtscFeature() {
# invtsc is safe only when the active accelerator can scale the host TSC;
# AMD and Intel expose that capability through different host flags.
if hasFlag "svm"; then
# AMD processor
@@ -131,6 +135,8 @@ configureTcgCpuModel() {
return 0
fi
# TCG uses the broad max model on native x86, but qemu64 is the compatible
# cross-architecture fallback.
if [[ "$ARCH" == "amd64" ]]; then
CPU_MODEL="max"
CPU_FEATURES+=",migratable=no"
@@ -158,6 +164,8 @@ configureTcg() {
composeCpuFlags() {
# Compose one -cpu value in precedence order: model, required features,
# then user-provided overrides.
CPU_FLAGS="${CPU_MODEL}${CPU_FEATURES:+,$CPU_FEATURES}${CPU_FLAGS:+,$CPU_FLAGS}"
return 0
@@ -170,6 +178,8 @@ configureHostCpuName() {
fi
if [ -n "$HOST_CPU" ]; then
# qemu-host expects a comma-separated CPU description with empty family
# and suffix fields, not QEMU's -cpu syntax.
HOST_CPU="${HOST_CPU%%,*},,"
else
HOST_CPU="QEMU, Virtual CPU,"
+8
View File
@@ -22,6 +22,8 @@ writeInfo() {
local content="$1"
# Replace the web status atomically so websocket readers never observe a
# partially written HTML fragment.
if ! printf '%s\n' "$content" > "$info_tmp"; then
rm -f -- "$info_tmp"
return 1
@@ -220,6 +222,8 @@ fi
trap finishProgress EXIT
trap 'exit 0' HUP INT QUIT
# SIGTERM requests one final measurement and web update rather than
# terminating between progress samples.
trap stopProgress TERM
if [[ "$body" == *"..." ]]; then
@@ -232,6 +236,8 @@ while true; do
bytes=$(getBytes "$path" "$mode")
effective_total="$total"
# An external downloader may provide authoritative completed and total byte
# counters; use them instead of filesystem size when available.
if [ -n "$status_file" ] && status=$(getStatus "$status_file"); then
read -r status_bytes status_total <<< "$status"
bytes="$status_bytes"
@@ -268,6 +274,8 @@ while true; do
fi
fi
else
# Floor the percentage rather than rounding so displayed completion
# never gets ahead of bytes actually written.
# Truncate to one decimal so progress is never reported early.
progress=$((bytes * 1000 / effective_total))
(( progress > 1000 )) && progress=1000
+12 -1
View File
@@ -42,6 +42,8 @@ validateHostMac() {
buildHostArguments() {
# qemu-host is a sidecar that bridges DSM's proprietary serial agent to
# Unix sockets used by shutdown and post-boot discovery helpers.
HOST_ARGS=()
HOST_ARGS+=("-cpu=$CPU_CORES")
HOST_ARGS+=("-cpu_arch=$HOST_CPU")
@@ -60,6 +62,8 @@ startHostBinary() {
local pid
# Remove stale sockets and pid state before starting the sidecar; a Unix
# socket path cannot be rebound while an old filesystem entry remains.
rm -f -- "$HOST_PID" "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET" || return 1
if enabled "$HOST_DEBUG"; then
@@ -85,6 +89,8 @@ waitForSocket() {
local timeout=5 pid
local deadline=$((SECONDS + timeout))
# Do not start QEMU until both sidecar sockets are ready; otherwise the
# VirtIO serial channel or API client may race initial creation.
while [ ! -S "$socket" ]; do
if ! readPidFile pid "$HOST_PID" || ! isAlive "$pid"; then
@@ -105,6 +111,11 @@ waitForSocket() {
configureSerialPorts() {
local bus
bus=$(getPciBus)
# Managed interactive mode separates the console and QEMU monitor into
# reconnecting sockets; other runs keep the simple combined stdio monitor.
if enabled "${SHUTDOWN:-Y}" && interactive; then
CONSOLE_SOCKET="$QEMU_DIR/console.sock"
@@ -122,7 +133,7 @@ configureSerialPorts() {
fi
SERIAL_OPTS+=" \
-device virtio-serial-pci,id=virtio-serial0,bus=pcie.0,addr=0x3 \
-device virtio-serial-pci,id=virtio-serial0,bus=$bus,addr=0x3 \
-chardev socket,id=charchannel0,path=$HOST_AGENT_SOCKET,reconnect-ms=1000 \
-device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=vchannel"
+47 -14
View File
@@ -2,20 +2,19 @@
set -Eeuo pipefail
: "${WEB_PORT:="5000"}" # Webserver port
: "${WSD_PORT:="8004"}" # Websockets port
# Sanitize port variables
WEB_PORT=$(strip "$WEB_PORT")
WSD_PORT=$(strip "$WSD_PORT")
WEB_PID="/run/nginx.pid"
WSD_LOG="/var/log/websocketd.log"
WSD_PID="$QEMU_DIR/websocketd.pid"
WSD_SOCKET="$QEMU_DIR/status-ws.sock"
prepareWebFiles() {
cp -r /var/www/* "$QEMU_DIR" || return 1
rm -f -- "$WSD_PID" "$WEB_PID" "$WSD_LOG" || return 1
rm -f -- "$WSD_PID" "$WSD_SOCKET" "$WEB_PID" "$WSD_LOG" || return 1
return 0
}
@@ -24,9 +23,8 @@ configureWebPorts() {
if ! sed -i \
-e "s|listen 5000 default_server;|listen $WEB_PORT default_server;|g" \
-e "s|proxy_pass http://127.0.0.1:8004/;|proxy_pass http://127.0.0.1:$WSD_PORT/;|g" \
/etc/nginx/sites-enabled/web.conf; then
error "Failed to configure webserver ports!"
error "Failed to configure webserver port!"
return 1
fi
@@ -35,6 +33,8 @@ configureWebPorts() {
configureIpv6Listen() {
# Use one dual-stack listener when IPv6 is active, avoiding separate IPv4
# and IPv6 sockets that can conflict on the same port.
if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]]; then
if ! sed -i \
@@ -49,11 +49,32 @@ configureIpv6Listen() {
return 0
}
configureNginx() {
mkdir -p /etc/nginx/sites-enabled || return 1
rm -f /etc/nginx/sites-enabled/default || return 1
# TODO: Use setfacl to grant www-data access to the Unix sockets
# and restore unprivileged nginx workers.
if ! sed -i \
-e 's/^user .*/user root;/' \
-e 's/^worker_processes.*/worker_processes 1;/' \
/etc/nginx/nginx.conf; then
error "Failed to configure nginx!"
return 1
fi
if ! cp /etc/nginx/default.conf /etc/nginx/sites-enabled/web.conf; then
error "Failed to copy nginx config!"
return 1
fi
return 0
}
configureWebServer() {
mkdir -p /etc/nginx/sites-enabled
cp /etc/nginx/default.conf /etc/nginx/sites-enabled/web.conf
configureNginx || return 1
configureWebPorts || return 1
configureIpv6Listen || return 1
@@ -67,6 +88,8 @@ stopWebServer() {
if readPidFile pid "$WEB_PID"; then
pKill "$pid" 2
# Escalate only after the normal termination grace period; stale nginx
# processes would otherwise keep the configured web port occupied.
if isAlive "$pid"; then
kill -9 -- "$pid" 2>/dev/null || :
fi
@@ -96,7 +119,7 @@ stopWebsocketServer() {
fi
fi
rm -f -- "$WSD_PID"
rm -f -- "$WSD_PID" "$WSD_SOCKET"
return 0
}
@@ -104,8 +127,7 @@ startWebsocketServer() {
# Start websocket server
websocketd \
--address 127.0.0.1 \
--port="$WSD_PORT" \
--unixsocket="$WSD_SOCKET" \
/run/socket.sh \
>"$WSD_LOG" 2>&1 &
@@ -118,20 +140,31 @@ startWebsocketServer() {
fi
local i
for (( i = 1; i <= 5; i++ )); do
for (( i = 1; i <= 50; i++ )); do
if ! isAlive "$pid"; then
rm -f -- "$WSD_PID"
rm -f -- "$WSD_PID" "$WSD_SOCKET"
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
error "Failed to start websocket server!"
return 1
fi
[ -S "$WSD_SOCKET" ] && return 0
sleep 0.1
done
return 0
pKill "$pid" 2
if isAlive "$pid"; then
kill -9 -- "$pid" 2>/dev/null || :
fi
rm -f -- "$WSD_PID" "$WSD_SOCKET"
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
error "Websocket server did not create its socket!"
return 1
}
prepareWebFiles
+4
View File
@@ -18,6 +18,8 @@ refresh() {
[[ "$msg" == "$lastmsg" ]] && return 0
lastmsg="$msg"
# websocketd clients interpret s: as a status update and c: as a command;
# suppress unchanged status to avoid redundant browser work.
echo "s: $msg"
return 0
@@ -37,6 +39,8 @@ inotifywait \
case "${event,,}" in
"delete"* )
echo "c: vnc" ;;
# moved_to covers the atomic replacement used by html()/writeAtomic(),
# while close_write handles direct writers.
"close_write"* | "moved_to"* )
refresh ;;
esac
+97
View File
@@ -7,6 +7,12 @@ info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "${1:-}" "\E[0m\n"; }
error () { printf "%b%s%b" "\E[1;31m " "ERROR: ${1:-}" "\E[0m\n" >&2; }
warn () { printf "%b%s%b" "\E[1;31m " "Warning: ${1:-}" "\E[0m\n" >&2; }
app() {
echo "Virtual DSM"
return 0
}
readPidFile() {
local -n _pid="$1"
@@ -17,6 +23,8 @@ readPidFile() {
return 1
fi
# Reject empty, zero, or nonnumeric pidfiles so cleanup can never signal an
# unintended process group.
if [[ ! "$_pid" =~ ^[1-9][0-9]*$ ]]; then
_pid=""
return 1
@@ -47,8 +55,27 @@ isAmdCpu() {
[[ "$vendor" == "AuthenticAMD" ]]
}
getPciBus() {
local machine="${1:-${MACHINE:-q35}}"
if [ -n "${PCI_BUS:-}" ]; then
echo "$PCI_BUS"
return 0
fi
case "${machine,,}" in
pc|pc-i440fx*) echo "pci.0" ;;
*) echo "pcie.0" ;;
esac
return 0
}
interactive() {
# A TTY on stdin is insufficient when /dev/tty is unavailable; require both
# before enabling interactive console handling.
[ -t 0 ] && : 2>/dev/null </dev/tty >/dev/tty
}
@@ -252,6 +279,8 @@ setOwner() {
[ ! -f "$file" ] && return 1
# Match generated files to the owner of their bind-mounted parent directory
# instead of assuming a fixed container or host UID.
dir=$(dirname -- "$file")
uid=$(stat -c '%u' "$dir") || return 1
gid=$(stat -c '%g' "$dir") || return 1
@@ -284,6 +313,64 @@ makeDir() {
return 0
}
finiteMemoryLimit() {
local limit="$1"
# cgroup v1 commonly reports this enormous sentinel for an unlimited memory
# limit; compare as decimal strings to avoid shell integer overflow.
local sentinel="4611686018427387904"
local i
[[ "$limit" =~ ^[0-9]+$ ]] || return 1
(( ${#limit} < ${#sentinel} )) && return 0
(( ${#limit} > ${#sentinel} )) && return 1
for (( i=0; i<${#sentinel}; i++ )); do
local left="${limit:i:1}"
local right="${sentinel:i:1}"
(( left < right )) && return 0
(( left > right )) && return 1
done
return 1
}
getMemoryInfo() {
local host_total
local host_avail
local limit=""
local current=""
host_total=$(free -b | awk '/^Mem:/ {print $2; exit}')
host_avail=$(free -b | awk '/^Mem:/ {print $7; exit}')
RAM_TOTAL="$host_total"
RAM_AVAIL="$host_avail"
if [ -r /sys/fs/cgroup/memory.max ] && [ -r /sys/fs/cgroup/memory.current ]; then
limit=$(< /sys/fs/cgroup/memory.max)
current=$(< /sys/fs/cgroup/memory.current)
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ] && [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then
limit=$(< /sys/fs/cgroup/memory/memory.limit_in_bytes)
current=$(< /sys/fs/cgroup/memory/memory.usage_in_bytes)
fi
# Use the tighter of host availability and the container's remaining cgroup
# allowance so RAM sizing cannot exceed either boundary.
if finiteMemoryLimit "$limit" && [[ "$current" =~ ^[0-9]+$ ]]; then
(( limit < RAM_TOTAL )) && RAM_TOTAL="$limit"
local available=$(( limit - current ))
(( available < 0 )) && available=0
(( available < RAM_AVAIL )) && RAM_AVAIL="$available"
fi
return 0
}
stateFile() {
local name="$1"
@@ -316,6 +403,8 @@ writeAtomic() {
local path="$1"
local content="$2"
# Use a per-process temporary file and rename so readers see either the old
# complete value or the new complete value.
local tmp="${path}.${BASHPID}.tmp"
if ! printf '%s\n' "$content" > "$tmp"; then
@@ -380,6 +469,8 @@ restoreState() {
local prefix="${4:-$PROCESS}"
local value
# Persistent state fills only unset variables unless force is requested,
# preserving explicit environment overrides.
if ! enabled "$force"; then
[ -z "${!var:-}" ] || return 0
fi
@@ -439,6 +530,8 @@ html() {
HTML="${HTML/\[4\]/$footer}"
HTML="${HTML/\[5\]/$FOOTER2}"
# Publish both the full page and websocket fragment atomically because nginx
# and websocketd may read them concurrently.
writeAtomic "$PAGE" "$HTML" || return 1
writeAtomic "$INFO" "$body" || return 1
@@ -512,6 +605,8 @@ setCountry() {
[[ "${TZ,,}" == "asia/shanghai" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/chongqing" ]] && COUNTRY="CN"
# Country detection is best-effort and tries independent services in order;
# failure leaves mirror selection at its global default.
[ -z "$COUNTRY" ] && getCountry "https://api.ipapi.is" ".location.country_code"
[ -z "$COUNTRY" ] && getCountry "https://ifconfig.co/json" ".country_iso"
[ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code"
@@ -536,6 +631,8 @@ addPackage() {
[ -z "$COUNTRY" ] && setCountry
# Use a mainland mirror only for on-demand package installation, avoiding
# slow or inaccessible Debian endpoints in that region.
if [[ "${COUNTRY^^}" == "CN" ]]; then
sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
fi
+2 -2
View File
@@ -39,6 +39,6 @@ server {
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_pass http://127.0.0.1:8004/;
}
proxy_pass http://unix:/run/shm/status-ws.sock:/;
}
}