Compare commits

..
25 Commits
Author SHA1 Message Date
KroeseandGitHub c70b93c2b6 docs: Readme (#1358) 2026-08-07 22:21:14 +02:00
KroeseandGitHub 4c700e87fe fix: Improve nginx configuration handling (#1357) 2026-08-07 22:20:28 +02:00
KroeseandGitHub 030255004f fix: Handle privileged user-mode ports (#1356) 2026-08-07 22:19:18 +02:00
KroeseandGitHub e64f9f2c39 fix: Wait for websocket sockets during startup (#1355) 2026-08-07 12:59:56 +02:00
KroeseandGitHub 537fc97d53 feat: Use Unix sockets for internal services (#1354) 2026-08-07 03:30:03 +02:00
KroeseandGitHub 438e992653 feat: Check available memory before installation starts (#1353) 2026-08-07 02:52:21 +02:00
KroeseandGitHub 615fad2a0d feat: Use dynamic PCI bus configuration (#1352) 2026-08-06 14:44:03 +02:00
KroeseandGitHub aac543e5c7 fix: Restrict minimal configured RAM amount to 1 GB (#1351) 2026-08-06 14:38:10 +02:00
KroeseandGitHub fec4f876e7 fix: Race between signal handler and cleanup (#1350) 2026-08-05 21:09:01 +02:00
KroeseandGitHub dced815c06 docs: Improve commenting (#1349) 2026-08-03 16:40:10 +02:00
KroeseandGitHub 51d6f3aeca fix: Reserve internal ports for user-mode forwarding (#1348) 2026-08-02 23:01:52 +02:00
KroeseandGitHub 4544620d48 feat: Increase indentation of printed QEMU arguments (#1347) 2026-08-01 05:49:50 +02:00
KroeseandGitHub de8468161f feat: Refactor negated command conditions (#1346) 2026-07-30 21:22:21 +02:00
renovate[bot]andGitHub 18d6be8210 chore(deps): update hadolint/hadolint-action action to v3.4.0 (#1345) 2026-07-30 21:06:27 +02:00
KroeseandGitHub e5b8cf3bf8 build: Update Passt to v2026_07_28 (#1344) 2026-07-30 10:10:41 +02:00
KroeseandGitHub ec12039f43 feat: Use deadline-based process timeouts (#1343) 2026-07-28 15:22:47 +02:00
KroeseandGitHub 3c7c3ca1b1 fix: Prevent race when reading PID files (#1342) 2026-07-28 14:22:08 +02:00
KroeseandGitHub 2b27f32cd4 fix: Disk options were applied to the controller (#1341) 2026-07-28 03:30:55 +02:00
KroeseandGitHub 55d1d50284 fix: Retry transient gateway errors in web status (#1340) 2026-07-27 23:49:22 +02:00
KroeseandGitHub 784b73b5b5 feat: Add DISK_OPTIONS support to disk devices (#1339) 2026-07-27 23:48:21 +02:00
KroeseandGitHub 85f00bb9cc docs: Added new DISK_OPTIONS variable (#1338) 2026-07-27 12:04:25 +02:00
KroeseandGitHub efe8732e47 fix: Avoid shadowing PID output variables (#1337) 2026-07-27 12:03:24 +02:00
KroeseandGitHub f6871dd61b fix: Clear invalid PID values in shared reader (#1336) 2026-07-26 17:52:14 +02:00
KroeseandGitHub a713b728ff fix: Use shared PID reader for helper processes (#1335) 2026-07-26 17:41:46 +02:00
KroeseandGitHub 0d74c6ac80 build: Update dependabot config (#1334) 2026-07-26 00:58:36 +02:00
25 changed files with 713 additions and 291 deletions
+7
View File
@@ -1,4 +1,5 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: docker - package-ecosystem: docker
directory: / directory: /
@@ -6,9 +7,15 @@ updates:
interval: weekly interval: weekly
cooldown: cooldown:
default-days: 7 default-days: 7
- package-ecosystem: github-actions - package-ecosystem: github-actions
directory: / directory: /
schedule: schedule:
interval: weekly interval: weekly
cooldown: cooldown:
default-days: 7 default-days: 7
ignore:
- dependency-name: "*"
update-types:
- version-update:semver-minor
- version-update:semver-patch
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
-e SC2153 -e SC2153
- -
name: Lint Dockerfile name: Lint Dockerfile
uses: hadolint/hadolint-action@v3.3.0 uses: hadolint/hadolint-action@v3.4.0
with: with:
dockerfile: Dockerfile dockerfile: Dockerfile
ignore: DL3008 ignore: DL3008
+11 -15
View File
@@ -7,8 +7,9 @@ ARG TARGETARCH
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG VERSION_ARG="0.0" ARG VERSION_ARG="0.0"
ARG VERSION_WSD="0.4.2"
ARG VERSION_CSTRUCT="4.7" ARG VERSION_CSTRUCT="4.7"
ARG VERSION_PASST="2026_07_16" ARG VERSION_PASST="2026_07_28"
ARG DEBCONF_NOWARNINGS="yes" ARG DEBCONF_NOWARNINGS="yes"
ARG DEBIAN_FRONTEND="noninteractive" ARG DEBIAN_FRONTEND="noninteractive"
@@ -29,10 +30,6 @@ RUN <<EOF
procps \ procps \
ipcalc \ ipcalc \
ethtool \ ethtool \
python3 \
python3-pip \
python3-msgpack \
python3-pysodium \
xz-utils \ xz-utils \
iptables \ iptables \
iproute2 \ iproute2 \
@@ -43,30 +40,29 @@ RUN <<EOF
e2fsprogs \ e2fsprogs \
diffutils \ diffutils \
qemu-utils \ qemu-utils \
websocketd \
iputils-ping \ iputils-ping \
inotify-tools \ inotify-tools \
ca-certificates \ ca-certificates \
netcat-openbsd \ netcat-openbsd \
qemu-system-x86 qemu-system-x86 \
python3 \
python3-pip \
python3-msgpack \
python3-pysodium
# Install Passt package # 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 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 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 apt-get clean
# Install Python dependencies # Install Python dependencies
pip3 install --no-cache-dir --break-system-packages --root-user-action=ignore "dissect.cstruct==$VERSION_CSTRUCT" 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 # Set version file
echo "$VERSION_ARG" > /etc/version echo "$VERSION_ARG" > /etc/version
+1
View File
@@ -39,6 +39,7 @@ An empty default means the variable is unset and its value is determined automat
| `DISK_DISCARD` | `unmap` | Discard/TRIM mode for the primary disk. | | `DISK_DISCARD` | `unmap` | Discard/TRIM mode for the primary disk. |
| `DISK_ROTATION` | `1` | Rotation rate reported to the guest. Use `1` to identify the disk as an SSD. | | `DISK_ROTATION` | `1` | Rotation rate reported to the guest. Use `1` to identify the disk as an SSD. |
| `DISK_FLAGS` | | Additional options used when creating `qcow2` disks. | | `DISK_FLAGS` | | Additional options used when creating `qcow2` disks. |
| `DISK_OPTIONS` | | Additional options appended to QEMU disk devices. |
| `ALLOCATE` | `N` | Preallocates space for the data disks. | | `ALLOCATE` | `N` | Preallocates space for the data disks. |
| `STORAGE` | `/storage` | Storage directory used for disks, settings, and downloads. | | `STORAGE` | `/storage` | Storage directory used for disks, settings, and downloads. |
+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 or Podman on a Linux host with KVM support.
- Docker Desktop or Podman (Desktop) on Windows 11 with nested virtualization enabled. - Docker Desktop or Podman (Desktop) on Windows 11 with nested virtualization enabled.
- At least 2 GB of available RAM. - At least 1 GB of available RAM.
- At least 32 GB of free disk space. - At least 16 GB of free disk space.
> [!NOTE] > [!NOTE]
> Docker Desktop on Linux, macOS, and Windows 10 does not currently provide KVM access to containers and is therefore not supported. > 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" address="/run/shm/qemu.ip"
gateway="/run/shm/qemu.gw" 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 [ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 0
location=$(<"$file") location=$(<"$file")
if ! curl -m 20 -ILfSs "http://$location/" > /dev/null; then 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 if enabled "$DHCP"; then
ip=$(<"$address") ip=$(<"$address")
echo "Failed to reach DSM at http://$location" echo "Failed to reach DSM at http://$location"
+13 -2
View File
@@ -6,6 +6,8 @@ DEV_OPTS=""
configureProcessor() { 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="-cpu $CPU_FLAGS"
CPU_OPTS+=" -smp $CPU_CORES,sockets=1,dies=1,cores=$CPU_CORES,threads=1" CPU_OPTS+=" -smp $CPU_CORES,sockets=1,dies=1,cores=$CPU_CORES,threads=1"
@@ -29,6 +31,8 @@ configureMonitor() {
configureMachine() { 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="-machine type=$MACHINE,smm=off,usb=off"
MAC_OPTS+=",vmport=off,dump-guest-core=off,hpet=off${KVM_OPTS}" MAC_OPTS+=",vmport=off,dump-guest-core=off,hpet=off${KVM_OPTS}"
@@ -37,9 +41,12 @@ configureMachine() {
configureVirtioDevices() { 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+=" -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 return 0
} }
@@ -47,11 +54,15 @@ configureVirtioDevices() {
buildArguments() { buildArguments() {
ARGS="$DEF_OPTS $CPU_OPTS $RAM_OPTS $MAC_OPTS $DISPLAY_OPTS $MON_OPTS $SERIAL_OPTS $NET_OPTS $DISK_OPTS $DEV_OPTS $ARGUMENTS" 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 ' ') ARGS=$(echo "$ARGS" | sed 's/\t/ /g' | tr -s ' ')
return 0 return 0
} }
finalizeMemory
configureMemory configureMemory
configureMonitor configureMonitor
configureMachine configureMachine
+41 -7
View File
@@ -7,6 +7,7 @@ set -Eeuo pipefail
: "${DISK_FMT:="raw"}" # Disk file format, 'raw' by default for best performance : "${DISK_FMT:="raw"}" # Disk file format, 'raw' by default for best performance
: "${DISK_TYPE:=""}" # Device type to be used, "sata", "nvme", "blk" or "scsi" : "${DISK_TYPE:=""}" # Device type to be used, "sata", "nvme", "blk" or "scsi"
: "${DISK_FLAGS:=""}" # Specifies the options for use with the qcow2 disk format : "${DISK_FLAGS:=""}" # Specifies the options for use with the qcow2 disk format
: "${DISK_OPTIONS:=""}" # Specifies additional options for the QEMU disk device
: "${DISK_CACHE:="none"}" # Caching mode, can be set to 'writeback' for better performance : "${DISK_CACHE:="none"}" # Caching mode, can be set to 'writeback' for better performance
: "${DISK_DISCARD:="unmap"}" # Controls whether unmap (TRIM) commands are passed to the host. : "${DISK_DISCARD:="unmap"}" # Controls whether unmap (TRIM) commands are passed to the host.
: "${DISK_ROTATION:="1"}" # Rotation rate, set to 1 for SSD storage and increase for HDD : "${DISK_ROTATION:="1"}" # Rotation rate, set to 1 for SSD storage and increase for HDD
@@ -16,6 +17,7 @@ DISK_IO=$(strip "$DISK_IO")
DISK_FMT=$(strip "$DISK_FMT") DISK_FMT=$(strip "$DISK_FMT")
DISK_TYPE=$(strip "$DISK_TYPE") DISK_TYPE=$(strip "$DISK_TYPE")
DISK_FLAGS=$(strip "$DISK_FLAGS") DISK_FLAGS=$(strip "$DISK_FLAGS")
DISK_OPTIONS=$(strip "$DISK_OPTIONS")
DISK_CACHE=$(strip "$DISK_CACHE") DISK_CACHE=$(strip "$DISK_CACHE")
DISK_DISCARD=$(strip "$DISK_DISCARD") DISK_DISCARD=$(strip "$DISK_DISCARD")
DISK_ROTATION=$(strip "$DISK_ROTATION") DISK_ROTATION=$(strip "$DISK_ROTATION")
@@ -23,6 +25,8 @@ DISK_ROTATION=$(strip "$DISK_ROTATION")
BOOT="$STORAGE/$BASE.boot.img" BOOT="$STORAGE/$BASE.boot.img"
SYSTEM="$STORAGE/$BASE.system.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 "$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 [ ! -s "$SYSTEM" ] && error "Virtual DSM system-image does not exist ($SYSTEM)" && exit 82
@@ -125,6 +129,8 @@ allocateRaw() {
return $? return $?
fi 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 "$dataSize" "$diskFile" &>/dev/null && return 0
fallocate -l -x "$dataSize" "$diskFile" && return 0 fallocate -l -x "$dataSize" "$diskFile" && return 0
truncate -s "$dataSize" "$diskFile" || return 1 truncate -s "$dataSize" "$diskFile" || return 1
@@ -157,6 +163,8 @@ normalizeSize() {
local free dataSize local free dataSize
local spare=1073741824 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 if [[ "${diskSpace,,}" == "max" || "${diskSpace,,}" == "half" ]]; then
free=$(df --output=avail -B 1 "$dir" | tail -n 1) free=$(df --output=avail -B 1 "$dir" | tail -n 1)
@@ -419,6 +427,8 @@ convertDisk() {
if [[ "$destinationFmt" == "raw" ]]; then if [[ "$destinationFmt" == "raw" ]]; then
if ! disabled "$ALLOCATE"; 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 # Work around qemu-img bug
if ! currentSize=$(stat -c%s "$tmpFile"); then if ! currentSize=$(stat -c%s "$tmpFile"); then
error "Failed to determine converted image size: $tmpFile" error "Failed to determine converted image size: $tmpFile"
@@ -433,6 +443,8 @@ convertDisk() {
fi fi
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 if ! mv "$tmpFile" "$destinationFile"; then
error "Failed to move converted $diskDesc image to $destinationFile." error "Failed to move converted $diskDesc image to $destinationFile."
exit 79 exit 79
@@ -477,6 +489,8 @@ checkFS () {
warn "the filesystem of $base is FUSE, this extra layer will negatively affect performance!" warn "the filesystem of $base is FUSE, this extra layer will negatively affect performance!"
fi 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 if ! supportsDirect "$fs"; then
warn "the filesystem of $base is $fs, which does not support O_DIRECT mode, adjusting settings..." warn "the filesystem of $base is $fs, which does not support O_DIRECT mode, adjusting settings..."
fi fi
@@ -504,9 +518,12 @@ createDevice () {
local diskCache="$7" local diskCache="$7"
local diskSerial="$8" local diskSerial="$8"
local diskSectors="$9" 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#,}"
local bootIndex="" local bootIndex=""
local diskId="data$diskIndex" local diskId="data$diskIndex"
@@ -520,29 +537,29 @@ createDevice () {
;; ;;
"usb" ) "usb" )
result+=",if=none \ result+=",if=none \
-device usb-storage,drive=${diskId}${bootIndex}${diskSerial}${diskSectors}" -device usb-storage,drive=${diskId}${bootIndex}${diskSerial}${diskSectors}${options}"
echo "$result" echo "$result"
;; ;;
"nvme" ) "nvme" )
result+=",if=none \ result+=",if=none \
-device nvme,drive=${diskId}${bootIndex},serial=deadbeaf${diskIndex}${diskSerial}${diskSectors}" -device nvme,drive=${diskId}${bootIndex},serial=deadbeaf${diskIndex}${diskSerial}${diskSectors}${options}"
echo "$result" echo "$result"
;; ;;
"ide" | "sata" ) "ide" | "sata" )
result+=",if=none \ result+=",if=none \
-device ich9-ahci,id=ahci${diskIndex},addr=$diskAddress \ -device ich9-ahci,id=ahci${diskIndex},addr=$diskAddress \
-device ide-hd,drive=${diskId},bus=ahci$diskIndex.0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}" -device ide-hd,drive=${diskId},bus=ahci$diskIndex.0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}${options}"
echo "$result" echo "$result"
;; ;;
"blk" | "virtio-blk" ) "blk" | "virtio-blk" )
result+=",if=none \ result+=",if=none \
-device virtio-blk-pci,drive=${diskId},bus=$bus,addr=$diskAddress,iothread=io2${bootIndex}${diskSerial}${diskSectors}" -device virtio-blk-pci,drive=${diskId},bus=$bus,addr=$diskAddress,iothread=io2${bootIndex}${diskSerial}${diskSectors}${options}"
echo "$result" echo "$result"
;; ;;
"scsi" | "virtio-scsi" ) "scsi" | "virtio-scsi" )
result+=",if=none \ result+=",if=none \
-device virtio-scsi-pci,id=${diskId}b,bus=$bus,addr=$diskAddress,iothread=io2,hotplug=off \ -device virtio-scsi-pci,id=${diskId}b,bus=$bus,addr=$diskAddress,iothread=io2,hotplug=off \
-device scsi-hd,drive=${diskId},bus=${diskId}b.0,channel=0,scsi-id=0,lun=0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}" -device scsi-hd,drive=${diskId},bus=${diskId}b.0,channel=0,scsi-id=0,lun=0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}${options}"
echo "$result" echo "$result"
;; ;;
esac esac
@@ -554,6 +571,8 @@ finishDisks () {
case "${DISK_TYPE,,}" in case "${DISK_TYPE,,}" in
"blk" | "scsi" | "virtio-blk" | "virtio-scsi" ) "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" ;; [[ "$DISK_OPTS" != *" -object iothread,id=io2"* ]] && DISK_OPTS+=" -object iothread,id=io2" ;;
esac esac
@@ -608,6 +627,8 @@ addDisk () {
previousExt=$(fmt2ext "$previousFmt") 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" ] && if [ -f "$diskBase.$previousExt" ] &&
[ -s "$diskBase.$previousExt" ]; then [ -s "$diskBase.$previousExt" ]; then
convertDisk "$diskBase.$previousExt" "$previousFmt" "$diskFile" "$diskFmt" "$diskBase" "$diskDesc" "$fs" || exit $? convertDisk "$diskBase.$previousExt" "$previousFmt" "$diskFile" "$diskFmt" "$diskBase" "$diskDesc" "$fs" || exit $?
@@ -640,6 +661,8 @@ addDisk () {
fi 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 if [ -f "$diskFile" ] && disabled "$ALLOCATE"; then
currentSize=$(getSize "$diskFile") || exit 73 currentSize=$(getSize "$diskFile") || exit 73
@@ -694,6 +717,8 @@ addDevice () {
[ -z "$diskDev" ] && return 0 [ -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 [ ! -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. # 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. # Whole disk passthrough with explicit sector sizes causes DSM not to recognize the disk.
if [[ "$devType" == "part" ]]; then if [[ "$devType" == "part" ]]; then
@@ -771,6 +796,11 @@ if [[ "$DISK_FLAGS" =~ [[:space:]] ]]; then
exit 78 exit 78
fi fi
if [[ "$DISK_OPTIONS" =~ [[:space:]] ]]; then
error "Invalid DISK_OPTIONS value '$DISK_OPTIONS', spaces are not allowed."
exit 78
fi
if [ -z "$ALLOCATE" ]; then if [ -z "$ALLOCATE" ]; then
ALLOCATE="N" ALLOCATE="N"
fi fi
@@ -783,6 +813,8 @@ else
DISK_ALLOC="preallocation=falloc" DISK_ALLOC="preallocation=falloc"
fi 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 "$BOOT" "$DISK_TYPE" "1" "0xa" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
DISK_OPTS+=$(createDevice "$SYSTEM" "$DISK_TYPE" "2" "0xb" "raw" "$DISK_IO" "$DISK_CACHE" "" "") DISK_OPTS+=$(createDevice "$SYSTEM" "$DISK_TYPE" "2" "0xb" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
@@ -822,6 +854,8 @@ DISK_DEVICES=( "$DEVICE" "$DEVICE2" "$DEVICE3" "$DEVICE4" )
DISK_INDEXES=( "3" "4" "5" "6" ) DISK_INDEXES=( "3" "4" "5" "6" )
DISK_ADDRESSES=( "0xc" "0xd" "0xe" "0xf" ) 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 for i in "${!DISK_FILES[@]}"; do
if [ -n "${DISK_DEVICES[i]}" ]; then if [ -n "${DISK_DEVICES[i]}" ]; then
+8
View File
@@ -17,8 +17,12 @@ RENDERNODE=$(strip "$RENDERNODE")
CPU_VENDOR=$(lscpu | awk '/Vendor ID/{print $3}') 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 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" [[ "${DISPLAY,,}" == "none" ]] && VGA="none"
if enabled "$LOSSY" && [[ "${DISPLAY,,}" == vnc=* ]]; then if enabled "$LOSSY" && [[ "${DISPLAY,,}" == vnc=* ]]; then
@@ -40,6 +44,8 @@ DISPLAY_OPTS+=" -vga $VGA"
[ ! -d /dev/dri ] && mkdir -m 755 /dev/dri [ ! -d /dev/dri ] && mkdir -m 755 /dev/dri
# Extract the card number from the render node # 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_NUMBER=$(echo "$RENDERNODE" | grep -oP '(?<=renderD)\d+')
CARD_DEVICE="/dev/dri/card$((CARD_NUMBER - 128))" 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." warn "render device '${RENDERNODE}' is unavailable or inaccessible."
fi 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 "xserver-xorg-video-intel" "Intel GPU drivers"
addPackage "qemu-system-modules-opengl" "OpenGL module" addPackage "qemu-system-modules-opengl" "OpenGL module"
+2 -2
View File
@@ -9,7 +9,8 @@ cd /run
. start.sh # Startup hook . start.sh # Startup hook
. utils.sh # Load functions . utils.sh # Load functions
. reset.sh # Initialize system . init.sh # Initialize system
. memory.sh # Check memory
. server.sh # Start webserver . server.sh # Start webserver
. install.sh # Run installation . install.sh # Run installation
. disk.sh # Initialize disks . disk.sh # Initialize disks
@@ -18,7 +19,6 @@ cd /run
. proc.sh # Initialize processor . proc.sh # Initialize processor
. serial.sh # Initialize serialport . serial.sh # Initialize serialport
. power.sh # Configure shutdown . power.sh # Configure shutdown
. memory.sh # Check available memory
. config.sh # Configure arguments . config.sh # Configure arguments
. finish.sh # Finish initialization . finish.sh # Finish initialization
+4 -1
View File
@@ -2,7 +2,10 @@
set -Eeuo pipefail set -Eeuo pipefail
if enabled "$DEBUG"; then if enabled "$DEBUG"; then
printf "QEMU arguments:\n\n%s\n\n" "${ARGS// -/$'\n-'}" printf "QEMU arguments:\n\n %s\n\n" "${ARGS// -/$'\n -'}"
fi fi
# Must always remain the very last command
enableTrap
return 0 return 0
+39 -98
View File
@@ -43,6 +43,8 @@ detectRootless() {
local uid_map 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) uid_map=$(awk '{$1=$1; print}' /proc/self/uid_map 2>/dev/null || true)
if [[ "$uid_map" == "0 0 4294967295" ]]; then if [[ "$uid_map" == "0 0 4294967295" ]]; then
@@ -66,6 +68,8 @@ checkPrivileged() {
last_cap=$(cat /proc/sys/kernel/cap_last_cap) last_cap=$(cat /proc/sys/kernel/cap_last_cap)
# Calculate the maximum capability value # 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)) local max_cap=$(((1 << (last_cap + 1)) - 1))
if [ "$cap_bnd" -eq "$max_cap" ]; then if [ "$cap_bnd" -eq "$max_cap" ]; then
@@ -75,15 +79,15 @@ checkPrivileged() {
return 0 return 0
} }
normalizeCpuCores() { checkCores() {
CPU_CORES=$(strip "$CPU_CORES") CPU_CORES=$(strip "$CPU_CORES")
[ -z "$CPU_CORES" ] && CPU_CORES=2 [ -z "$CPU_CORES" ] && CPU_CORES=2
[[ "${CPU_CORES,,}" == "max" ]] && CPU_CORES="$CORES" [[ "${CPU_CORES,,}" == "max" ]] && CPU_CORES="$CORES"
[[ "${CPU_CORES,,}" == "half" ]] && CPU_CORES=$(( CORES / 2 )) [[ "${CPU_CORES,,}" == "half" ]] && CPU_CORES=$(( CORES / 2 ))
[ -z "${CPU_CORES##*[!0-9]*}" ] && error "Invalid amount of CPU_CORES: $CPU_CORES" && exit 15 [ -z "${CPU_CORES##*[!0-9]*}" ] && error "Invalid amount of CPU_CORES: $CPU_CORES" && exit 15
[ "$CPU_CORES" -lt "1" ] && CPU_CORES=1 [ "$CPU_CORES" -lt "1" ] && CPU_CORES=1
if [ "$CPU_CORES" -gt "$CORES" ]; then 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." warn "The amount for CPU_CORES (${CPU_CORES}) exceeds the amount of logical cores available (${CORES}) and will be limited."
CPU_CORES="$CORES" CPU_CORES="$CORES"
@@ -92,6 +96,20 @@ normalizeCpuCores() {
return 0 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() { checkStorage() {
# Check system # Check system
@@ -101,6 +119,8 @@ checkStorage() {
if [ ! -d "/dev/shm" ]; then if [ ! -d "/dev/shm" ]; then
error "Directory /dev/shm not found!" && exit 14 error "Directory /dev/shm not found!" && exit 14
else 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" [ ! -d "$QEMU_DIR" ] && ln -s /dev/shm "$QEMU_DIR"
fi fi
@@ -109,7 +129,9 @@ checkStorage() {
# Check folder # Check folder
if [[ "${STORAGE,,}" != "/storage" ]]; then if [[ "${STORAGE,,}" != "/storage" ]]; then
mkdir -p "$STORAGE" if ! mkdir -p -- "$STORAGE"; then
error "Cannot create storage folder ($STORAGE)!" && exit 13
fi
fi fi
if [ ! -d "$STORAGE" ]; then if [ ! -d "$STORAGE" ]; then
@@ -125,11 +147,9 @@ checkStorage() {
return 0 return 0
} }
checkFilesystem() { checkHost() {
# Check filesystem # Check filesystem
FS=$(stat -f -c %T "$STORAGE")
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then
DISK_IO="threads" DISK_IO="threads"
DISK_CACHE="writeback" DISK_CACHE="writeback"
@@ -138,86 +158,6 @@ checkFilesystem() {
return 0 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() { checkKvm() {
# Check KVM support # Check KVM support
@@ -231,6 +171,8 @@ checkKvm() {
if disabled "$KVM"; then if disabled "$KVM"; then
warn "KVM acceleration is disabled, this will cause the machine to run about 10 times slower!" warn "KVM acceleration is disabled, this will cause the machine to run about 10 times slower!"
else else
# KVM accelerates only matching host and guest instruction sets; cross-
# architecture execution must fall back to software emulation.
if [[ "${ARCH,,}" != "$TARGET" ]]; then if [[ "${ARCH,,}" != "$TARGET" ]]; then
KVM="N" 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." warn "your CPU architecture is ${ARCH^^} and cannot provide KVM acceleration for ${PLATFORM^^} instructions, so the machine will run about 10 times slower."
@@ -254,7 +196,7 @@ checkKvm() {
fi fi
if ! grep -qw "sse4_2" <<< "$flags"; then if ! grep -qw "sse4_2" <<< "$flags"; then
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!" error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
! enabled "$DEBUG" && exit 88 enabled "$DEBUG" || exit 88
fi fi
fi fi
fi fi
@@ -275,7 +217,7 @@ checkKvm() {
error "KVM acceleration is not available $KVM_ERR, this will cause the machine to run about 10 times slower." error "KVM acceleration is not available $KVM_ERR, this will cause the machine to run about 10 times slower."
error "See the FAQ for possible causes, or disable acceleration by adding the \"KVM=N\" variable (not recommended)." ;; error "See the FAQ for possible causes, or disable acceleration by adding the \"KVM=N\" variable (not recommended)." ;;
esac esac
! enabled "$DEBUG" && exit 88 enabled "$DEBUG" || exit 88
fi fi
fi fi
@@ -288,6 +230,7 @@ checkKvm() {
TZ=$(strip "$TZ") TZ=$(strip "$TZ")
STORAGE=$(strip "$STORAGE") STORAGE=$(strip "$STORAGE")
COUNTRY=$(strip "$COUNTRY") COUNTRY=$(strip "$COUNTRY")
MACHINE=$(strip "${MACHINE,,}")
DISK_SIZE=$(strip "$DISK_SIZE") DISK_SIZE=$(strip "$DISK_SIZE")
# Helper variables # Helper variables
@@ -315,22 +258,17 @@ SOCKETS=1
CPU=$(cpu) CPU=$(cpu)
SYS=$(uname -r) SYS=$(uname -r)
ARCH=$(dpkg --print-architecture) ARCH=$(dpkg --print-architecture)
IFS=. read -r KERNEL MINOR _ <<< "$SYS"
CORES=$(grep -c '^processor' /proc/cpuinfo) CORES=$(grep -c '^processor' /proc/cpuinfo)
IFS=. read -r KERNEL MINOR _ <<< "$SYS"
if grep -qi "socket(s)" <<< "$(lscpu)"; then checkSockets
SOCKETS=$(lscpu | grep -m 1 -i 'socket(s)' | awk '{print $2}') checkCores
[ -z "${SOCKETS##*[!0-9]*}" ] && SOCKETS=1
[ "$SOCKETS" -lt "1" ] && SOCKETS=1
fi
normalizeCpuCores
checkStorage checkStorage
checkFilesystem getMemoryInfo
normalizeRamSize
# Print system info # Print system info
SYS="${SYS/-generic/}" SYS="${SYS/-generic/}"
FS=$(stat -f -c %T "$STORAGE")
FS="${FS/UNKNOWN //}" FS="${FS/UNKNOWN //}"
FS="${FS/ext2\/ext3/ext4}" FS="${FS/ext2\/ext3/ext4}"
FS=$(echo "$FS" | sed 's/[)(]//g') 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 " CPU: ${CPU} | RAM: ${AVAIL_MEM/ GB/}/$TOTAL_MEM | DISK: $SPACE_GB (${FS}) | KERNEL: ${SYS}"
echo echo
checkHost
checkKvm checkKvm
# Runtime state is intentionally discarded at each container start; persistent
# machine and disk identity lives under STORAGE instead.
# Cleanup files # Cleanup files
rm -f "$QEMU_DIR"/dsm.url rm -f "$QEMU_DIR"/dsm.url
rm -f "$QEMU_DIR"/{qemu.*,*.{pid,sock,pipe}} rm -f "$QEMU_DIR"/{qemu.*,*.{pid,sock,pipe}}
+28 -4
View File
@@ -3,6 +3,8 @@ set -Eeuo pipefail
: "${URL:=""}" # URL of the PAT file to be downloaded. : "${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 if [ -f "$STORAGE/dsm.ver" ]; then
BASE=$(<"$STORAGE/dsm.ver") BASE=$(<"$STORAGE/dsm.ver")
BASE="${BASE//[![:print:]]/}" BASE="${BASE//[![:print:]]/}"
@@ -16,6 +18,8 @@ FN="boot.pat"
DIR=$(find / -maxdepth 1 -type d -iname "$FN" -print -quit) DIR=$(find / -maxdepth 1 -type d -iname "$FN" -print -quit)
[ ! -d "$DIR" ] && DIR=$(find "$STORAGE" -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 if [ -d "$DIR" ]; then
BASE="DSM_VirtualDSM" && URL="file://$DIR" BASE="DSM_VirtualDSM" && URL="file://$DIR"
if [[ ! -s "$STORAGE/$BASE.boot.img" || ! -s "$STORAGE/$BASE.system.img" ]]; then 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") 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 if [ -n "$URL" ] && [ ! -s "$FILE" ] && [ ! -d "$DIR" ]; then
BASE=$(basename "$URL" .pat) BASE=$(basename "$URL" .pat)
if [ ! -s "$STORAGE/$BASE.system.img" ]; then if [ ! -s "$STORAGE/$BASE.system.img" ]; then
@@ -42,6 +48,8 @@ if [ -n "$URL" ] && [ ! -s "$FILE" ] && [ ! -d "$DIR" ]; then
fi fi
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 if [[ -s "$STORAGE/$BASE.boot.img" && -s "$STORAGE/$BASE.system.img" ]]; then
return 0 # Previous installation found return 0 # Previous installation found
fi fi
@@ -55,6 +63,8 @@ DL_GLOBAL="https://global.synologydownload.com/download/DSM"
[[ "${URL,,}" == *"cndl.synology"* ]] && DL="$DL_CHINA" [[ "${URL,,}" == *"cndl.synology"* ]] && DL="$DL_CHINA"
[[ "${URL,,}" == *"global.synology"* ]] && DL="$DL_GLOBAL" [[ "${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 if [ -z "$DL" ]; then
[ -z "$COUNTRY" ] && setCountry [ -z "$COUNTRY" ] && setCountry
[ -z "$COUNTRY" ] && info "Warning: could not detect country to select mirror!" [ -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 error "Unable to install on $FS filesystems, please use a different filesystem for /storage." && exit 61
fi 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 if [[ "${FS,,}" != "exfat"* && "${FS,,}" != "ntfs"* && "${FS,,}" != "unknown"* ]]; then
TMP="$STORAGE/tmp" TMP="$STORAGE/tmp"
rm -rf "$TMP" rm -rf "$TMP"
@@ -206,6 +218,8 @@ fi
SIZE=$(stat -c%s "$PAT") 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 if ((SIZE<250000000)); then
error "The specified PAT file is probably an update pack as it's too small." && exit 62 error "The specified PAT file is probably an update pack as it's too small." && exit 62
fi fi
@@ -213,6 +227,8 @@ fi
MSG="Extracting installation image..." MSG="Extracting installation image..."
info "Install: $MSG" && html "$MSG" 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 if { tar tf "$PAT"; } >/dev/null 2>&1; then
tar xpf "$PAT" -C "$TMP/." tar xpf "$PAT" -C "$TMP/."
@@ -231,6 +247,8 @@ fi
MSG="Preparing system partition..." MSG="Preparing system partition..."
info "Install: $MSG" && html "$MSG" 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) BOOT=$(find "$TMP" -name "*.bin.zip" -print -quit)
[ -z "$BOOT" ] && error "The PAT file contains no boot image." && exit 67 [ -z "$BOOT" ] && error "The PAT file contains no boot image." && exit 67
[ ! -s "$BOOT" ] && error "The PAT boot image archive is empty." && exit 67 [ ! -s "$BOOT" ] && error "The PAT boot image archive is empty." && exit 67
@@ -254,7 +272,7 @@ if ! touch "$SYSTEM"; then
error "Could not create file $SYSTEM for the system disk." && exit 98 error "Could not create file $SYSTEM for the system disk." && exit 98
fi fi
! setOwner "$SYSTEM" && warn "failed to set the owner for \"$SYSTEM\" !" setOwner "$SYSTEM" || warn "failed to set the owner for \"$SYSTEM\" !"
if [[ "${FS,,}" == "btrfs" ]]; then if [[ "${FS,,}" == "btrfs" ]]; then
{ chattr +C "$SYSTEM"; } || : { chattr +C "$SYSTEM"; } || :
@@ -273,6 +291,8 @@ if ! fallocate -l "$SYSTEM_SIZE" "$SYSTEM" &>/dev/null; then
fi fi
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" PART="$TMP/partition.fdisk"
{ {
@@ -320,6 +340,8 @@ OFFSET="1048576" # 2048 * 512
NUMBLOCKS="2097152" # (16777216 * 512) / 4096 NUMBLOCKS="2097152" # (16777216 * 512) / 4096
MSG="Installing system partition..." 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;\ fakeroot -- bash -c "set -Eeu;\
[ -s $HDP.txz ] && tar xpfJ $HDP.txz --absolute-names -C $MOUNT/;\ [ -s $HDP.txz ] && tar xpfJ $HDP.txz --absolute-names -C $MOUNT/;\
[ -s $IDB.txz ] && tar xpfJ $IDB.txz --absolute-names -C $INDEX_DB/;\ [ -s $IDB.txz ] && tar xpfJ $IDB.txz --absolute-names -C $INDEX_DB/;\
@@ -329,8 +351,10 @@ fakeroot -- bash -c "set -Eeu;\
rm -rf "$MOUNT" rm -rf "$MOUNT"
echo "$BASE" > "$STORAGE/dsm.ver" echo "$BASE" > "$STORAGE/dsm.ver"
! setOwner "$STORAGE/dsm.ver" && warn "failed to set the owner for \"$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 if [[ "$URL" == "file://$STORAGE/$BASE.pat" ]]; then
rm -f "$PAT" rm -f "$PAT"
else else
@@ -338,11 +362,11 @@ else
fi fi
if [ -f "$STORAGE/$BASE.pat" ]; then if [ -f "$STORAGE/$BASE.pat" ]; then
! setOwner "$STORAGE/$BASE.pat" && warn "failed to set the owner for \"$STORAGE/$BASE.pat\" !" setOwner "$STORAGE/$BASE.pat" || warn "failed to set the owner for \"$STORAGE/$BASE.pat\" !"
fi fi
mv -f "$BOOT" "$STORAGE/$BASE.boot.img" mv -f "$BOOT" "$STORAGE/$BASE.boot.img"
! setOwner "$STORAGE/$BASE.boot.img" && warn "failed to set the owner for \"$STORAGE/$BASE.boot.img\" !" setOwner "$STORAGE/$BASE.boot.img" || warn "failed to set the owner for \"$STORAGE/$BASE.boot.img\" !"
rm -rf "$TMP" rm -rf "$TMP"
+152 -29
View File
@@ -1,78 +1,144 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
msg="Checking memory..." normalizeMemory() {
enabled "$DEBUG" && echo "$msg"
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 return 0
} }
checkConfiguredMemory() { checkConfiguredMemory() {
local final="$1"
if disabled "$RAM_CHECK" || [[ "${RAM_SIZE,,}" == "max" || "${RAM_SIZE,,}" == "half" ]]; then if disabled "$RAM_CHECK" || [[ "${RAM_SIZE,,}" == "max" || "${RAM_SIZE,,}" == "half" ]]; then
return 0 return 0
fi fi
local wanted msg avail_mem local wanted avail_mem
wanted=$(numfmt --from=iec "$RAM_SIZE") wanted=$(numfmt --from=iec "$RAM_SIZE")
avail_mem=$(formatBytes "$RAM_AVAIL") avail_mem=$(formatBytes "$RAM_AVAIL")
if (( (wanted + RAM_SPARE) > RAM_AVAIL )); then 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 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 else
RAM_SIZE="max" 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 fi
else else
if (( (wanted + (RAM_SPARE * 3)) > RAM_AVAIL )); then 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 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 else
warn "$msg please consider a lower amount." enabled "$final" && warn "$msg please consider a lower amount."
fi fi
fi fi
fi fi
return 0 return 0
} }
configureHalfMemory() { configureHalfMemory() {
local wanted
if [[ "${RAM_SIZE,,}" != "half" ]]; then if [[ "${RAM_SIZE,,}" != "half" ]]; then
return 0 return 0
fi fi
if (( (RAM_AVAIL / 2) > RAM_SPARE )); then if (( (RAM_AVAIL / 2) > RAM_SPARE )); then
wanted=$(( (RAM_AVAIL / 2) / 1048577 ))
RAM_SIZE="${wanted}M" local wanted=$(( RAM_AVAIL / 2 ))
info "Allocated $wanted MB of RAM for $(app)."
# Divide by one byte more than a MiB to round down
local target=$(( wanted / 1048577 ))
RAM_SIZE="${target}M"
RAM_ALLOCATION="$wanted"
else else
RAM_SIZE="max" RAM_SIZE="max"
fi fi
return 0 return 0
} }
configureMaxMemory() { configureMaxMemory() {
local wanted
if [[ "${RAM_SIZE,,}" != "max" ]]; then if [[ "${RAM_SIZE,,}" != "max" ]]; then
return 0 return 0
fi 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 if (( RAM_AVAIL < (RAM_SPARE * 2) )); then
wanted=$(( RAM_AVAIL / 2 )) local wanted=$(( RAM_AVAIL / 2 ))
else else
wanted=$(( RAM_AVAIL - (RAM_SPARE * 3) )) local wanted=$(( RAM_AVAIL - (RAM_SPARE * 3) ))
if (( wanted < (RAM_SPARE * 6) )); then if (( wanted < (RAM_SPARE * 6) )); then
wanted=$(( RAM_AVAIL - RAM_SPARE )) wanted=$(( RAM_AVAIL - RAM_SPARE ))
@@ -80,33 +146,90 @@ configureMaxMemory() {
fi fi
wanted=$(( wanted / 1048577 )) # Divide by one byte more than a MiB to round down
RAM_SIZE="${wanted}M" 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 return 0
} }
checkMinimumMemory() { checkMinimumMemory() {
local wanted
local wanted
wanted=$(numfmt --from=iec "$RAM_SIZE") wanted=$(numfmt --from=iec "$RAM_SIZE")
if [ "$wanted" -lt "$RAM_MINIMUM" ]; then 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 exit 16
fi fi
return 0 return 0
} }
getMemoryInfo checkMemoryAllocation() {
checkConfiguredMemory local final="${1:-N}"
configureHalfMemory local configured
configureMaxMemory
checkMinimumMemory 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 return 0
+124 -55
View File
@@ -307,6 +307,27 @@ containerID() {
return 0 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() { disableIPv6() {
local dev="$1" local dev="$1"
@@ -371,6 +392,8 @@ natGuestIP() {
local start="30" local start="30"
fi 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 for (( second=start; second<=254; second++ )); do
guest=$(guestIP "172.$second.$third.0" 2) guest=$(guestIP "172.$second.$third.0" 2)
subnet=$(networkCIDR "$guest") || return 1 subnet=$(networkCIDR "$guest") || return 1
@@ -405,36 +428,6 @@ natGuestIP() {
return 1 return 1
} }
kernelAtLeast() {
local major="$1"
local minor="${2:-0}"
(( KERNEL > major || (KERNEL == major && MINOR >= minor) ))
}
canBindToDevice() {
local dev="$1"
[ -n "$dev" ] || return 1
kernelAtLeast 5 7 || return 1
[ -d "/sys/class/net/$dev" ] || return 1
command -v python3 > /dev/null 2>&1 || return 0
python3 - "$dev" > /dev/null 2>&1 <<'PY'
import socket
import sys
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.setsockopt(
socket.SOL_SOCKET,
socket.SO_BINDTODEVICE,
sys.argv[1].encode() + b"\0",
)
PY
}
# ###################################### # ######################################
# DNS / port helpers # DNS / port helpers
# ###################################### # ######################################
@@ -449,6 +442,7 @@ configureDNS() {
local gateway="$6" local gateway="$6"
local upstream="${7:-}" local upstream="${7:-}"
local arguments="$DNSMASQ_OPTS" local arguments="$DNSMASQ_OPTS"
local pid
if ! echo "$gateway" > /run/shm/qemu.gw; then if ! echo "$gateway" > /run/shm/qemu.gw; then
error "Failed to write gateway file." error "Failed to write gateway file."
@@ -458,7 +452,10 @@ configureDNS() {
enabled "${DNSMASQ_DISABLE:-}" && return 0 enabled "${DNSMASQ_DISABLE:-}" && return 0
enabled "$DEBUG" && echo "Starting dnsmasq daemon..." enabled "$DEBUG" && echo "Starting dnsmasq daemon..."
[ -s "$DNSMASQ_PID" ] && pKill "$(<"$DNSMASQ_PID")" if readPidFile pid "$DNSMASQ_PID"; then
pKill "$pid"
fi
rm -f "$DNSMASQ_PID" rm -f "$DNSMASQ_PID"
if isNAT; then if isNAT; then
@@ -518,7 +515,7 @@ configureDNS() {
arguments+=" --log-facility=$log" arguments+=" --log-facility=$log"
arguments=$(echo "$arguments" | sed 's/\t/ /g' | tr -s ' ' | sed 's/^ *//') arguments=$(echo "$arguments" | sed 's/\t/ /g' | tr -s ' ' | sed 's/^ *//')
enabled "$DEBUG" && printf "Dnsmasq arguments:\n\n%s\n\n" "${arguments// -/$'\n-'}" enabled "$DEBUG" && printf "Dnsmasq arguments:\n\n %s\n\n" "${arguments// -/$'\n -'}"
{ $DNSMASQ ${arguments:+ $arguments}; local rc=$?; } || : { $DNSMASQ ${arguments:+ $arguments}; local rc=$?; } || :
@@ -541,11 +538,12 @@ configureDNS() {
return 0 return 0
} }
getHostPorts() { normalizePorts() {
local list="${HOST_PORTS// /}," local list="$1"
local port ports="" local mode="${2:-tcp}"
local mode="${1:-tcp}" local port num
local ports=""
for port in ${list//,/ }; do for port in ${list//,/ }; do
@@ -554,42 +552,87 @@ getHostPorts() {
case "$mode" in case "$mode" in
"tcp" ) "tcp" )
[[ "$port" == *"/udp" ]] && continue [[ "$port" == *"/udp" ]] && continue
local num="${port%/tcp}" num="${port%/tcp}"
[ -n "$num" ] && ports+="$num,"
;; ;;
"all" ) "all" )
if [[ "$port" == *"/udp" ]]; then if [[ "$port" == *"/udp" ]]; then
local num="${port%/udp}" num="${port%/udp}"
[ -n "$num" ] && ports+="$num/udp," [ -n "$num" ] && ports+="$num/udp,"
else else
local num="${port%/tcp}" num="${port%/tcp}"
[ -n "$num" ] && ports+="$num/tcp," [ -n "$num" ] && ports+="$num/tcp,"
fi fi
continue
;; ;;
*) *)
return 1 return 1
;; ;;
esac esac
[ -n "$num" ] && ports+="$num,"
done done
# Remove duplicates # Remove duplicates
ports=$(echo "${ports//,,/,}," | awk 'BEGIN{RS=ORS=","} !seen[$0]++' | sed 's/,*$//g') echo "${ports//,,/,}," | awk 'BEGIN{RS=ORS=","} !seen[$0]++' | sed 's/,*$//g'
echo "$ports"
return 0 return 0
} }
getReservedPorts() {
local list=""
local mode="${1:-tcp}"
# Reserve the DNS port while the internal dnsmasq resolver is active.
if ! enabled "${DNSMASQ_DISABLE:-}" && ! isNAT; then
list+="53/tcp,53/udp,"
fi
normalizePorts "$list" "$mode"
return $?
}
getCustomHostPorts() {
local mode="${1:-tcp}"
local reserved user port
local ports=""
reserved=$(getReservedPorts "all")
user=$(normalizePorts "$HOST_PORTS" "all")
for port in ${user//,/ }; do
[[ ",$reserved," == *",$port,"* ]] && continue
ports+="$port,"
done
normalizePorts "$ports" "$mode"
return $?
}
getHostPorts() {
local mode="${1:-tcp}"
local reserved custom
# Merge internal reservations with user-defined host ports without mutating HOST_PORTS.
# User entries already covered by an internal reservation are silently ignored.
reserved=$(getReservedPorts "all")
custom=$(getCustomHostPorts "all")
normalizePorts "$reserved,$custom" "$mode"
return $?
}
getUserPorts() { 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 defaults="22/tcp,5000/tcp,5001/tcp"
local list="$defaults,${USER_PORTS// /}," local list="$defaults,${USER_PORTS// /},"
local ports="" local ports=""
local userport hostport exclude local userport hostport exclude reserved
reserved=$(getReservedPorts "all")
exclude=$(getHostPorts "all") exclude=$(getHostPorts "all")
for userport in ${list//,/ }; do for userport in ${list//,/ }; do
@@ -611,7 +654,9 @@ getUserPorts() {
if [[ "$num/$proto" == "$hostport" ]]; then if [[ "$num/$proto" == "$hostport" ]]; then
if [[ "$hostport" != "${WEB_PORT:-}/tcp" ]]; then if [[ ",$reserved," == *",$hostport,"* ]]; then
warn "Could not assign port $hostport to \"USER_PORTS\" because it is reserved by the container!"
elif [[ "$hostport" != "${WEB_PORT:-}/tcp" ]]; then
warn "Could not assign port $hostport to \"USER_PORTS\" because it is already in \"HOST_PORTS\"!" warn "Could not assign port $hostport to \"USER_PORTS\" because it is already in \"HOST_PORTS\"!"
fi fi
@@ -621,8 +666,14 @@ getUserPorts() {
done 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 done
# Remove duplicates # Remove duplicates
@@ -659,7 +710,6 @@ getSlirp() {
getPasst() { getPasst() {
local list port local list port
local bind="$UPLINK"
local tcp="" udp="" args="" local tcp="" udp="" args=""
list=$(getUserPorts) list=$(getUserPorts)
@@ -689,12 +739,8 @@ getPasst() {
tcp="${tcp%,}" tcp="${tcp%,}"
udp="${udp%,}" udp="${udp%,}"
if canBindToDevice "$DEV"; then [ -n "$tcp" ] && args+=" -t $tcp"
bind="%$DEV" [ -n "$udp" ] && args+=" -u $udp"
fi
[ -n "$tcp" ] && args+=" -t $bind/$tcp"
[ -n "$udp" ] && args+=" -u $bind/$udp"
echo "$args" echo "$args"
return 0 return 0
@@ -884,6 +930,7 @@ configurePasst() {
PASST_OPTS+=" -H $HOST" PASST_OPTS+=" -H $HOST"
PASST_OPTS+=" -M $GATEWAY_MAC" PASST_OPTS+=" -M $GATEWAY_MAC"
PASST_OPTS+=" --runas $EUID:$(id -g)"
PASST_OPTS+=" -P $PASST_PID" PASST_OPTS+=" -P $PASST_PID"
PASST_OPTS+=" -s $PASST_SOCKET" PASST_OPTS+=" -s $PASST_SOCKET"
PASST_OPTS+=" -l $log" PASST_OPTS+=" -l $log"
@@ -945,6 +992,17 @@ configurePasst() {
return 0 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() { createBridge() {
local gateway="$1" local gateway="$1"
@@ -1901,7 +1959,10 @@ validateHost() {
validateHostPorts() { validateHostPorts() {
if isNAT && [[ "${HOST_PORTS,,}" == *"/udp"* ]]; then local custom
custom=$(getCustomHostPorts "all")
if isNAT && [[ "$custom" == *"/udp"* ]]; then
warn "UDP ports in \"HOST_PORTS\" are not yet implemented for NAT networking." warn "UDP ports in \"HOST_PORTS\" are not yet implemented for NAT networking."
fi fi
@@ -2111,6 +2172,7 @@ showHostInfo() {
echo " DNS: $nameservers" echo " DNS: $nameservers"
fi fi
enabled "$DEBUG" && echo
return 0 return 0
} }
@@ -2165,6 +2227,7 @@ initializeNetwork() {
configureMTU configureMTU
configureMAC configureMAC
configureBridge
showHostInfo showHostInfo
@@ -2218,6 +2281,8 @@ else
if ! configureNAT; then if ! configureNAT; then
closeInterfaces closeInterfaces
# NAT setup failure is recoverable: tear down partial interfaces and
# continue with the default user-mode backend.
NETWORK="user" NETWORK="user"
if ! enabled "$ROOTLESS" || enabled "$DEBUG"; then if ! enabled "$ROOTLESS" || enabled "$DEBUG"; then
@@ -2265,6 +2330,8 @@ else
fi 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" NET_OPTS+=" -device $ADAPTER,id=net0,netdev=hostnet0,romfile=,mac=$MAC"
if [[ "$GUEST_MTU" != "0" && "$GUEST_MTU" != "1500" ]]; then if [[ "$GUEST_MTU" != "0" && "$GUEST_MTU" != "1500" ]]; then
@@ -2275,6 +2342,8 @@ if [[ "$GUEST_MTU" != "0" && "$GUEST_MTU" != "1500" ]]; then
fi fi
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 if ! echo "$UPLINK" > "$QEMU_DIR"/qemu.ip; then
error "Failed to write QEMU IP file!" error "Failed to write QEMU IP file!"
exit 24 exit 24
+40 -27
View File
@@ -67,16 +67,10 @@ displayReason() {
readQemuPid() { readQemuPid() {
local -n _pid="$1" # Interactive startup uses a wrapper-created PID file before QEMU writes its
local file # own pidfile, so accept either during startup and shutdown races.
readPidFile "$1" "$QEMU_START_PID" && return 0
for file in "$QEMU_START_PID" "$QEMU_PID"; do readPidFile "$1" "$QEMU_PID"
if [ -s "$file" ] && read -r _pid < "$file"; then
return 0
fi
done
return 1
} }
qemuPidFile() { qemuPidFile() {
@@ -100,16 +94,14 @@ waitQemuExit() {
waitQemuPid() { waitQemuPid() {
local -n _pid="$1" local cnt=0
local cnt=0 value
while ! readQemuPid value; do while ! readQemuPid "$1"; do
sleep 0.02 sleep 0.02
cnt=$((cnt + 1)) cnt=$((cnt + 1))
(( cnt >= 50 )) && return 1 (( cnt >= 50 )) && return 1
done done
_pid="$value"
return 0 return 0
} }
@@ -118,8 +110,8 @@ forceKillQemu() {
local reason="$1" local reason="$1"
local pid display local pid display
! readQemuPid pid && return 0 readQemuPid pid || return 0
! isAlive "$pid" && return 0 isAlive "$pid" || return 0
display=$(displayReason "$reason") display=$(displayReason "$reason")
error "Forcefully terminating $(app), reason: $display..." error "Forcefully terminating $(app), reason: $display..."
@@ -194,6 +186,8 @@ startQemu() {
rm -f -- "$QEMU_START_PID" 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 trap '' INT QUIT
@@ -248,6 +242,8 @@ sendGuestShutdown() {
local pid="$1" local pid="$1"
local response 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 # 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' || : # nc -q 1 -w 1 -U "$QEMU_DIR/monitor.sock" &> /dev/null <<<'system_powerdown' || :
@@ -275,6 +271,8 @@ sendGuestShutdown() {
normalizeTimeout() { 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 term_grace=3 # seconds before loop ends to send SIGTERM
local cleanup_grace=3 # seconds reserved after the loop for cleanup local cleanup_grace=3 # seconds reserved after the loop for cleanup
@@ -315,8 +313,10 @@ waitForShutdown() {
local slp=$! local slp=$!
# Stop waiting if the process has exited # Stop waiting if the process has exited
! isAlive "$pid" && break 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 # Workaround for stale/zombie QEMU pid file
[ ! -s "$QEMU_START_PID" ] && [ ! -s "$QEMU_PID" ] && break [ ! -s "$QEMU_START_PID" ] && [ ! -s "$QEMU_PID" ] && break
@@ -335,7 +335,7 @@ waitForShutdown() {
return 0 return 0
} }
graceful_shutdown() { gracefulShutdown() {
local sig="$1" local sig="$1"
local pid code local pid code
@@ -344,8 +344,10 @@ graceful_shutdown() {
code=$(signalCode "$sig") 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 if (( code == 130 && SHUTDOWN_SIGNAL == code )); then
SHUTDOWN_SKIP=1 SHUTDOWN_SKIP=1
echo && info "Received SIGINT again, forcing shutdown..." echo && info "Received SIGINT again, forcing shutdown..."
@@ -356,11 +358,14 @@ graceful_shutdown() {
return return
fi fi
set +e
start=$SECONDS start=$SECONDS
SHUTDOWN_SIGNAL=$code 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" touch "$QEMU_END"
echo && info "Received $sig signal, sending shutdown command..." echo && info "Received $sig signal, sending shutdown command..."
if ! readQemuPid pid; then if ! readQemuPid pid; then
@@ -382,13 +387,21 @@ graceful_shutdown() {
finish "$code" 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" [ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT"
if interactive; then
_trap graceful_shutdown SIGINT
fi
_trap graceful_shutdown SIGTERM SIGHUP SIGABRT SIGQUIT
return 0 return 0
+6
View File
@@ -37,6 +37,8 @@ exitIfShuttingDown() {
queryGuest() { 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=$?; } || : { json=$(curl --unix-socket "$socket" -m 20 -sk "$url"); local rc=$?; } || :
exitIfShuttingDown exitIfShuttingDown
@@ -116,6 +118,8 @@ writeDsmLocation() {
pollGuestLocation() { pollGuestLocation() {
# Keep polling until the guest reports a usable address, but stop promptly
# when container shutdown begins.
while [ ! -s "$file" ]; do while [ ! -s "$file" ]; do
# Check if not shutting down # Check if not shutting down
@@ -183,6 +187,8 @@ buildStaticMessage() {
ip=$(<"$address") ip=$(<"$address")
local port="${location##*:}" 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 if [[ "${nic,,}" != "macvlan" ]]; then
msg="port $port" msg="port $port"
else else
+11 -1
View File
@@ -47,7 +47,7 @@ checkSse42() {
if ! hasFlag "sse4_2"; then if ! hasFlag "sse4_2"; then
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!" error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
! enabled "$DEBUG" && exit 88 enabled "$DEBUG" || exit 88
fi fi
return 0 return 0
@@ -66,6 +66,8 @@ trimSpaces() {
removeCpuArgument() { removeCpuArgument() {
# CPU configuration has dedicated variables. Remove raw -cpu arguments so
# option ordering cannot silently override the validated model and flags.
local args=" ${ARGUMENTS:-} " local args=" ${ARGUMENTS:-} "
while [[ "$args" =~ [[:space:]]-cpu([[:space:]][^[:space:]]+|=[^[:space:]]+)? ]]; do while [[ "$args" =~ [[:space:]]-cpu([[:space:]][^[:space:]]+|=[^[:space:]]+)? ]]; do
@@ -94,6 +96,8 @@ configureKvmCpuModel() {
appendKvmInvtscFeature() { 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 if hasFlag "svm"; then
# AMD processor # AMD processor
@@ -131,6 +135,8 @@ configureTcgCpuModel() {
return 0 return 0
fi fi
# TCG uses the broad max model on native x86, but qemu64 is the compatible
# cross-architecture fallback.
if [[ "$ARCH" == "amd64" ]]; then if [[ "$ARCH" == "amd64" ]]; then
CPU_MODEL="max" CPU_MODEL="max"
CPU_FEATURES+=",migratable=no" CPU_FEATURES+=",migratable=no"
@@ -158,6 +164,8 @@ configureTcg() {
composeCpuFlags() { 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}" CPU_FLAGS="${CPU_MODEL}${CPU_FEATURES:+,$CPU_FEATURES}${CPU_FLAGS:+,$CPU_FLAGS}"
return 0 return 0
@@ -170,6 +178,8 @@ configureHostCpuName() {
fi fi
if [ -n "$HOST_CPU" ]; then 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%%,*},," HOST_CPU="${HOST_CPU%%,*},,"
else else
HOST_CPU="QEMU, Virtual CPU," HOST_CPU="QEMU, Virtual CPU,"
+8
View File
@@ -22,6 +22,8 @@ writeInfo() {
local content="$1" 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 if ! printf '%s\n' "$content" > "$info_tmp"; then
rm -f -- "$info_tmp" rm -f -- "$info_tmp"
return 1 return 1
@@ -220,6 +222,8 @@ fi
trap finishProgress EXIT trap finishProgress EXIT
trap 'exit 0' HUP INT QUIT trap 'exit 0' HUP INT QUIT
# SIGTERM requests one final measurement and web update rather than
# terminating between progress samples.
trap stopProgress TERM trap stopProgress TERM
if [[ "$body" == *"..." ]]; then if [[ "$body" == *"..." ]]; then
@@ -232,6 +236,8 @@ while true; do
bytes=$(getBytes "$path" "$mode") bytes=$(getBytes "$path" "$mode")
effective_total="$total" 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 if [ -n "$status_file" ] && status=$(getStatus "$status_file"); then
read -r status_bytes status_total <<< "$status" read -r status_bytes status_total <<< "$status"
bytes="$status_bytes" bytes="$status_bytes"
@@ -268,6 +274,8 @@ while true; do
fi fi
fi fi
else 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. # Truncate to one decimal so progress is never reported early.
progress=$((bytes * 1000 / effective_total)) progress=$((bytes * 1000 / effective_total))
(( progress > 1000 )) && progress=1000 (( progress > 1000 )) && progress=1000
+18 -8
View File
@@ -42,6 +42,8 @@ validateHostMac() {
buildHostArguments() { 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=()
HOST_ARGS+=("-cpu=$CPU_CORES") HOST_ARGS+=("-cpu=$CPU_CORES")
HOST_ARGS+=("-cpu_arch=$HOST_CPU") HOST_ARGS+=("-cpu_arch=$HOST_CPU")
@@ -60,6 +62,8 @@ startHostBinary() {
local pid 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 rm -f -- "$HOST_PID" "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET" || return 1
if enabled "$HOST_DEBUG"; then if enabled "$HOST_DEBUG"; then
@@ -73,7 +77,7 @@ startHostBinary() {
pid=$! pid=$!
fi fi
echo "$pid" > "$HOST_PID" printf '%s\n' "$pid" > "$HOST_PID"
return 0 return 0
} }
@@ -82,23 +86,24 @@ waitForSocket() {
local socket="$1" local socket="$1"
local exit_code="$2" local exit_code="$2"
local pid cnt=0 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 while [ ! -S "$socket" ]; do
if ! read -r pid < "$HOST_PID" || ! isAlive "$pid"; then if ! readPidFile pid "$HOST_PID" || ! isAlive "$pid"; then
error "qemu-host exited unexpectedly!" error "qemu-host exited unexpectedly!"
exit "$exit_code" exit "$exit_code"
fi fi
sleep 0.1 if (( SECONDS >= deadline )); then
cnt=$((cnt + 1))
if (( cnt > 50 )); then
error "Failed to create qemu-host socket: $socket" error "Failed to create qemu-host socket: $socket"
exit "$exit_code" exit "$exit_code"
fi fi
sleep 0.1
done done
return 0 return 0
@@ -106,6 +111,11 @@ waitForSocket() {
configureSerialPorts() { 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 if enabled "${SHUTDOWN:-Y}" && interactive; then
CONSOLE_SOCKET="$QEMU_DIR/console.sock" CONSOLE_SOCKET="$QEMU_DIR/console.sock"
@@ -123,7 +133,7 @@ configureSerialPorts() {
fi fi
SERIAL_OPTS+=" \ 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 \ -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" -device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=vchannel"
+49 -16
View File
@@ -2,20 +2,19 @@
set -Eeuo pipefail set -Eeuo pipefail
: "${WEB_PORT:="5000"}" # Webserver port : "${WEB_PORT:="5000"}" # Webserver port
: "${WSD_PORT:="8004"}" # Websockets port
# Sanitize port variables # Sanitize port variables
WEB_PORT=$(strip "$WEB_PORT") WEB_PORT=$(strip "$WEB_PORT")
WSD_PORT=$(strip "$WSD_PORT")
WEB_PID="/run/nginx.pid" WEB_PID="/run/nginx.pid"
WSD_LOG="/var/log/websocketd.log" WSD_LOG="/var/log/websocketd.log"
WSD_PID="$QEMU_DIR/websocketd.pid" WSD_PID="$QEMU_DIR/websocketd.pid"
WSD_SOCKET="$QEMU_DIR/status-ws.sock"
prepareWebFiles() { prepareWebFiles() {
cp -r /var/www/* "$QEMU_DIR" || return 1 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 return 0
} }
@@ -24,9 +23,8 @@ configureWebPorts() {
if ! sed -i \ if ! sed -i \
-e "s|listen 5000 default_server;|listen $WEB_PORT default_server;|g" \ -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 /etc/nginx/sites-enabled/web.conf; then
error "Failed to configure webserver ports!" error "Failed to configure webserver port!"
return 1 return 1
fi fi
@@ -35,6 +33,8 @@ configureWebPorts() {
configureIpv6Listen() { 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 [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]]; then
if ! sed -i \ if ! sed -i \
@@ -49,11 +49,32 @@ configureIpv6Listen() {
return 0 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() { configureWebServer() {
mkdir -p /etc/nginx/sites-enabled configureNginx || return 1
cp /etc/nginx/default.conf /etc/nginx/sites-enabled/web.conf
configureWebPorts || return 1 configureWebPorts || return 1
configureIpv6Listen || return 1 configureIpv6Listen || return 1
@@ -64,9 +85,11 @@ stopWebServer() {
local pid local pid
if [ -s "$WEB_PID" ] && read -r pid < "$WEB_PID" && [ -n "$pid" ]; then if readPidFile pid "$WEB_PID"; then
pKill "$pid" 2 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 if isAlive "$pid"; then
kill -9 -- "$pid" 2>/dev/null || : kill -9 -- "$pid" 2>/dev/null || :
fi fi
@@ -88,7 +111,7 @@ stopWebsocketServer() {
local pid local pid
if [ -s "$WSD_PID" ] && read -r pid < "$WSD_PID" && [ -n "$pid" ]; then if readPidFile pid "$WSD_PID"; then
pKill "$pid" 2 pKill "$pid" 2
if isAlive "$pid"; then if isAlive "$pid"; then
@@ -96,7 +119,7 @@ stopWebsocketServer() {
fi fi
fi fi
rm -f -- "$WSD_PID" rm -f -- "$WSD_PID" "$WSD_SOCKET"
return 0 return 0
} }
@@ -104,8 +127,7 @@ startWebsocketServer() {
# Start websocket server # Start websocket server
websocketd \ websocketd \
--address 127.0.0.1 \ --unixsocket="$WSD_SOCKET" \
--port="$WSD_PORT" \
/run/socket.sh \ /run/socket.sh \
>"$WSD_LOG" 2>&1 & >"$WSD_LOG" 2>&1 &
@@ -118,20 +140,31 @@ startWebsocketServer() {
fi fi
local i local i
for (( i = 1; i <= 5; i++ )); do for (( i = 1; i <= 50; i++ )); do
if ! isAlive "$pid"; then if ! isAlive "$pid"; then
rm -f -- "$WSD_PID" rm -f -- "$WSD_PID" "$WSD_SOCKET"
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2 [ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
error "Failed to start websocket server!" error "Failed to start websocket server!"
return 1 return 1
fi fi
[ -S "$WSD_SOCKET" ] && return 0
sleep 0.1 sleep 0.1
done 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 prepareWebFiles
+4
View File
@@ -18,6 +18,8 @@ refresh() {
[[ "$msg" == "$lastmsg" ]] && return 0 [[ "$msg" == "$lastmsg" ]] && return 0
lastmsg="$msg" 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" echo "s: $msg"
return 0 return 0
@@ -37,6 +39,8 @@ inotifywait \
case "${event,,}" in case "${event,,}" in
"delete"* ) "delete"* )
echo "c: vnc" ;; echo "c: vnc" ;;
# moved_to covers the atomic replacement used by html()/writeAtomic(),
# while close_write handles direct writers.
"close_write"* | "moved_to"* ) "close_write"* | "moved_to"* )
refresh ;; refresh ;;
esac esac
+128 -18
View File
@@ -7,6 +7,32 @@ 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; } 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; } 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"
_pid=""
if ! _pid=$(cat -- "$2" 2>/dev/null); then
_pid=""
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
fi
return 0
}
hasFlag() { hasFlag() {
# Match a whitespace-delimited token in /proc/cpuinfo # Match a whitespace-delimited token in /proc/cpuinfo
@@ -29,8 +55,27 @@ isAmdCpu() {
[[ "$vendor" == "AuthenticAMD" ]] [[ "$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() { 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 [ -t 0 ] && : 2>/dev/null </dev/tty >/dev/tty
} }
@@ -118,14 +163,13 @@ isAlive() {
waitPid() { waitPid() {
local i=0
local pid="$1" local pid="$1"
local timeout="${2:-10}" local timeout="${2:-10}"
local deadline=$((SECONDS + timeout))
while [ -n "$pid" ] && isAlive "$pid"; do while [ -n "$pid" ] && isAlive "$pid"; do
(( SECONDS >= deadline )) && return 1
sleep 0.2 sleep 0.2
i=$((i + 1))
(( i >= timeout * 5 )) && return 1
done done
return 0 return 0
@@ -133,18 +177,16 @@ waitPid() {
waitPidFile() { waitPidFile() {
local i=0 pid local pid
local file="$1" local file="$1"
local timeout="${2:-10}" local timeout="${2:-10}"
local deadline=$((SECONDS + timeout))
[ ! -s "$file" ] && return 0 readPidFile pid "$file" || return 0
! read -r pid <"$file" && return 0
[ -z "$pid" ] && return 0
while [ -s "$file" ] && isAlive "$pid"; do while [ -s "$file" ] && isAlive "$pid"; do
(( SECONDS >= deadline )) && return 1
sleep 0.2 sleep 0.2
i=$((i + 1))
(( i >= timeout * 5 )) && return 1
done done
rm -f -- "$file" rm -f -- "$file"
@@ -167,19 +209,19 @@ pKill() {
fWait() { fWait() {
local i=0
local name="$1" local name="$1"
local timeout="${2:-10}" local timeout="${2:-10}"
local deadline=$((SECONDS + timeout))
[ -z "$name" ] && return 0 [ -z "$name" ] && return 0
while pgrep -f -l "$name" >/dev/null; do while pgrep -f -l "$name" >/dev/null; do
sleep 0.2 if (( SECONDS >= deadline )); then
i=$((i + 1))
if (( i >= timeout * 5 )); then
warn "Timed out while waiting for process: $name" warn "Timed out while waiting for process: $name"
break break
fi fi
sleep 0.2
done done
return 0 return 0
@@ -203,9 +245,7 @@ sKill() {
local pid local pid
local file="$1" local file="$1"
[ ! -s "$file" ] && return 0 readPidFile pid "$file" || return 0
! read -r pid <"$file" && return 0
[ -z "$pid" ] && return 0
if isAlive "$pid"; then if isAlive "$pid"; then
{ kill -15 -- "$pid" || :; } 2>/dev/null { kill -15 -- "$pid" || :; } 2>/dev/null
@@ -239,11 +279,13 @@ setOwner() {
[ ! -f "$file" ] && return 1 [ ! -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") dir=$(dirname -- "$file")
uid=$(stat -c '%u' "$dir") || return 1 uid=$(stat -c '%u' "$dir") || return 1
gid=$(stat -c '%g' "$dir") || return 1 gid=$(stat -c '%g' "$dir") || return 1
! chown "$uid:$gid" "$file" && return 1 chown "$uid:$gid" "$file" || return 1
return 0 return 0
} }
@@ -254,7 +296,7 @@ makeDir() {
local dir uid gid local dir uid gid
[ -d "$path" ] && return 0 [ -d "$path" ] && return 0
! mkdir -p "$path" && return 1 mkdir -p "$path" || return 1
dir=$(dirname -- "$path") dir=$(dirname -- "$path")
@@ -271,6 +313,64 @@ makeDir() {
return 0 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() { stateFile() {
local name="$1" local name="$1"
@@ -303,6 +403,8 @@ writeAtomic() {
local path="$1" local path="$1"
local content="$2" 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" local tmp="${path}.${BASHPID}.tmp"
if ! printf '%s\n' "$content" > "$tmp"; then if ! printf '%s\n' "$content" > "$tmp"; then
@@ -367,6 +469,8 @@ restoreState() {
local prefix="${4:-$PROCESS}" local prefix="${4:-$PROCESS}"
local value local value
# Persistent state fills only unset variables unless force is requested,
# preserving explicit environment overrides.
if ! enabled "$force"; then if ! enabled "$force"; then
[ -z "${!var:-}" ] || return 0 [ -z "${!var:-}" ] || return 0
fi fi
@@ -426,6 +530,8 @@ html() {
HTML="${HTML/\[4\]/$footer}" HTML="${HTML/\[4\]/$footer}"
HTML="${HTML/\[5\]/$FOOTER2}" 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 "$PAGE" "$HTML" || return 1
writeAtomic "$INFO" "$body" || return 1 writeAtomic "$INFO" "$body" || return 1
@@ -499,6 +605,8 @@ setCountry() {
[[ "${TZ,,}" == "asia/shanghai" ]] && COUNTRY="CN" [[ "${TZ,,}" == "asia/shanghai" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/chongqing" ]] && 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://api.ipapi.is" ".location.country_code"
[ -z "$COUNTRY" ] && getCountry "https://ifconfig.co/json" ".country_iso" [ -z "$COUNTRY" ] && getCountry "https://ifconfig.co/json" ".country_iso"
[ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code" [ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code"
@@ -523,6 +631,8 @@ addPackage() {
[ -z "$COUNTRY" ] && setCountry [ -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 if [[ "${COUNTRY^^}" == "CN" ]]; then
sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
fi fi
+2 -2
View File
@@ -39,6 +39,6 @@ server {
proxy_read_timeout 3600s; proxy_read_timeout 3600s;
proxy_send_timeout 3600s; proxy_send_timeout 3600s;
proxy_pass http://127.0.0.1:8004/; proxy_pass http://unix:/run/shm/status-ws.sock:/;
} }
} }
+10 -3
View File
@@ -71,6 +71,13 @@ function processInfo() {
var response = request; var response = request;
request = null; request = null;
var status = response.status;
if (status == 502 || status == 503 || status == 504) {
schedule();
return true;
}
var msg = response.responseText; var msg = response.responseText;
if (msg == null || msg.length == 0) { if (msg == null || msg.length == 0) {
@@ -83,9 +90,9 @@ function processInfo() {
return false; return false;
} }
var notFound = (response.status == 404); var notFound = (status == 404);
if (response.status == 200) { if (status == 200) {
if (msg.toLowerCase().indexOf("<html>") !== -1) { if (msg.toLowerCase().indexOf("<html>") !== -1) {
notFound = true; notFound = true;
} else { } else {
@@ -103,7 +110,7 @@ function processInfo() {
return true; return true;
} }
setError("Error: Received statuscode " + response.status); setError("Error: Received statuscode " + status);
return false; return false;
} catch (e) { } catch (e) {