Compare commits

..
39 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
KroeseandGitHub 63e4529eff fix: Add cleanup for failed web server startup (#1333) 2026-07-25 13:47:20 +02:00
KroeseandGitHub afd848cebb fix: Make healthcheck succeed during download (#1332) 2026-07-25 12:16:41 +02:00
KroeseandGitHub 695b075130 feat: Provide host access through system.lan (#1331) 2026-07-25 03:38:36 +02:00
KroeseandGitHub 7eff53e722 feat: Improve disk error handling (#1328) 2026-07-24 13:51:59 +02:00
KroeseandGitHub bcaf0e5980 fix: Improve network error handling (#1327) 2026-07-24 13:27:09 +02:00
KroeseandGitHub faa820c2cc feat: Inline local variable declarations (#1326) 2026-07-24 13:11:40 +02:00
KroeseandGitHub 81e477fcc4 feat: Warn when DSM and container share an IP address (#1325) 2026-07-24 12:45:54 +02:00
KroeseandGitHub abd3a1c3df fix: Network mode shown for DHCP mode (#1324) 2026-07-24 12:03:57 +02:00
KroeseandGitHub 3b59bcd284 fix: Ensure progress reaches 100% (#1322) 2026-07-23 23:03:54 +02:00
KroeseandGitHub f1b56f394a build: Create a detailed .gitignore file (#1321) 2026-07-23 15:42:59 +02:00
KroeseandGitHub 1a983ebcd1 feat: Improve error handling for webserver config (#1320) 2026-07-23 04:17:26 +02:00
KroeseandGitHub 41c76198fa feat: Use atomic writes for progress updates (#1319) 2026-07-23 04:10:09 +02:00
KroeseandGitHub 222b5649b0 fix: Prevent stale polling responses for web status (#1318) 2026-07-23 04:00:12 +02:00
KroeseandGitHub e14fd4b711 docs: Environment variables (#1317) 2026-07-21 13:14:32 +02:00
26 changed files with 1449 additions and 459 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
+268 -1
View File
@@ -1 +1,268 @@
build.sh ##############################
# Operating System Files
##############################
.DS_Store
.AppleDouble
.LSOverride
Thumbs.db
ehthumbs.db
Desktop.ini
Icon?
$RECYCLE.BIN/
.Spotlight-V100/
.Trashes/
.fseventsd
##############################
# IDEs
##############################
.vscode/
.idea/
*.iml
*.ipr
*.iws
##############################
# VS Code
##############################
.history/
*.code-workspace
##############################
# Vim
##############################
*.swp
*.swo
Session.vim
##############################
# Sublime
##############################
*.sublime-workspace
*.sublime-project
##############################
# Temporary Files
##############################
*.tmp
*.temp
*.bak
*.old
*.orig
*.rej
*.save
##############################
# Logs
##############################
*.log
logs/
log/
*.out
*.err
*.trace
##############################
# Runtime
##############################
*.pid
*.seed
*.pid.lock
##############################
# Secrets
##############################
.env
.env.*
!.env.example
*.pem
*.key
*.crt
*.cer
*.p12
*.pfx
*.kdbx
*.secret
*.token
##############################
# SSH
##############################
.ssh/
##############################
# Docker
##############################
docker-compose.override.yml
docker-compose.local.yml
##############################
# VM Storage
##############################
storage/
windows/
downloads/
##############################
# Disk Images
##############################
*.qcow2
*.qcow
*.vhd
*.vhdx
*.vdi
*.raw
*.img
*.iso
*.bin
##############################
# QEMU
##############################
*.nvram
*.fd
*.efi
*.sock
*.monitor
*.serial
##############################
# Samba
##############################
shared/
share/
##############################
# Backups
##############################
backup/
backups/
*.backup
##############################
# Cache
##############################
.cache/
.cache-loader/
.tmp/
temp/
tmp/
##############################
# Python
##############################
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.venv/
venv/
##############################
# Node
##############################
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
##############################
# Build
##############################
dist/
build/
out/
release/
##############################
# Coverage
##############################
coverage/
.coverage
coverage.xml
##############################
# Archives
##############################
*.zip
*.tar
*.tar.gz
*.tgz
*.7z
*.rar
##############################
# Generated Config
##############################
config.local.*
settings.local.*
local.env
##############################
# Test Files
##############################
test-output/
playwright-report/
##############################
# macOS
##############################
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
##############################
# Linux
##############################
*~
.nfs*
##############################
# Windows
##############################
*.stackdump
##############################
# Misc
##############################
*.cache
*.lock
*.lock.json
*.bak.*
##############################
# Keep Examples
##############################
!.gitkeep
!.env.example
+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
+6 -5
View File
@@ -8,8 +8,8 @@ An empty default means the variable is unset and its value is determined automat
| Variable | Default | Description | | Variable | Default | Description |
|---|---|---| |---|---|---|
| `URL` | | URL or local path to the DSM `.pat` installation file. Downloads the default Virtual DSM image automatically when unset. | | `URL` | | URL or local path to a custom `.pat` installation file. |
| `COUNTRY` | | Country code used to select the Synology download mirror. Detected automatically when unset. | | `COUNTRY` | | Country code used to select the Synology download mirror. |
| `HOST_MAC` | | MAC address reported to DSM. | | `HOST_MAC` | | MAC address reported to DSM. |
| `HOST_MODEL` | | Synology host model reported to DSM. | | `HOST_MODEL` | | Synology host model reported to DSM. |
| `HOST_SERIAL` | | Synology host serial number reported to DSM. | | `HOST_SERIAL` | | Synology host serial number reported to DSM. |
@@ -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. |
@@ -47,14 +48,14 @@ An empty default means the variable is unset and its value is determined automat
| Variable | Default | Description | | Variable | Default | Description |
|---|---|---| |---|---|---|
| `NETWORK` | | Network mode, such as `nat`, `user`, or `N` to disable networking. | | `NETWORK` | | Network mode, such as `nat`, `user`, or `N` to disable networking. |
| `DHCP` | `N` | Enables macvtap networking so DSM receives an address from the external LAN through DHCP. | | `DHCP` | `N` | Enables macvtap networking so DSM receives a DHCP address. |
| `HOST` | `VirtualDSM` | Hostname assigned to DSM. | | `HOST` | | Hostname assigned to the machine on the network. |
| `IP` | | Overrides the automatically selected guest IPv4 address. | | `IP` | | Overrides the automatically selected guest IPv4 address. |
| `MAC` | | Guest network adapter MAC address. | | `MAC` | | Guest network adapter MAC address. |
| `ADAPTER` | `virtio-net-pci` | QEMU network adapter model. | | `ADAPTER` | `virtio-net-pci` | QEMU network adapter model. |
| `DEV` | `eth0` | Container network interface used as the uplink. | | `DEV` | `eth0` | Container network interface used as the uplink. |
| `MTU` | | MTU assigned to the guest network interface. | | `MTU` | | MTU assigned to the guest network interface. |
| `MASK` | `255.255.255.0` | IPv4 netmask. | | `MASK` | `255.255.255.0` | IPv4 netmask for guest network. |
| `TAP` | `dsm` | TAP or macvtap interface name. | | `TAP` | `dsm` | TAP or macvtap interface name. |
| `BRIDGE` | `docker` | Bridge name used for NAT networking. | | `BRIDGE` | `docker` | Bridge name used for NAT networking. |
| `HOST_PORTS` | | Ports excluded from guest forwarding. | | `HOST_PORTS` | | Ports excluded from guest forwarding. |
+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.
+5 -1
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"
[ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 1 # 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") 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
+62 -24
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
@@ -154,9 +160,11 @@ normalizeSize() {
local diskDesc="$2" local diskDesc="$2"
local dir="$3" local dir="$3"
local gb free space local free dataSize
local dataSize 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)
@@ -168,12 +176,12 @@ normalizeSize() {
fi fi
(( free < spare )) && free="$spare" (( free < spare )) && free="$spare"
gb=$(( free / 1073741825 )) local gb=$(( free / 1073741825 ))
diskSpace="${gb}G" diskSpace="${gb}G"
fi fi
space="${diskSpace// /}" local space="${diskSpace// /}"
[ -z "$space" ] && space="256G" [ -z "$space" ] && space="256G"
[ -z "${space//[0-9. ]}" ] && space="${space}G" [ -z "${space//[0-9. ]}" ] && space="${space}G"
space=$(echo "${space^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g') space=$(echo "${space^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
@@ -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
@@ -573,13 +592,12 @@ addDisk () {
local diskCache="$9" local diskCache="$9"
local fs dir used space local fs dir used space
local diskExt diskFile local diskExt dataSize
local dataSize missing
local available currentSize local available currentSize
local previousExt previousFmt local previousExt
diskExt=$(fmt2ext "$diskFmt") diskExt=$(fmt2ext "$diskFmt")
diskFile="$diskBase.$diskExt" local diskFile="$diskBase.$diskExt"
dir=$(dirname "$diskFile") dir=$(dirname "$diskFile")
[ ! -d "$dir" ] && return 0 [ ! -d "$dir" ] && return 0
@@ -587,7 +605,11 @@ addDisk () {
space=$(normalizeSize "$diskSpace" "$diskDesc" "$dir") space=$(normalizeSize "$diskSpace" "$diskDesc" "$dir")
dataSize=$(numfmt --from=iec "$space") dataSize=$(numfmt --from=iec "$space")
fs=$(stat -f -c %T "$dir") if ! fs=$(stat -f -c %T "$dir"); then
error "Failed to determine filesystem type of \"$dir\" !"
return 1
fi
checkFS "$fs" "$diskFile" "$diskDesc" || exit $? checkFS "$fs" "$diskFile" "$diskDesc" || exit $?
if ! supportsDirect "$fs"; then if ! supportsDirect "$fs"; then
@@ -595,23 +617,26 @@ addDisk () {
diskCache="writeback" diskCache="writeback"
fi fi
if [ ! -s "$diskFile" ] ; then if [ ! -f "$diskFile" ] || [ ! -s "$diskFile" ]; then
if [[ "${diskFmt,,}" != "raw" ]]; then if [[ "${diskFmt,,}" != "raw" ]]; then
previousFmt="raw" local previousFmt="raw"
else else
previousFmt="qcow2" local previousFmt="qcow2"
fi fi
previousExt=$(fmt2ext "$previousFmt") previousExt=$(fmt2ext "$previousFmt")
if [ -s "$diskBase.$previousExt" ] ; then # 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 $? convertDisk "$diskBase.$previousExt" "$previousFmt" "$diskFile" "$diskFmt" "$diskBase" "$diskDesc" "$fs" || exit $?
fi fi
fi fi
if [ -s "$diskFile" ]; then if [ -f "$diskFile" ] && [ -s "$diskFile" ]; then
currentSize=$(getSize "$diskFile") || exit 71 currentSize=$(getSize "$diskFile") || exit 71
@@ -636,23 +661,25 @@ 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
used=$(du -sB 1 "$diskFile" | cut -f1) used=$(du -sB 1 "$diskFile" | cut -f1)
available=$(df --output=avail -B 1 "$dir" | tail -n 1) available=$(df --output=avail -B 1 "$dir" | tail -n 1)
missing=$(( currentSize - used - available )) local missing=$(( currentSize - used - available ))
(( missing < 0 )) && missing=0 (( missing < 0 )) && missing=0
if (( missing > 0 )); then if (( missing > 0 )); then
local gb base msg local gb base
gb=$(formatBytes "$available") gb=$(formatBytes "$available")
base=$(baseDir "$dir") base=$(baseDir "$dir")
missing=$(formatBytes "$missing") missing=$(formatBytes "$missing")
currentSize=$(formatBytes "$currentSize") currentSize=$(formatBytes "$currentSize")
msg="The virtual size of the ${diskDesc,,} is $currentSize" local msg="The virtual size of the ${diskDesc,,} is $currentSize"
if [ -n "$used" ] && [[ "$used" != "0" ]]; then if [ -n "$used" ] && [[ "$used" != "0" ]]; then
used=$(formatBytes "$used") used=$(formatBytes "$used")
@@ -690,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
@@ -767,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
@@ -779,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" "" "")
@@ -818,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
+41 -107
View File
@@ -41,8 +41,10 @@ detectEngine() {
detectRootless() { 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
@@ -58,8 +60,6 @@ checkPrivileged() {
local cap_bnd local cap_bnd
local last_cap local last_cap
local max_cap
# Get the capability bounding set # Get the capability bounding set
cap_bnd=$(grep '^CapBnd:' /proc/$$/status | awk '{print $2}') cap_bnd=$(grep '^CapBnd:' /proc/$$/status | awk '{print $2}')
cap_bnd=$(printf "%d" "0x${cap_bnd}") cap_bnd=$(printf "%d" "0x${cap_bnd}")
@@ -68,7 +68,9 @@ 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
max_cap=$(((1 << (last_cap + 1)) - 1)) # 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 if [ "$cap_bnd" -eq "$max_cap" ]; then
PRIVILEGED="Y" PRIVILEGED="Y"
@@ -77,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"
@@ -94,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
@@ -103,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
@@ -111,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
@@ -127,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"
@@ -140,91 +158,6 @@ checkFilesystem() {
return 0 return 0
} }
finiteMemoryLimit() {
local limit="$1"
local sentinel="4611686018427387904"
local i=0
local left=""
local right=""
[[ "$limit" =~ ^[0-9]+$ ]] || return 1
(( ${#limit} < ${#sentinel} )) && return 0
(( ${#limit} > ${#sentinel} )) && return 1
for (( i=0; i<${#sentinel}; i++ )); do
left="${limit:i:1}"
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=""
local available=""
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"
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
@@ -238,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."
@@ -261,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
@@ -282,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
@@ -295,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
@@ -322,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')
@@ -349,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
+306 -136
View File
@@ -84,8 +84,7 @@ getMTU() {
minMTU() { minMTU() {
local mtu="" local mtu min=""
local min=""
for mtu in "$@"; do for mtu in "$@"; do
[[ -z "$mtu" || "$mtu" == "0" ]] && continue [[ -z "$mtu" || "$mtu" == "0" ]] && continue
@@ -124,7 +123,7 @@ gatewayMAC() {
maskToCIDR() { maskToCIDR() {
local mask="$1" local mask="$1"
local prefix="" local prefix
if ! command -v ipcalc > /dev/null 2>&1; then if ! command -v ipcalc > /dev/null 2>&1; then
error "Required command 'ipcalc' is not installed!" error "Required command 'ipcalc' is not installed!"
@@ -154,7 +153,7 @@ maskToCIDR() {
networkCIDR() { networkCIDR() {
local ip="$1" local ip="$1"
local network="" local network
network=$(ipcalc -n -b "$ip/$MASK" 2>/dev/null | awk ' network=$(ipcalc -n -b "$ip/$MASK" 2>/dev/null | awk '
/^Network:/ { /^Network:/ {
@@ -172,6 +171,37 @@ networkCIDR() {
return 0 return 0
} }
upstreamIP() {
local subnet="$1"
local guest="$2"
local gateway="$3"
local broadcast candidate last
broadcast=$(ipcalc -n -b "$subnet" 2>/dev/null | awk '
/^Broadcast:/ {
print $2
exit
}
')
if [[ ! "$broadcast" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
return 1
fi
last="${broadcast##*.}"
for (( last--; last>=2; last-- )); do
candidate="${broadcast%.*}.$last"
[[ "$candidate" == "$guest" || "$candidate" == "$gateway" ]] && continue
echo "$candidate"
return 0
done
return 1
}
detectInterface() { detectInterface() {
if [ -n "$DEV" ]; then if [ -n "$DEV" ]; then
@@ -207,16 +237,23 @@ formatAddress() {
return 0 return 0
} }
defaultGateway() {
ip -4 route list default dev "$1" 2>/dev/null |
awk '$1 == "default" { for (i = 1; i < NF; i++) if ($i == "via") { print $(i + 1); exit } }' || :
return 0
}
detectAddresses() { detectAddresses() {
GATEWAY=$(ip route list dev "$DEV" | awk ' /^default/ {print $3}' | head -n 1) GATEWAY=$(defaultGateway "$DEV")
{ UPLINK=$(ip address show dev "$DEV" | grep inet | awk '/inet / { print $2 }' | cut -f1 -d/ | head -n 1); } 2>/dev/null || : { UPLINK=$(ip address show dev "$DEV" | grep inet | awk '/inet / { print $2 }' | cut -f1 -d/ | head -n 1); } 2>/dev/null || :
IP6="" IP6=""
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
local rc=0 { IP6=$(ip -6 addr show dev "$DEV" scope global up); local rc=$?; } 2>/dev/null || :
{ IP6=$(ip -6 addr show dev "$DEV" scope global up); rc=$?; } 2>/dev/null || :
(( rc != 0 )) && IP6="" (( rc != 0 )) && IP6=""
[ -n "$IP6" ] && IP6=$(echo "$IP6" | sed -e's/^.*inet6 \([^ ]*\)\/.*$/\1/;t;d' | head -n 1) [ -n "$IP6" ] && IP6=$(echo "$IP6" | sed -e's/^.*inet6 \([^ ]*\)\/.*$/\1/;t;d' | head -n 1)
fi fi
@@ -226,7 +263,7 @@ detectAddresses() {
detectAdapter() { detectAdapter() {
local result="" local result
NIC="" NIC=""
BUS="" BUS=""
@@ -252,7 +289,7 @@ detectAdapter() {
containerID() { containerID() {
local id="" local id
id=$(hostname -s 2>/dev/null || true) id=$(hostname -s 2>/dev/null || true)
@@ -260,7 +297,7 @@ containerID() {
id=$(< /etc/machine-id) id=$(< /etc/machine-id)
fi fi
if [ -z "$id" ] && [ -s /proc/sys/kernel/random/boot_id ]; then if [ -z "$id" ] && [ -r /proc/sys/kernel/random/boot_id ]; then
id=$(< /proc/sys/kernel/random/boot_id) id=$(< /proc/sys/kernel/random/boot_id)
fi fi
@@ -270,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"
@@ -286,7 +344,7 @@ disableIPv6() {
subnetInUse() { subnetInUse() {
local subnet="$1" local subnet="$1"
local broader="" narrower="" routes="" local broader narrower routes
if ! broader=$(ip -4 route show table all match "$subnet" 2>/dev/null); then if ! broader=$(ip -4 route show table all match "$subnet" 2>/dev/null); then
error "Failed to inspect existing routes for subnet $subnet." error "Failed to inspect existing routes for subnet $subnet."
@@ -324,17 +382,18 @@ guestIP() {
natGuestIP() { natGuestIP() {
local ip="$1" local ip="$1"
local start="" guest="" subnet="" local guest subnet second third
local second="" third="" rc=""
third=$(cut -d. -f3 <<< "$ip") third=$(cut -d. -f3 <<< "$ip")
if [[ "$ip" == "172.30."* ]]; then if [[ "$ip" == "172.30."* ]]; then
start="31" local start="31"
else else
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
@@ -342,7 +401,7 @@ natGuestIP() {
if subnetInUse "$subnet"; then if subnetInUse "$subnet"; then
continue continue
else else
rc=$? local rc=$?
(( rc == 1 )) || return 1 (( rc == 1 )) || return 1
fi fi
@@ -357,7 +416,7 @@ natGuestIP() {
if subnetInUse "$subnet"; then if subnetInUse "$subnet"; then
continue continue
else else
rc=$? local rc=$?
(( rc == 1 )) || return 1 (( rc == 1 )) || return 1
fi fi
@@ -369,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
# ###################################### # ######################################
@@ -411,7 +440,9 @@ configureDNS() {
local host="$4" local host="$4"
local mask="$5" local mask="$5"
local gateway="$6" local gateway="$6"
local arguments="$DNSMASQ_OPTS" rc local upstream="${7:-}"
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."
@@ -421,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
@@ -459,6 +493,11 @@ configureDNS() {
# Add DNS entry for container # Add DNS entry for container
arguments+=" --address=/host.lan/$gateway" arguments+=" --address=/host.lan/$gateway"
# Add DNS entry for the upstream gateway.
if isNAT && [ -n "$upstream" ]; then
arguments+=" --address=/system.lan/$upstream"
fi
# Avoid returning IPv6 records when the active network mode is IPv4-only. # Avoid returning IPv6 records when the active network mode is IPv4-only.
if isNAT || [ -z "$IP6" ]; then if isNAT || [ -z "$IP6" ]; then
arguments+=" --filter-AAAA" arguments+=" --filter-AAAA"
@@ -476,9 +515,9 @@ 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}; rc=$?; } || : { $DNSMASQ ${arguments:+ $arguments}; local rc=$?; } || :
if (( rc != 0 )); then if (( rc != 0 )); then
@@ -499,11 +538,12 @@ configureDNS() {
return 0 return 0
} }
getHostPorts() { normalizePorts() {
local port="" ports="" local list="$1"
local num="" mode="${1:-tcp}" local mode="${2:-tcp}"
local list="${HOST_PORTS// /}," local port num
local ports=""
for port in ${list//,/ }; do for port in ${list//,/ }; do
@@ -513,6 +553,7 @@ getHostPorts() {
"tcp" ) "tcp" )
[[ "$port" == *"/udp" ]] && continue [[ "$port" == *"/udp" ]] && continue
num="${port%/tcp}" num="${port%/tcp}"
[ -n "$num" ] && ports+="$num,"
;; ;;
"all" ) "all" )
if [[ "$port" == *"/udp" ]]; then if [[ "$port" == *"/udp" ]]; then
@@ -522,39 +563,82 @@ getHostPorts() {
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 num="" ports="" proto="" local ports=""
local userport="" hostport="" local userport hostport exclude reserved
local exclude="" reserved=$(getReservedPorts "all")
exclude=$(getHostPorts "all") exclude=$(getHostPorts "all")
for userport in ${list//,/ }; do for userport in ${list//,/ }; do
proto="tcp" local proto="tcp"
num="$userport" local num="$userport"
if [[ "$userport" == *"/udp" ]]; then if [[ "$userport" == *"/udp" ]]; then
proto="udp" proto="udp"
@@ -570,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
@@ -580,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
@@ -593,7 +685,7 @@ getUserPorts() {
getSlirp() { getSlirp() {
local ip="$1" local ip="$1"
local args="" list="" local args="" list
list=$(getUserPorts) list=$(getUserPorts)
@@ -617,9 +709,8 @@ getSlirp() {
getPasst() { getPasst() {
local args="" list="" port="" local list port
local num="" tcp="" udp="" local tcp="" udp="" args=""
local bind="$UPLINK"
list=$(getUserPorts) list=$(getUserPorts)
@@ -629,12 +720,12 @@ getPasst() {
if [[ "$port" == *"/udp" ]]; then if [[ "$port" == *"/udp" ]]; then
num="${port%/udp}" local num="${port%/udp}"
[ -n "$num" ] && udp+="$num," [ -n "$num" ] && udp+="$num,"
elif [[ "$port" == *"/tcp" ]]; then elif [[ "$port" == *"/tcp" ]]; then
num="${port%/tcp}" local num="${port%/tcp}"
[ -n "$num" ] && tcp+="$num," [ -n "$num" ] && tcp+="$num,"
else else
@@ -648,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
@@ -665,8 +752,7 @@ getPasst() {
configureVTAP() { configureVTAP() {
local msg="" local msg dev
local rc
enabled "$DEBUG" && echo "Configuring MACVTAP networking..." enabled "$DEBUG" && echo "Configuring MACVTAP networking..."
@@ -678,7 +764,7 @@ configureVTAP() {
fi fi
# Create a macvtap network for the VM guest # Create a macvtap network for the VM guest
{ msg=$(ip link add link "$DEV" name "$TAP" address "$MAC" type macvtap mode bridge 2>&1); rc=$?; } || : { msg=$(ip link add link "$DEV" name "$TAP" address "$MAC" type macvtap mode bridge 2>&1); local rc=$?; } || :
case "$msg" in case "$msg" in
"RTNETLINK answers: File exists"* ) "RTNETLINK answers: File exists"* )
@@ -711,15 +797,35 @@ configureVTAP() {
sleep 2 sleep 2
done done
local TAP_NR TAP_PATH MAJOR MINOR local TAP_NR MAJOR MINOR
TAP_NR=$(</sys/class/net/"$TAP"/ifindex)
TAP_PATH="/dev/tap${TAP_NR}" if ! dev=$(cat /sys/devices/virtual/net/"$TAP"/tap*/dev); then
error "Failed to determine device numbers for MACVTAP interface \"$TAP\" !"
return 1
fi
IFS=: read -r MAJOR MINOR <<< "$dev"
if [[ ! "$MAJOR" =~ ^[0-9]+$ || ! "$MINOR" =~ ^[0-9]+$ ]]; then
error "Failed to parse device numbers for MACVTAP interface \"$TAP\" !"
return 1
fi
if (( MAJOR < 1 )); then
error "Cannot find: sys/devices/virtual/net/$TAP"
return 1
fi
if ! TAP_NR=$(<"/sys/class/net/$TAP/ifindex"); then
error "Failed to determine interface index of MACVTAP interface \"$TAP\" !"
return 1
fi
# Create dev file (there is no udev in container: need to be done manually) # Create dev file (there is no udev in container: need to be done manually)
IFS=: read -r MAJOR MINOR < <(cat /sys/devices/virtual/net/"$TAP"/tap*/dev) local TAP_PATH="/dev/tap${TAP_NR}"
(( MAJOR < 1)) && error "Cannot find: sys/devices/virtual/net/$TAP" && return 1
[[ ! -e "$TAP_PATH" && -e "/dev0/${TAP_PATH##*/}" ]] && ln -s "/dev0/${TAP_PATH##*/}" "$TAP_PATH" [[ ! -e "$TAP_PATH" && -e "/dev0/${TAP_PATH##*/}" ]] &&
ln -s "/dev0/${TAP_PATH##*/}" "$TAP_PATH"
if [[ ! -e "$TAP_PATH" ]]; then if [[ ! -e "$TAP_PATH" ]]; then
{ mknod "$TAP_PATH" c "$MAJOR" "$MINOR"; rc=$?; } || : { mknod "$TAP_PATH" c "$MAJOR" "$MINOR"; rc=$?; } || :
@@ -753,7 +859,7 @@ configureSlirp() {
ip=$(guestIP "$ip" 4) ip=$(guestIP "$ip" 4)
local gateway="${ip%.*}.1" local gateway="${ip%.*}.1"
local subnet="" local subnet
subnet=$(networkCIDR "$ip") || return 1 subnet=$(networkCIDR "$ip") || return 1
local ipv6="ipv6=off," local ipv6="ipv6=off,"
@@ -761,7 +867,7 @@ configureSlirp() {
NET_OPTS="-netdev user,id=hostnet0,ipv4=on,host=$gateway,net=$subnet,dhcpstart=$ip,${ipv6}hostname=$HOST" NET_OPTS="-netdev user,id=hostnet0,ipv4=on,host=$gateway,net=$subnet,dhcpstart=$ip,${ipv6}hostname=$HOST"
local forward="" local forward
forward=$(getSlirp "$ip") forward=$(getSlirp "$ip")
[ -n "$forward" ] && NET_OPTS+=",$forward" [ -n "$forward" ] && NET_OPTS+=",$forward"
@@ -818,12 +924,13 @@ configurePasst() {
# Pass an explicit MTU to passt. # Pass an explicit MTU to passt.
PASST_OPTS+=" -m $passt_mtu" PASST_OPTS+=" -m $passt_mtu"
local forward="" local forward
forward=$(getPasst) forward=$(getPasst)
[ -n "$forward" ] && PASST_OPTS+="$forward" [ -n "$forward" ] && PASST_OPTS+="$forward"
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"
@@ -854,11 +961,10 @@ configurePasst() {
if ! "$PASST" ${PASST_OPTS:+$PASST_OPTS} >/dev/null 2>&1; then if ! "$PASST" ${PASST_OPTS:+$PASST_OPTS} >/dev/null 2>&1; then
local rc=0
rm -f "$log" rm -f "$log"
PASST_OPTS="${PASST_OPTS/ -q/}" PASST_OPTS="${PASST_OPTS/ -q/}"
{ "$PASST" ${PASST_OPTS:+$PASST_OPTS}; rc=$?; } || : { "$PASST" ${PASST_OPTS:+$PASST_OPTS}; local rc=$?; } || :
if (( rc != 0 )); then if (( rc != 0 )); then
[ -f "$log" ] && [ -s "$log" ] && cat "$log" [ -f "$log" ] && [ -s "$log" ] && cat "$log"
@@ -886,13 +992,24 @@ 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"
local rc msg="" local msg
# Create a bridge with a static IP for the VM guest # Create a bridge with a static IP for the VM guest
{ msg=$(ip link add dev "$BRIDGE" type bridge 2>&1); rc=$?; } || : { msg=$(ip link add dev "$BRIDGE" type bridge 2>&1); local rc=$?; } || :
if (( rc != 0 )); then if (( rc != 0 )); then
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1 enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
@@ -930,10 +1047,10 @@ createBridge() {
createTap() { createTap() {
local tuntap="$1" local tuntap="$1"
local rc msg="" local msg
# Set tap to the bridge created # Set tap to the bridge created
{ msg=$(ip tuntap add dev "$TAP" mode tap 2>&1); rc=$?; } || : { msg=$(ip tuntap add dev "$TAP" mode tap 2>&1); local rc=$?; } || :
if (( rc != 0 )); then if (( rc != 0 )); then
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1 enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
@@ -976,7 +1093,7 @@ hasTable() {
getTablesBackend() { getTablesBackend() {
local version="" local version
version=$(iptables --version 2>/dev/null || true) version=$(iptables --version 2>/dev/null || true)
case "$version" in case "$version" in
@@ -989,7 +1106,7 @@ getTablesBackend() {
setTables() { setTables() {
local mode="$1" local mode="$1"
local path="" local path
path=$(command -v "iptables-$mode" 2>/dev/null || true) path=$(command -v "iptables-$mode" 2>/dev/null || true)
[ -z "$path" ] && return 1 [ -z "$path" ] && return 1
@@ -1003,7 +1120,7 @@ showRules() {
local chain="$2" local chain="$2"
local label="$3" local label="$3"
local rule_tag="$4" local rule_tag="$4"
local rules="" local rules
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)" local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
enabled "$DEBUG" || return 0 enabled "$DEBUG" || return 0
@@ -1022,7 +1139,7 @@ showRules() {
checkExistingTables() { checkExistingTables() {
local msg="" rules="" conflicts="" local rules conflicts
local rule_tag="QEMU_DNAT" local rule_tag="QEMU_DNAT"
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)" local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
@@ -1040,7 +1157,7 @@ checkExistingTables() {
<<< "$rules" || true) <<< "$rules" || true)
if [ -n "$conflicts" ]; then if [ -n "$conflicts" ]; then
msg="your existing NAT rules may take precedence over VM port forwarding" local msg="your existing NAT rules may take precedence over VM port forwarding"
if enabled "$DEBUG"; then if enabled "$DEBUG"; then
warn "${msg}." warn "${msg}."
@@ -1060,7 +1177,7 @@ checkExistingTables() {
<<< "$rules" || true) <<< "$rules" || true)
if [ -n "$conflicts" ]; then if [ -n "$conflicts" ]; then
msg="your existing firewall rules may block traffic forwarded to or from the VM" local msg="your existing firewall rules may block traffic forwarded to or from the VM"
if enabled "$DEBUG"; then if enabled "$DEBUG"; then
warn "${msg}." warn "${msg}."
@@ -1088,13 +1205,13 @@ runTableRule() {
local silent="$1" local silent="$1"
local result="$2" local result="$2"
local rc msg="" local msg
shift 2 shift 2
printf -v "$result" '%s' "" printf -v "$result" '%s' ""
{ msg=$("$@" 2>&1); rc=$?; } || : { msg=$("$@" 2>&1); local rc=$?; } || :
(( rc == 0 )) && return 0 (( rc == 0 )) && return 0
printf -v "$result" '%s' "$msg" printf -v "$result" '%s' "$msg"
@@ -1154,8 +1271,8 @@ applyTables() {
local ip="$1" local ip="$1"
local subnet="$2" local subnet="$2"
local silent="${3:-N}" local silent="${3:-N}"
local exclude="" port="" local exclude port
local table_error="" local table_error
local dnat_chain="QEMU_DNAT" local dnat_chain="QEMU_DNAT"
local rule_tag="$dnat_chain" local rule_tag="$dnat_chain"
@@ -1283,8 +1400,8 @@ applyTables() {
clearTables() { clearTables() {
local table="" line="" chain="" local line
local rules="" remaining="" message="" local rules remaining message
local dnat_chain="QEMU_DNAT" local dnat_chain="QEMU_DNAT"
local rule_tag="$dnat_chain" local rule_tag="$dnat_chain"
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)" local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
@@ -1309,15 +1426,15 @@ clearTables() {
while IFS= read -r line; do while IFS= read -r line; do
case "$line" in case "$line" in
\*nat ) table="nat" ;; \*nat ) local table="nat" ;;
\*filter ) table="filter" ;; \*filter ) local table="filter" ;;
\*mangle ) table="mangle" ;; \*mangle ) local table="mangle" ;;
\*raw ) table="raw" ;; \*raw ) local table="raw" ;;
esac esac
if [[ "$line" == -A* ]] && [[ "$line" =~ $own_rule ]]; then if [[ "$line" == -A* ]] && [[ "$line" =~ $own_rule ]]; then
chain="${line#-A }" local chain="${line#-A }"
chain="${chain%% *}" chain="${chain%% *}"
# Rules inside this chain are removed together by the flush below. # Rules inside this chain are removed together by the flush below.
@@ -1383,7 +1500,7 @@ clearTables() {
hasTaggedRules() { hasTaggedRules() {
local save="$1" local save="$1"
local rules="" local rules
local dnat_chain="QEMU_DNAT" local dnat_chain="QEMU_DNAT"
local rule_tag="$dnat_chain" local rule_tag="$dnat_chain"
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)" local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
@@ -1405,9 +1522,8 @@ configureTables() {
local ip="$1" local ip="$1"
local subnet="$2" local subnet="$2"
local preferred="" local preferred
local alternate="" local alternate_save
local alternate_save=""
local preferred_clean="N" local preferred_clean="N"
local alternate_dirty="N" local alternate_dirty="N"
@@ -1418,8 +1534,8 @@ configureTables() {
} }
case "$preferred" in case "$preferred" in
"nft" ) alternate="legacy" ;; "nft" ) local alternate="legacy" ;;
"legacy" ) alternate="nft" ;; "legacy" ) local alternate="nft" ;;
* ) * )
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1 enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
warn "unsupported IP tables backend: $preferred" warn "unsupported IP tables backend: $preferred"
@@ -1605,10 +1721,46 @@ configureTables() {
return 1 return 1
} }
addUpstream() {
local upstream="$1"
local table_error
local rule_tag="QEMU_DNAT"
[ -n "$upstream" ] || return 1
[ -n "$GATEWAY" ] || return 1
if ! ip address add "$upstream/32" dev "$BRIDGE"; then
if ! enabled "$ROOTLESS" || enabled "$DEBUG"; then
warn "failed to add the system.lan address; access through that name will be unavailable."
fi
return 1
fi
if ! runTableRule "Y" table_error \
iptables -t nat -A PREROUTING \
-i "$BRIDGE" \
-d "$upstream" \
-m comment --comment "$rule_tag" \
-j DNAT --to-destination "$GATEWAY"; then
ip address del "$upstream/32" dev "$BRIDGE" > /dev/null 2>&1 || :
if ! enabled "$ROOTLESS" || enabled "$DEBUG"; then
[ -n "$table_error" ] && echo "$table_error" >&2
warn "failed to configure system.lan forwarding; access through that name will be unavailable."
fi
return 1
fi
return 0
}
configureNAT() { configureNAT() {
local tuntap="TUN device is missing. $ADD_ERR --device /dev/net/tun" local tuntap="TUN device is missing. $ADD_ERR --device /dev/net/tun"
local rc ip subnet msg="" forwarding="" local ip subnet upstream="" forwarding=""
enabled "$DEBUG" && echo "Configuring NAT networking..." enabled "$DEBUG" && echo "Configuring NAT networking..."
@@ -1616,7 +1768,8 @@ configureNAT() {
if [ ! -c /dev/net/tun ]; then if [ ! -c /dev/net/tun ]; then
[ ! -d /dev/net ] && mkdir -m 755 /dev/net > /dev/null 2>&1 || : [ ! -d /dev/net ] && mkdir -m 755 /dev/net > /dev/null 2>&1 || :
{ msg=$(mknod /dev/net/tun c 10 200 2>&1); rc=$?; } || : local msg
{ msg=$(mknod /dev/net/tun c 10 200 2>&1); local rc=$?; } || :
if (( rc == 0 )); then if (( rc == 0 )); then
chmod 666 /dev/net/tun chmod 666 /dev/net/tun
@@ -1635,7 +1788,7 @@ configureNAT() {
forwarding=$(< /proc/sys/net/ipv4/ip_forward) forwarding=$(< /proc/sys/net/ipv4/ip_forward)
if [[ "$forwarding" != "1" ]]; then if [[ "$forwarding" != "1" ]]; then
{ sysctl -w net.ipv4.ip_forward=1 > /dev/null 2>&1; rc=$?; } || : { sysctl -w net.ipv4.ip_forward=1 > /dev/null 2>&1; local rc=$?; } || :
forwarding="" forwarding=""
[ -r /proc/sys/net/ipv4/ip_forward ] && [ -r /proc/sys/net/ipv4/ip_forward ] &&
@@ -1657,11 +1810,15 @@ configureNAT() {
local gateway="${ip%.*}.1" local gateway="${ip%.*}.1"
subnet=$(networkCIDR "$ip") || return 1 subnet=$(networkCIDR "$ip") || return 1
if [ -n "$GATEWAY" ]; then
upstream=$(upstreamIP "$subnet" "$ip" "$gateway") || upstream=""
fi
if subnetInUse "$subnet"; then if subnetInUse "$subnet"; then
error "VM subnet $subnet conflicts with an existing route inside the container." error "VM subnet $subnet conflicts with an existing route inside the container."
return 1 return 1
else else
rc=$? local rc=$?
(( rc == 1 )) || return 1 (( rc == 1 )) || return 1
fi fi
@@ -1675,16 +1832,20 @@ configureNAT() {
configureTables "$ip" "$subnet" || return 1 configureTables "$ip" "$subnet" || return 1
if [ -n "$upstream" ] && ! addUpstream "$upstream"; then
upstream=""
fi
NET_OPTS="-netdev tap,id=hostnet0,ifname=$TAP" NET_OPTS="-netdev tap,id=hostnet0,ifname=$TAP"
if [ -c /dev/vhost-net ]; then if [ -c /dev/vhost-net ]; then
{ exec 40>>/dev/vhost-net; rc=$?; } 2>/dev/null || : { exec 40>>/dev/vhost-net; local rc=$?; } 2>/dev/null || :
(( rc == 0 )) && NET_OPTS+=",vhost=on,vhostfd=40" (( rc == 0 )) && NET_OPTS+=",vhost=on,vhostfd=40"
fi fi
NET_OPTS+=",script=no,downscript=no" NET_OPTS+=",script=no,downscript=no"
configureDNS "$BRIDGE" "$ip" "$MAC" "$HOST" "$MASK" "$gateway" || return 1 configureDNS "$BRIDGE" "$ip" "$MAC" "$HOST" "$MASK" "$gateway" "$upstream" || return 1
IP="$ip" IP="$ip"
return 0 return 0
@@ -1740,7 +1901,7 @@ closeNetwork() {
checkOS() { checkOS() {
local iface="macvlan" local iface="macvlan"
local os="" kernel="" local os="" kernel
kernel=$(uname -a) kernel=$(uname -a)
@@ -1798,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
@@ -1891,14 +2055,12 @@ configureMTU() {
configureMAC() { configureMAC() {
local container="" local container
local file=""
container=$(containerID) container=$(containerID)
if [ -z "$MAC" ]; then if [ -z "$MAC" ]; then
file="$STORAGE/dsm.mac" local file="$STORAGE/dsm.mac"
if [ -s "$file" ]; then if [ -s "$file" ]; then
if ! MAC=$(readFile "$file"); then if ! MAC=$(readFile "$file"); then
@@ -1941,7 +2103,7 @@ configureMAC() {
showHostInfo() { showHostInfo() {
local mtu="" host="" uplink="" prefix="" local mtu host uplink prefix
prefix=$(ip -4 -o address show dev "$DEV" scope global 2>/dev/null | prefix=$(ip -4 -o address show dev "$DEV" scope global 2>/dev/null |
awk -v ip="$UPLINK" ' awk -v ip="$UPLINK" '
@@ -2010,6 +2172,7 @@ showHostInfo() {
echo " DNS: $nameservers" echo " DNS: $nameservers"
fi fi
enabled "$DEBUG" && echo
return 0 return 0
} }
@@ -2028,10 +2191,10 @@ showGuestInfo() {
local mode="${NETWORK,,}" local mode="${NETWORK,,}"
if isNAT; then if enabled "$DHCP"; then
mode="NAT"
elif enabled "$DHCP"; then
mode="DHCP" mode="DHCP"
elif isNAT; then
mode="NAT"
elif isUserMode; then elif isUserMode; then
mode="User ($mode)" mode="User ($mode)"
elif [ -z "$mode" ]; then elif [ -z "$mode" ]; then
@@ -2064,6 +2227,7 @@ initializeNetwork() {
configureMTU configureMTU
configureMAC configureMAC
configureBridge
showHostInfo showHostInfo
@@ -2117,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
@@ -2164,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
@@ -2174,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
+47 -34
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() {
@@ -92,7 +86,7 @@ qemuPidFile() {
waitQemuExit() { waitQemuExit() {
local timeout="${1:-10}" local timeout="${1:-10}"
local file="" local file
qemuPidFile file qemuPidFile file
waitPidFile "$file" "$timeout" waitPidFile "$file" "$timeout"
@@ -100,26 +94,24 @@ 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
} }
forceKillQemu() { 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..."
@@ -145,7 +137,7 @@ cleanupHelpers() {
startConsole() { startConsole() {
local output="${1:-/dev/tty}" local output="${1:-/dev/tty}"
local cnt=0 pid="" local cnt=0
rm -f -- "$CONSOLE_SOCKET" "$CONSOLE_PID" rm -f -- "$CONSOLE_SOCKET" "$CONSOLE_PID"
@@ -159,7 +151,7 @@ startConsole() {
exec nc -lU "$CONSOLE_SOCKET" </dev/tty >"$output" exec nc -lU "$CONSOLE_SOCKET" </dev/tty >"$output"
) & ) &
pid=$! local pid="$!"
echo "$pid" > "$CONSOLE_PID" echo "$pid" > "$CONSOLE_PID"
while [ ! -S "$CONSOLE_SOCKET" ]; do while [ ! -S "$CONSOLE_SOCKET" ]; do
@@ -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
@@ -246,14 +240,16 @@ finish() {
sendGuestShutdown() { sendGuestShutdown() {
local pid="$1" local pid="$1"
local response url 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' || :
# Send shutdown command to guest agent via serial port # Send shutdown command to guest agent via serial port
API_TIMEOUT=$(strip "$API_TIMEOUT") API_TIMEOUT=$(strip "$API_TIMEOUT")
url="http://localhost/read?command=$API_CMD&timeout=$API_TIMEOUT" local url="http://localhost/read?command=$API_CMD&timeout=$API_TIMEOUT"
response=$(curl --unix-socket "$HOST_API_SOCKET" -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1) response=$(curl --unix-socket "$HOST_API_SOCKET" -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1)
if [[ "$response" =~ "\"success\"" ]]; then if [[ "$response" =~ "\"success\"" ]]; then
@@ -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,17 +335,19 @@ waitForShutdown() {
return 0 return 0
} }
graceful_shutdown() { gracefulShutdown() {
local sig="$1" local sig="$1"
local pid="" code=0 local pid code
[[ $BASHPID != "$TRAP_PID" ]] && return [[ $BASHPID != "$TRAP_PID" ]] && return
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
+33 -13
View File
@@ -9,6 +9,7 @@ cd /run
. utils.sh # Load functions . utils.sh # Load functions
info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "$1" "\E[0m\n" >&2; } info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "$1" "\E[0m\n" >&2; }
warn () { printf "%b%s%b" "\E[1;33m " "WARNING: $1" "\E[0m\n" >&2; }
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; }
disabled "$NETWORK" && exit 0 disabled "$NETWORK" && exit 0
@@ -36,9 +37,9 @@ exitIfShuttingDown() {
queryGuest() { queryGuest() {
local rc # 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"); rc=$?; } || : { json=$(curl --unix-socket "$socket" -m 20 -sk "$url"); local rc=$?; } || :
exitIfShuttingDown exitIfShuttingDown
@@ -54,9 +55,8 @@ readJsonField() {
local query="$1" local query="$1"
local result local result
local rc
{ result=$(jq -r "$query" <<< "$json"); rc=$?; } || : { result=$(jq -r "$query" <<< "$json"); local rc=$?; } || :
if (( rc != 0 )); then if (( rc != 0 )); then
error "$jq_err $rc ( $json )" error "$jq_err $rc ( $json )"
@@ -74,12 +74,12 @@ readJsonField() {
readGuestStatus() { readGuestStatus() {
local result msg rc local result msg
result=$(readJsonField '.status') || return 1 result=$(readJsonField '.status') || return 1
if [[ "$result" != "success" ]]; then if [[ "$result" != "success" ]]; then
{ msg=$(jq -r '.message // empty' <<< "$json"); rc=$?; } || : { msg=$(jq -r '.message // empty' <<< "$json"); local rc=$?; } || :
if (( rc != 0 )); then if (( rc != 0 )); then
error "$jq_err $rc ( $json )" error "$jq_err $rc ( $json )"
@@ -118,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
@@ -140,14 +142,29 @@ pollGuestLocation() {
return 0 return 0
} }
checkAddressConflict() {
local guest_ip="${location%:*}"
local container_ip=""
[ -s "$address" ] && container_ip=$(<"$address")
[ -z "$container_ip" ] && return 0
[[ "$guest_ip" != "$container_ip" ]] && return 0
warn "DSM is using the same IP as the container, this will cause connectivity issues."
warn "change the container's macvlan IP or assign DSM a different address in your router."
return 0
}
writeDhcpPage() { writeDhcpPage() {
local title body script html local html
msg="http://$location" msg="http://$location"
title="<title>Virtual DSM</title>" local title="<title>Virtual DSM</title>"
body="The location of DSM is <a href='http://$location'>http://$location</a>" local body="The location of DSM is <a href='http://$location'>http://$location</a>"
script="<script>setTimeout(function(){ window.location.assign('http://$location'); }, 3000);</script>" local script="<script>setTimeout(function(){ window.location.assign('http://$location'); }, 3000);</script>"
html=$(<"$template") html=$(<"$template")
html="${html/\[1\]/$title}" html="${html/\[1\]/$title}"
@@ -164,12 +181,14 @@ writeDhcpPage() {
buildStaticMessage() { buildStaticMessage() {
local nic ip port local nic ip
nic=$(<"$driver") nic=$(<"$driver")
ip=$(<"$address") ip=$(<"$address")
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
@@ -196,6 +215,7 @@ exitIfShuttingDown
location=$(<"$file") location=$(<"$file")
if enabled "$DHCP"; then if enabled "$DHCP"; then
checkAddressConflict
writeDhcpPage writeDhcpPage
else else
buildStaticMessage buildStaticMessage
+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,"
+90 -7
View File
@@ -2,6 +2,7 @@
set -Eeuo pipefail set -Eeuo pipefail
info="/run/shm/msg.html" info="/run/shm/msg.html"
info_tmp="${info}.${BASHPID}.tmp"
escape() { escape() {
@@ -17,6 +18,25 @@ escape() {
return 0 return 0
} }
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
fi
if ! mv -f -- "$info_tmp" "$info"; then
rm -f -- "$info_tmp"
return 1
fi
return 0
}
getBytes() { getBytes() {
local path="$1" local path="$1"
@@ -48,6 +68,24 @@ getBytes() {
return 0 return 0
} }
getStatus() {
local file="$1"
local bytes total extra=""
[ -r "$file" ] || return 1
read -r bytes total extra < "$file" || return 1
if [[ ! "$bytes" =~ ^[0-9]+$ ||
! "$total" =~ ^[0-9]+$ ||
-n "$extra" ]]; then
return 1
fi
printf '%s %s\n' "$bytes" "$total"
return 0
}
formatSize() { formatSize() {
local bytes="$1" local bytes="$1"
@@ -117,7 +155,19 @@ printSizeProgress() {
return 0 return 0
} }
finishLogProgress() { stopProgress() {
if [ -z "$status_file" ]; then
exit 0
fi
stopping="Y"
return 0
}
finishProgress() {
rm -f -- "$info_tmp"
if [[ "$output" == "log" && "$printed" == "Y" ]]; then if [[ "$output" == "log" && "$printed" == "Y" ]]; then
printf '\n' printf '\n'
@@ -132,6 +182,7 @@ body=$(escape "$3")
output="${4:-}" output="${4:-}"
step_bytes="${5:-536870912}" step_bytes="${5:-536870912}"
mode="${6:-apparent}" mode="${6:-apparent}"
status_file="${7:-}"
if [[ -n "$total" && ! "$total" =~ ^(0|[1-9][0-9]*)$ ]]; then if [[ -n "$total" && ! "$total" =~ ^(0|[1-9][0-9]*)$ ]]; then
printf 'Invalid total size: %s\n' "$total" >&2 printf 'Invalid total size: %s\n' "$total" >&2
@@ -163,13 +214,17 @@ printed="N"
next_percent=10 next_percent=10
next_bytes="$step_bytes" next_bytes="$step_bytes"
log_mode="percent" log_mode="percent"
stopping="N"
if [ -z "$total" ] || [[ "$total" == "0" ]]; then if [ -z "$total" ] || [[ "$total" == "0" ]]; then
log_mode="size" log_mode="size"
fi fi
trap finishLogProgress EXIT trap finishProgress EXIT
trap 'exit 0' HUP INT QUIT TERM 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 if [[ "$body" == *"..." ]]; then
body="<p class=\"loading\">${body::-3}</p>" body="<p class=\"loading\">${body::-3}</p>"
@@ -177,13 +232,36 @@ fi
while true; do while true; do
final_pass="${stopping:-}"
bytes=$(getBytes "$path" "$mode") 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"
if (( status_total > 0 )); then
effective_total="$status_total"
fi
fi
# A real total may become available shortly after aria2 starts.
if [[ "$log_mode" == "size" &&
"$printed" == "N" &&
-n "$effective_total" &&
"$effective_total" != "0" ]]; then
log_mode="percent"
fi
if (( bytes > 4096 )); then if (( bytes > 4096 )); then
write_html="Y" write_html="Y"
if [ -z "$total" ] || [[ "$total" == "0" ]] || (( bytes > total )); then if [ -z "$effective_total" ] ||
[[ "$effective_total" == "0" ]] ||
(( bytes > effective_total )); then
size=$(formatSize "$bytes") size=$(formatSize "$bytes")
if [[ "$output" == "log" ]]; then if [[ "$output" == "log" ]]; then
@@ -196,8 +274,10 @@ 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 / total)) progress=$((bytes * 1000 / effective_total))
(( progress > 1000 )) && progress=1000 (( progress > 1000 )) && progress=1000
percent=$((progress / 10)) percent=$((progress / 10))
@@ -219,9 +299,12 @@ while true; do
fi fi
if [[ "$write_html" == "Y" ]]; then if [[ "$write_html" == "Y" ]]; then
printf '%s\n' "${body//(\[P\])/($size)}" > "$info" writeInfo "${body//(\[P\])/($size)}"
fi fi
fi fi
sleep 1 & wait $! [[ "$final_pass" == "Y" ]] && break
sleep 1 &
wait $! || :
done done
+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"
+108 -22
View File
@@ -2,37 +2,71 @@
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
} }
configureWebPorts() { configureWebPorts() {
sed -i "s/listen 5000 default_server;/listen $WEB_PORT default_server;/g" /etc/nginx/sites-enabled/web.conf if ! sed -i \
sed -i "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 -e "s|listen 5000 default_server;|listen $WEB_PORT default_server;|g" \
/etc/nginx/sites-enabled/web.conf; then
error "Failed to configure webserver port!"
return 1
fi
return 0 return 0
} }
configureIpv6Listen() { configureIpv6Listen() {
# shellcheck disable=SC2143 # 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
sed -i "s/listen $WEB_PORT default_server;/listen [::]:$WEB_PORT default_server ipv6only=off;/g" /etc/nginx/sites-enabled/web.conf
if ! sed -i \
"s/listen $WEB_PORT default_server;/listen [::]:$WEB_PORT default_server ipv6only=off;/g" \
/etc/nginx/sites-enabled/web.conf; then
error "Failed to configure IPv6 webserver listener!"
return 1
fi
fi
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 fi
return 0 return 0
@@ -40,15 +74,31 @@ configureIpv6Listen() {
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
return 0 return 0
} }
stopWebServer() {
local pid
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
fi
rm -f -- "$WEB_PID"
return 0
}
startWebServer() { startWebServer() {
# Start webserver # Start webserver
@@ -57,12 +107,27 @@ startWebServer() {
return 0 return 0
} }
stopWebsocketServer() {
local pid
if readPidFile pid "$WSD_PID"; then
pKill "$pid" 2
if isAlive "$pid"; then
kill -9 -- "$pid" 2>/dev/null || :
fi
fi
rm -f -- "$WSD_PID" "$WSD_SOCKET"
return 0
}
startWebsocketServer() { 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 &
@@ -70,19 +135,36 @@ startWebsocketServer() {
if ! echo "$pid" > "$WSD_PID"; then if ! echo "$pid" > "$WSD_PID"; then
kill "$pid" 2>/dev/null || : kill "$pid" 2>/dev/null || :
rm -f -- "$WSD_PID"
return 1 return 1
fi fi
sleep 0.1 local i
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
[ -S "$WSD_SOCKET" ] && return 0
sleep 0.1
done
pKill "$pid" 2
if isAlive "$pid"; then
kill -9 -- "$pid" 2>/dev/null || :
fi fi
return 0 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
@@ -93,7 +175,11 @@ disabled "${WEB:-}" && return 0
configureWebServer configureWebServer
startWebServer if startWebServer && startWebsocketServer; then
startWebsocketServer return 0
fi
return 0 stopWebsocketServer || :
stopWebServer || :
return 1
+22 -5
View File
@@ -3,6 +3,8 @@ set -Eeuo pipefail
lastmsg="" lastmsg=""
path="/run/shm/msg.html" path="/run/shm/msg.html"
dir=$(dirname -- "$path")
name=$(basename -- "$path")
refresh() { refresh() {
@@ -16,16 +18,31 @@ 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
} }
refresh refresh
inotifywait -m "$path" | inotifywait \
while read -r fp event fn; do -m -q \
-e close_write,moved_to,delete \
--format '%e %f' \
"$dir" |
while read -r event file; do
[[ "$file" == "$name" ]] || continue
case "${event,,}" in case "${event,,}" in
"modify"* ) refresh ;; "delete"* )
"delete_self" ) echo "c: vnc" ;; echo "c: vnc" ;;
esac # moved_to covers the atomic replacement used by html()/writeAtomic(),
# while close_write handles direct writers.
"close_write"* | "moved_to"* )
refresh ;;
esac
done done
+154 -29
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,19 +177,16 @@ waitPid() {
waitPidFile() { waitPidFile() {
local i=0 local 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"
@@ -168,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
@@ -201,12 +242,10 @@ fKill() {
sKill() { 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
@@ -240,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
} }
@@ -255,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")
@@ -272,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"
@@ -300,6 +399,27 @@ writeFile() {
return 0 return 0
} }
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
rm -f -- "$tmp"
return 1
fi
if ! mv -f -- "$tmp" "$path"; then
rm -f -- "$tmp"
return 1
fi
return 0
}
readFile() { readFile() {
local path="$1" local path="$1"
@@ -349,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
@@ -362,8 +484,7 @@ restoreState() {
escape () { escape () {
local s local s=${1//&/\&amp;}
s=${1//&/\&amp;}
s=${s//</\&lt;} s=${s//</\&lt;}
s=${s//>/\&gt;} s=${s//>/\&gt;}
s=${s//'"'/\&quot;} s=${s//'"'/\&quot;}
@@ -389,7 +510,7 @@ html() {
local title local title
local body local body
local script local script="${2:-}"
local footer local footer
title=$(escape "$APP") title=$(escape "$APP")
@@ -401,8 +522,6 @@ html() {
body="<p class=\"loading\">${body/.../}</p>" body="<p class=\"loading\">${body/.../}</p>"
fi fi
[ -n "${2:-}" ] && script="$2" || script=""
local HTML local HTML
HTML=$(<"$TEMPLATE") HTML=$(<"$TEMPLATE")
HTML="${HTML/\[1\]/$title}" HTML="${HTML/\[1\]/$title}"
@@ -411,8 +530,10 @@ html() {
HTML="${HTML/\[4\]/$footer}" HTML="${HTML/\[4\]/$footer}"
HTML="${HTML/\[5\]/$FOOTER2}" HTML="${HTML/\[5\]/$FOOTER2}"
echo "$HTML" > "$PAGE" || return 1 # Publish both the full page and websocket fragment atomically because nginx
echo "$body" > "$INFO" || return 1 # and websocketd may read them concurrently.
writeAtomic "$PAGE" "$HTML" || return 1
writeAtomic "$INFO" "$body" || return 1
return 0 return 0
} }
@@ -459,9 +580,9 @@ getCountry() {
local url=$1 local url=$1
local query=$2 local query=$2
local rc json result local json result
{ json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); rc=$?; } || : { json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); local rc=$?; } || :
(( rc != 0 )) && return 0 (( rc != 0 )) && return 0
{ result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || : { result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || :
@@ -484,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"
@@ -508,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:/;
} }
} }
+48 -8
View File
@@ -1,15 +1,27 @@
var timer;
var request; var request;
var booting = false; var booting = false;
var interval = 1000; var interval = 1000;
function abortRequest() {
if (!request) {
return false;
}
request.onreadystatechange = null;
request.abort();
request = null;
return true;
}
function getInfo() { function getInfo() {
var url = "msg.html"; var url = "msg.html";
try { try {
if (request) { abortRequest();
request.abort();
}
if (window.XMLHttpRequest) { if (window.XMLHttpRequest) {
request = new XMLHttpRequest(); request = new XMLHttpRequest();
@@ -56,7 +68,17 @@ function processInfo() {
return true; return true;
} }
var msg = request.responseText; var response = request;
request = null;
var status = response.status;
if (status == 502 || status == 503 || status == 504) {
schedule();
return true;
}
var msg = response.responseText;
if (msg == null || msg.length == 0) { if (msg == null || msg.length == 0) {
if (booting) { if (booting) {
@@ -68,9 +90,9 @@ function processInfo() {
return false; return false;
} }
var notFound = (request.status == 404); var notFound = (status == 404);
if (request.status == 200) { if (status == 200) {
if (msg.toLowerCase().indexOf("<html>") !== -1) { if (msg.toLowerCase().indexOf("<html>") !== -1) {
notFound = true; notFound = true;
} else { } else {
@@ -88,7 +110,7 @@ function processInfo() {
return true; return true;
} }
setError("Error: Received statuscode " + request.status); setError("Error: Received statuscode " + status);
return false; return false;
} catch (e) { } catch (e) {
@@ -157,7 +179,9 @@ function setError(text) {
} }
function schedule() { function schedule() {
setTimeout(getInfo, interval);
clearTimeout(timer);
timer = setTimeout(getInfo, interval);
} }
function reload() { function reload() {
@@ -179,11 +203,27 @@ function connect() {
switch (cmd) { switch (cmd) {
case "s": case "s":
var aborted = abortRequest();
processMsg(msg); processMsg(msg);
if (aborted &&
msg.toLowerCase().indexOf("href=") == -1) {
schedule();
}
break; break;
case "e": case "e":
if (abortRequest()) {
schedule();
}
setError(msg); setError(msg);
break; break;
default: default:
console.warn("Unknown event: " + cmd); console.warn("Unknown event: " + cmd);
break; break;