From dced815c06d70be03dfc9d69feb96615315d527e Mon Sep 17 00:00:00 2001 From: Kroese Date: Mon, 3 Aug 2026 16:40:10 +0200 Subject: [PATCH] docs: Improve commenting (#1349) --- src/check.sh | 4 ++++ src/config.sh | 6 ++++++ src/disk.sh | 26 ++++++++++++++++++++++++++ src/display.sh | 8 ++++++++ src/install.sh | 24 ++++++++++++++++++++++++ src/memory.sh | 6 ++++++ src/network.sh | 10 ++++++++++ src/power.sh | 12 ++++++++++++ src/print.sh | 6 ++++++ src/proc.sh | 10 ++++++++++ src/progress.sh | 8 ++++++++ src/reset.sh | 16 ++++++++++++++++ src/serial.sh | 8 ++++++++ src/server.sh | 6 ++++++ src/socket.sh | 4 ++++ src/utils.sh | 16 ++++++++++++++++ 16 files changed, 170 insertions(+) diff --git a/src/check.sh b/src/check.sh index 000b501..a7ba38f 100644 --- a/src/check.sh +++ b/src/check.sh @@ -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" diff --git a/src/config.sh b/src/config.sh index 0c30dc4..866153a 100644 --- a/src/config.sh +++ b/src/config.sh @@ -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}" @@ -47,6 +51,8 @@ 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 diff --git a/src/disk.sh b/src/disk.sh index 59f226a..20d5a76 100644 --- a/src/disk.sh +++ b/src/disk.sh @@ -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 @@ -508,6 +520,8 @@ createDevice () { local diskSectors="$9" local bus="${PCI_BUS:-pcie.0}" + # q35 uses pcie.0, while legacy pc/i440fx machines expose their devices on + # pci.0 unless the caller supplied an explicit bus. [[ -z "${PCI_BUS:-}" && ( "${MACHINE,,}" == pc || "${MACHINE,,}" == pc-i440fx* ) ]] && bus="pci.0" local options="" @@ -559,6 +573,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 +629,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 +663,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 +719,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 +815,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 +856,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 diff --git a/src/display.sh b/src/display.sh index 49e6de1..dbad929 100644 --- a/src/display.sh +++ b/src/display.sh @@ -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" diff --git a/src/install.sh b/src/install.sh index cd8bb53..068901a 100644 --- a/src/install.sh +++ b/src/install.sh @@ -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 diff --git a/src/memory.sh b/src/memory.sh index 6e30cb8..e596129 100644 --- a/src/memory.sh +++ b/src/memory.sh @@ -21,6 +21,8 @@ checkConfiguredMemory() { 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," + # ZFS ARC can release memory under pressure, so current free-memory checks + # are advisory rather than a reason to reduce the requested guest RAM. if [[ "${FS,,}" == "zfs" ]]; then info "$msg but since ZFS is active this will be ignored." else @@ -48,6 +50,8 @@ configureHalfMemory() { return 0 fi + # half uses half of currently available memory only when that still leaves + # the host reserve; otherwise it falls through to the max calculation. if (( (RAM_AVAIL / 2) > RAM_SPARE )); then wanted=$(( (RAM_AVAIL / 2) / 1048577 )) RAM_SIZE="${wanted}M" @@ -66,6 +70,8 @@ configureMaxMemory() { return 0 fi + # max normally leaves multiple reserve units for the container and host, + # but scales that margin down on memory-constrained systems. if (( RAM_AVAIL < (RAM_SPARE * 2) )); then wanted=$(( RAM_AVAIL / 2 )) diff --git a/src/network.sh b/src/network.sh index 0ac4e3a..8c51ed2 100644 --- a/src/network.sh +++ b/src/network.sh @@ -371,6 +371,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 @@ -606,6 +608,8 @@ getHostPorts() { 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// /}," @@ -2241,6 +2245,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 +2294,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 +2306,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 diff --git a/src/power.sh b/src/power.sh index c700bc0..3b609d3 100644 --- a/src/power.sh +++ b/src/power.sh @@ -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 @@ -336,6 +346,8 @@ graceful_shutdown() { if [ -f "$QEMU_END" ]; 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..." diff --git a/src/print.sh b/src/print.sh index 1b9c30e..70ec303 100644 --- a/src/print.sh +++ b/src/print.sh @@ -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 diff --git a/src/proc.sh b/src/proc.sh index 555ee76..8316d56 100644 --- a/src/proc.sh +++ b/src/proc.sh @@ -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," diff --git a/src/progress.sh b/src/progress.sh index 708eaf1..5c36a20 100644 --- a/src/progress.sh +++ b/src/progress.sh @@ -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 diff --git a/src/reset.sh b/src/reset.sh index 4edca77..0774251 100644 --- a/src/reset.sh +++ b/src/reset.sh @@ -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 @@ -96,6 +100,8 @@ checkStorage() { # Check system + # Runtime sockets, pidfiles, and status files live in shared memory so they + # are fast, ephemeral, and visible to helper processes. QEMU_DIR="/run/shm" if [ ! -d "/dev/shm" ]; then @@ -141,6 +147,8 @@ checkFilesystem() { finiteMemoryLimit() { local limit="$1" + # cgroup v1 commonly reports an enormous sentinel instead of an unlimited + # marker; compare as decimal text to avoid shell integer overflow. local sentinel="4611686018427387904" local i @@ -179,6 +187,8 @@ getMemoryInfo() { 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 works in both limited and unlimited containers. if finiteMemoryLimit "$limit" && [[ "$current" =~ ^[0-9]+$ ]]; then (( limit < RAM_TOTAL )) && RAM_TOTAL="$limit" @@ -204,6 +214,8 @@ normalizeRamSize() { if [[ "${RAM_SIZE,,}" != "max" && "${RAM_SIZE,,}" != "half" ]]; then + # Preserve the historical shorthand: small bare numbers mean GiB, while + # values of 130 or more are interpreted as MiB. if [ -z "${RAM_SIZE//[0-9. ]}" ]; then [ "${RAM_SIZE%%.*}" -lt "130" ] && RAM_SIZE="${RAM_SIZE}G" || RAM_SIZE="${RAM_SIZE}M" fi @@ -231,6 +243,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." @@ -344,6 +358,8 @@ echo 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}} diff --git a/src/serial.sh b/src/serial.sh index 273e334..37e26f3 100644 --- a/src/serial.sh +++ b/src/serial.sh @@ -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,8 @@ waitForSocket() { configureSerialPorts() { + # 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" diff --git a/src/server.sh b/src/server.sh index 98a404f..7b930a4 100644 --- a/src/server.sh +++ b/src/server.sh @@ -35,6 +35,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 \ @@ -67,6 +69,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 @@ -117,6 +121,8 @@ startWebsocketServer() { return 1 fi + # Keep the sidecar alive briefly before accepting startup as successful, + # surfacing immediate bind or script failures with its captured log. local i for (( i = 1; i <= 5; i++ )); do diff --git a/src/socket.sh b/src/socket.sh index 5b620e0..3dace8e 100644 --- a/src/socket.sh +++ b/src/socket.sh @@ -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 diff --git a/src/utils.sh b/src/utils.sh index 3b2723d..536b0c7 100644 --- a/src/utils.sh +++ b/src/utils.sh @@ -17,6 +17,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 @@ -49,6 +51,8 @@ isAmdCpu() { 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 } @@ -252,6 +256,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 @@ -316,6 +322,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 +388,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 +449,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 +524,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 +550,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