Compare commits

..
36 Commits
Author SHA1 Message Date
KroeseandGitHub 47af1d07f4 build: Update QEMU base image to v7.48 (#1394) 2026-08-19 20:11:14 +02:00
KroeseandGitHub 4660512f12 docs: GPU acceleration (#1393) 2026-08-18 05:15:43 +02:00
KroeseandGitHub 6d37a0426b build: Update QEMU base image to v7.47 (#1392) 2026-08-18 05:14:00 +02:00
KroeseandGitHub 604cad0207 feat: Support GPU acceleration for AMD GPU's (#1391) 2026-08-18 03:23:38 +02:00
KroeseandGitHub 3ad7e78787 fix: Sanitize config variables (#1390) 2026-08-18 03:02:33 +02:00
KroeseandGitHub 5f908488ef docs: GPU acceleration (#1389) 2026-08-18 02:30:58 +02:00
KroeseandGitHub dcdc796e59 docs: GPU acceleration (#1388) 2026-08-18 02:16:21 +02:00
KroeseandGitHub 087d876eaf docs: Add AMD GPU support (#1387) 2026-08-18 02:04:17 +02:00
KroeseandGitHub 25a7d55cdd fix: Improve argument ordering (#1386) 2026-08-17 15:01:24 +02:00
KroeseandGitHub 9683b195f3 docs: Environment variables (#1385) 2026-08-16 16:12:54 +02:00
KroeseandGitHub f810576651 docs: Environment variables (#1384) 2026-08-16 15:54:51 +02:00
KroeseandGitHub 5fd954bea4 fix: Set machine name (#1383) 2026-08-16 08:45:26 +02:00
KroeseandGitHub 768b8fa51a feat: Don't remove casing from machine name (#1382) 2026-08-16 07:27:05 +02:00
KroeseandGitHub b909555ef3 fix: Make RNG device configurable (#1381) 2026-08-16 06:18:04 +02:00
KroeseandGitHub a77026eafb build: Update QEMU base image to v7.46 (#1380) 2026-08-16 03:00:51 +02:00
KroeseandGitHub e8ef357ed2 fix: Restore HTTP portal redirect fallback (#1379) 2026-08-15 11:50:16 +02:00
KroeseandGitHub a16c27ecce fix: Use replacement navigation for web transitions (#1378) 2026-08-14 19:40:47 +02:00
KroeseandGitHub 1917eae0ae fix: Do not set no-store on index page (#1377) 2026-08-14 14:02:07 +02:00
KroeseandGitHub d5bb90f012 fix: Disable COW for Virtual DSM boot image on Btrfs (#1376) 2026-08-13 16:03:19 +02:00
KroeseandGitHub d854ae811c fix: Prevent caching of web root (#1375) 2026-08-13 07:00:48 +02:00
KroeseandGitHub 03993aa773 fix: Prevent estimated progress from reaching 100% (#1374) 2026-08-11 20:08:08 +02:00
KroeseandGitHub b39ff267f2 fix: Update entrypoint (#1373) 2026-08-11 17:48:21 +02:00
KroeseandGitHub f7870d1693 fix: Only disable no-store for msg.html (#1372) 2026-08-11 15:47:14 +02:00
KroeseandGitHub c648338a18 feat: Use Intel Mesa package from base image (#1371) 2026-08-11 15:42:50 +02:00
KroeseandGitHub 9fe52c874e fix: Prevent caching of web status (#1370) 2026-08-10 23:47:43 +02:00
KroeseandGitHub c5eae88dfc fix: Improve error reporting (#1369) 2026-08-10 14:33:35 +02:00
KroeseandGitHub 1d0ac30ae1 fix: Add comment (#1368) 2026-08-10 07:25:00 +02:00
KroeseandGitHub c355cbe5dc fix: Separate stopped container status lines (#1367) 2026-08-10 07:23:35 +02:00
KroeseandGitHub 6f69eb5c68 fix: Stop console after QEMU exits (#1366) 2026-08-10 03:36:49 +02:00
KroeseandGitHub 07595976b2 feat: Use stopAllServers from base image (#1365) 2026-08-10 03:01:45 +02:00
KroeseandGitHub ec8440db03 build: Switched to QEMU base image (#1363) 2026-08-10 02:06:17 +02:00
KroeseandGitHub 06ddd19d1a fix: Add missing HTML element (#1364) 2026-08-09 14:36:02 +02:00
KroeseandGitHub 04cd929c9c feat: Show progress while extracting DSM image (#1362) 2026-08-09 13:33:33 +02:00
KroeseandGitHub c64046411b fix: Preserve web status after connection loss (#1361) 2026-08-09 13:15:44 +02:00
KroeseandGitHub cb8f440564 feat: Add QMP configuration variables (#1360) 2026-08-09 11:24:49 +02:00
KroeseandGitHub 8b6624cf2f fix: Do not fail if marker cannot be created (#1359) 2026-08-08 15:51:56 +02:00
29 changed files with 1352 additions and 6105 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
uses: hadolint/hadolint-action@v3.4.0
with:
dockerfile: Dockerfile
ignore: DL3008
ignore: DL3008,DL3067
failure-threshold: warning
-
name: Validate JSON and YML files
+5 -42
View File
@@ -1,15 +1,12 @@
# syntax=docker/dockerfile:1
FROM qemux/qemu-host:2.06 AS builder
FROM debian:trixie-slim
FROM qemux/qemu-host:2.06 AS host
FROM scratch
ARG TARGETARCH
ARG TARGETPLATFORM
COPY --from=qemux/qemu:7.48 / /
ARG VERSION_ARG="0.0"
ARG VERSION_WSD="0.4.2"
ARG VERSION_CSTRUCT="4.7"
ARG VERSION_PASST="2026_07_28"
ARG DEBCONF_NOWARNINGS="yes"
ARG DEBIAN_FRONTEND="noninteractive"
@@ -20,44 +17,10 @@ RUN <<EOF
apt-get update
apt-get --no-install-recommends -y install \
jq \
tini \
curl \
wget \
fdisk \
unzip \
nginx \
procps \
ipcalc \
ethtool \
xz-utils \
iptables \
iproute2 \
dnsmasq \
fakeroot \
apt-utils \
net-tools \
e2fsprogs \
diffutils \
qemu-utils \
iputils-ping \
inotify-tools \
ca-certificates \
netcat-openbsd \
qemu-system-x86 \
python3 \
python3-pip \
python3-msgpack \
python3-pysodium
# Install Passt package
wget "https://github.com/qemus/passt/releases/download/v${VERSION_PASST}/passt_${VERSION_PASST}_${TARGETARCH}.deb" -O /tmp/passt.deb -q --timeout=10
dpkg -i /tmp/passt.deb
# Install Websocketd package
wget "https://github.com/qemus/websocketd/releases/download/v${VERSION_WSD}/websocketd-${VERSION_WSD}_${TARGETARCH}.deb" -O /tmp/wsd.deb -q --timeout=10
dpkg -i /tmp/wsd.deb
apt-get clean
# Install Python dependencies
@@ -71,12 +34,12 @@ EOF
COPY --chmod=755 ./src /run/
COPY --chmod=755 ./web /var/www/
COPY --chmod=755 --from=builder /qemu-host.bin /run/host.bin
COPY --chmod=755 --from=host /qemu-host.bin /run/host.bin
COPY --chmod=744 ./web/conf/nginx.conf /etc/nginx/default.conf
ADD --chmod=775 https://raw.githubusercontent.com/sud0woodo/patology/refs/heads/main/patology.py /run/extract.py
VOLUME /storage
EXPOSE 22 139 445 5000
EXPOSE 445 5000
ENV RAM_SIZE="2G"
ENV CPU_CORES="2"
+21 -8
View File
@@ -33,7 +33,7 @@ An empty default means the variable is unset and its value is determined automat
|---|---|---|
| `DISK_SIZE` | `256G` | Size of the main data disk. |
| `DISK_FMT` | `raw` | Disk image format: `raw` or `qcow2`. |
| `DISK_TYPE` | `scsi` | Disk device type, such as `sata`, `scsi`, `nvme`, or `blk`. |
| `DISK_TYPE` | `scsi` | Disk device type, such as `ide`, `scsi`, or `blk`. |
| `DISK_CACHE` | `none` | Disk cache mode, such as `none` or `writeback`. |
| `DISK_IO` | `native` | Disk I/O mode, such as `native`, `threads`, or `io_uring`. |
| `DISK_DISCARD` | `unmap` | Discard/TRIM mode for the primary disk. |
@@ -56,7 +56,7 @@ An empty default means the variable is unset and its value is determined automat
| `DEV` | `eth0` | Container network interface used as the uplink. |
| `MTU` | | MTU assigned to the guest network interface. |
| `MASK` | `255.255.255.0` | IPv4 netmask for guest network. |
| `TAP` | `dsm` | TAP or macvtap interface name. |
| `TAP` | `qemu` | TAP or macvtap interface name. |
| `BRIDGE` | `docker` | Bridge name used for NAT networking. |
| `HOST_PORTS` | | Ports excluded from guest forwarding. |
| `USER_PORTS` | | Additional ports to forward to DSM when using user-mode networking. |
@@ -71,24 +71,34 @@ An empty default means the variable is unset and its value is determined automat
| Variable | Default | Description |
|---|---|---|
| `DISPLAY` | `none` | Display backend, such as `vnc`, `disabled`, or `none`. |
| `LOSSY` | `N` | Enables lossy VNC compression to reduce bandwidth usage. |
| `VGA` | `none` | QEMU video adapter model. |
| `GPU` | `N` | Enables Intel iGPU acceleration. |
| `GPU` | `N` | Enables experimental GPU acceleration. |
| `RENDERNODE` | `/dev/dri/renderD128` | Render node used for GPU acceleration. |
## ⚙️ System
## 🎈 Memory Ballooning
Also see [Dynamic memory allocation](https://github.com/qemus/qemu/blob/master/docs/ballooning.md) for usage instructions and important caveats.
| Variable | Default | Description |
|---|---|---|
| `MACHINE` | `q35` | QEMU machine type. |
| `ARGUMENTS` | | Additional raw arguments appended to the QEMU command line. |
| `BALLOONING` | `N` | Enables dynamic memory ballooning. |
| `BALLOONING_MIN_MEM` | `33%` | Minimum amount of memory retained by the VM. |
| `BALLOONING_RAM_THRESHOLD` | `80.0` | Host RAM usage percentage at which ballooning begins adjusting memory. |
| `BALLOONING_RAM_THRESHOLD_HARD` | `90.0` | Host RAM usage percentage at which ballooning becomes more aggressive. |
| `BALLOONING_PSI_PRESSURE` | `10.0` | PSI memory pressure level at which ballooning becomes more aggressive. |
| `BALLOONING_PSI_PRESSURE_MAX` | `50.0` | PSI memory pressure level at which ballooning reaches its strongest response. |
| `BALLOONING_HYSTERESIS` | `128M` | Minimum memory change before the balloon target is updated. |
| `BALLOONING_KP` | `0.5` | Proportional gain used by the ballooning controller. |
| `BALLOONING_KI` | `0.05` | Integral gain used by the ballooning controller. |
| `BALLOONING_INTERVAL` | `5` | Polling interval in seconds. |
| `BALLOONING_DEBUG` | `N` | Enables debug output for the ballooning monitor. |
## 🔌 Shutdown
| Variable | Default | Description |
|---|---|---|
| `SHUTDOWN` | `Y` | Enables graceful shutdown. |
| `TIMEOUT` | `115` | Maximum time, in seconds, to wait before forcing DSM to stop. |
| `TIMEOUT` | `105` | Maximum time, in seconds, to wait before forcing DSM to stop. |
| `API_TIMEOUT` | `90` | Maximum time, in seconds, to wait for the shutdown API call. |
## 🐞 Debugging
@@ -98,3 +108,6 @@ An empty default means the variable is unset and its value is determined automat
| `DEBUG` | `N` | Enables verbose debug output. |
| `TRACE` | `N` | Enables shell command tracing. |
| `HOST_DEBUG` | `N` | Enables debug output for the DSM host helper. |
| `MONITOR` | | QEMU monitor configuration. |
| `QMP` | | QEMU Machine Protocol configuration. |
| `ARGUMENTS` | | Additional raw arguments appended to the QEMU command line. |
+10 -4
View File
@@ -22,6 +22,7 @@ Virtual DSM in a Docker container.
- Near-native performance with KVM acceleration
- Customizable CPU, memory, and storage allocation
- Supports multiple disks and physical disk passthrough
- Dynamic memory allocation with memory ballooning
- Supports NAT, user-mode, macvlan, and macvtap networking
## Usage 🐳
@@ -200,9 +201,9 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
- 'c *:* rwm'
```
### How do I pass through the GPU?
### How do I enable GPU acceleration?
To pass through your Intel GPU, add the following lines to your compose file:
To enable hardware-accelerated graphics, add the following lines to your compose file:
```yaml
environment:
@@ -211,8 +212,13 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
- /dev/dri
```
> [!NOTE]
> This can be used to enable the facial recognition function in Synology Photos, but does not provide hardware transcoding for video.
This can be used to enable the facial recognition function in Synology Photos, but does not provide hardware transcoding for video.
### How do I enable dynamic memory allocation?
By default, the DSM is allocated the full amount of RAM configured via `RAM_SIZE` for its entire lifetime.
However, you can enable [memory ballooning](https://github.com/qemus/qemu/blob/master/docs/ballooning.md) if you want the container to dynamically reclaim unused guest RAM based on host memory pressure.
### How do I install a specific version of vDSM?
+7 -3
View File
@@ -8,20 +8,24 @@ cd /run
: "${NETWORK:="Y"}"
[ -f "/run/shm/qemu.end" ] && echo "QEMU is shutting down..." && exit 1
# Treat the startup window as healthy so container health checks do not restart
# the service before QEMU has had time to publish its PID.
[ ! -s "/run/shm/qemu.pid" ] && echo "QEMU is not running yet..." && exit 0
disabled "$NETWORK" && echo "Networking is disabled." && exit 0
file="/run/shm/dsm.url"
address="/run/shm/qemu.ip"
address="/run/shm/qemu.host"
gateway="/run/shm/qemu.gw"
# dsm.url is written only after the guest agent reports both the DSM
# address and its configured HTTP port.
[ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 0
[ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 0
location=$(<"$file")
if ! curl -m 20 -ILfSs "http://$location/" > /dev/null; then
if ! curl -m 20 -LfSs -o /dev/null "http://$location/"; 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.
+51 -11
View File
@@ -1,8 +1,18 @@
#!/usr/bin/env bash
set -Eeuo pipefail
DEF_OPTS="-nodefaults -boot strict=on"
DEV_OPTS=""
: "${RNG:=""}"
: "${QMP:=""}"
: "${UUID:=""}"
: "${MONITOR:=""}"
msg="Configuring QEMU..."
enabled "$DEBUG" && echo "$msg"
# Sanitize variables
QMP=$(strip "$QMP")
UUID=$(strip "$UUID")
MONITOR=$(strip "$MONITOR")
configureProcessor() {
@@ -23,37 +33,67 @@ configureMemory() {
configureMonitor() {
MON_OPTS="-name $PROCESS,process=$PROCESS,debug-threads=on"
MON_OPTS+=" -pidfile $QEMU_PID"
MON_OPTS=""
[ -n "$QMP" ] && MON_OPTS+=" -qmp $QMP"
[ -n "$MONITOR" ] && MON_OPTS+=" -monitor $MONITOR"
MON_OPTS="${MON_OPTS# }"
local name="${APP// /-}"
ID_OPTS="-name $name,process=$PROCESS,debug-threads=on"
PID_OPTS="-pidfile $QEMU_PID"
return 0
}
configureMachine() {
local smm="off"
enabled "${SMM:-}" && smm="on"
# 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=$smm,usb=off"
MAC_OPTS+=",vmport=off,dump-guest-core=off,hpet=off${KVM_OPTS}"
[ -n "$UUID" ] && ID_OPTS+=" -uuid $UUID"
[ -n "${SM_BIOS:-}" ] && ID_OPTS+=" $SM_BIOS"
return 0
}
configureVirtioDevices() {
configureDevices() {
local bus
bus=$(getPciBus)
DEV_OPTS="-device virtio-balloon-pci,id=balloon0,bus=$bus,addr=0x4"
DEV_OPTS+=" -object rng-random,id=objrng0,filename=/dev/urandom"
DEV_OPTS+=" -device virtio-rng-pci,rng=objrng0,id=rng0,bus=$bus,addr=0x1c"
DEV_OPTS=""
if ! disabled "$RNG"; then
DEV_OPTS+=" -object rng-random,id=objrng0,filename=/dev/urandom"
DEV_OPTS+=" -device virtio-rng-pci,rng=objrng0,id=rng0,bus=$bus,addr=0x1c"
fi
# Keep the existing balloon device by default. When dynamic ballooning is
# enabled, expose guest statistics and a dedicated QMP control socket.
if ! enabled "${BALLOONING:-}"; then
DEV_OPTS+=" -device virtio-balloon-pci,id=balloon0,bus=$bus,addr=0x4"
else
MON_OPTS+=" -qmp unix:${BALLOONING_SOCKET},server=on,wait=off"
DEV_OPTS+=" -device virtio-balloon-pci,free-page-reporting=on,guest-stats-polling-interval=1,id=balloon0,bus=$bus,addr=0x4"
fi
DEV_OPTS="${DEV_OPTS# }"
return 0
}
buildArguments() {
ARGS="$DEF_OPTS $CPU_OPTS $RAM_OPTS $MAC_OPTS $DISPLAY_OPTS $MON_OPTS $SERIAL_OPTS $NET_OPTS $DISK_OPTS $DEV_OPTS $ARGUMENTS"
local default="-nodefaults"
local boot="-boot strict=on"
ARGS="$default $MAC_OPTS $CPU_OPTS $RAM_OPTS $ID_OPTS $PID_OPTS $DISPLAY_OPTS $MON_OPTS $SERIAL_OPTS $NET_OPTS $DISK_OPTS $boot $BOOT_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 ' ')
@@ -67,7 +107,7 @@ configureMemory
configureMonitor
configureMachine
configureProcessor
configureVirtioDevices
configureDevices
buildArguments
-871
View File
@@ -1,871 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Docker environment variables
: "${DISK_IO:="native"}" # I/O Mode, can be set to 'native', 'threads' or 'io_uring'
: "${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_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_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
# Sanitize all variables
DISK_IO=$(strip "$DISK_IO")
DISK_FMT=$(strip "$DISK_FMT")
DISK_TYPE=$(strip "$DISK_TYPE")
DISK_FLAGS=$(strip "$DISK_FLAGS")
DISK_OPTIONS=$(strip "$DISK_OPTIONS")
DISK_CACHE=$(strip "$DISK_CACHE")
DISK_DISCARD=$(strip "$DISK_DISCARD")
DISK_ROTATION=$(strip "$DISK_ROTATION")
BOOT="$STORAGE/$BASE.boot.img"
SYSTEM="$STORAGE/$BASE.system.img"
# The boot and system images are installation artifacts, not optional data
# disks, and must exist before any user storage is attached.
[ ! -s "$BOOT" ] && error "Virtual DSM boot-image does not exist ($BOOT)" && exit 81
[ ! -s "$SYSTEM" ] && error "Virtual DSM system-image does not exist ($SYSTEM)" && exit 82
if ! setOwner "$BOOT"; then
warn "failed to set the owner for \"$BOOT\" !"
fi
if ! setOwner "$SYSTEM"; then
warn "failed to set the owner for \"$SYSTEM\" !"
fi
fmt2ext() {
local diskFmt="$1"
case "${diskFmt,,}" in
qcow2) echo "qcow2" ;;
raw) echo "img" ;;
*) error "Unrecognized disk format: $diskFmt" && exit 78 ;;
esac
}
ext2fmt() {
local diskExt="$1"
case "${diskExt,,}" in
qcow2) echo "qcow2" ;;
img) echo "raw" ;;
*) error "Unrecognized file extension: .$diskExt" && exit 78 ;;
esac
}
getSize() {
local diskFile="$1"
local diskExt diskFmt size
diskExt=$(echo "${diskFile//*./}" | sed 's/^.*\.//')
diskFmt=$(ext2fmt "$diskExt")
case "${diskFmt,,}" in
raw)
stat -c%s "$diskFile"
;;
qcow2)
size=$(qemu-img info --output=json -f "$diskFmt" "$diskFile" | jq -r '."virtual-size" // empty')
if [[ ! "$size" =~ ^[0-9]+$ ]]; then
error "Failed to determine virtual size of $diskFile"
exit 78
fi
echo "$size"
;;
*)
error "Unrecognized disk format: $diskFmt"
exit 78
;;
esac
}
isCow() {
local fs="$1"
if [[ "${fs,,}" == "btrfs" ]]; then
return 0
fi
return 1
}
supportsDirect() {
local fs="$1"
if [[ "${fs,,}" == "ecryptfs" || "${fs,,}" == "tmpfs" ]]; then
return 1
fi
return 0
}
validDiskType() {
case "${1,,}" in
"ide" | "sata" | "nvme" | "usb" | "scsi" | "blk" | \
"virtio-blk" | "virtio-scsi" | "auto" | "none" )
return 0
;;
esac
return 1
}
allocateRaw() {
local diskFile="$1"
local dataSize="$2"
if disabled "$ALLOCATE"; then
truncate -s "$dataSize" "$diskFile"
return $?
fi
# Prefer real allocation, retry with zero-range allocation where supported,
# and fall back to a sparse file when the host filesystem rejects both.
fallocate -l "$dataSize" "$diskFile" &>/dev/null && return 0
fallocate -l -x "$dataSize" "$diskFile" && return 0
truncate -s "$dataSize" "$diskFile" || return 1
return 0
}
getDiskOptions() {
local fs="$1"
local diskFmt="$2"
local diskParam="$DISK_ALLOC"
isCow "$fs" && diskParam+=",nocow=on"
if [[ "${diskFmt,,}" != "raw" ]]; then
[ -n "$DISK_FLAGS" ] && diskParam+=",$DISK_FLAGS"
fi
echo "$diskParam"
return 0
}
normalizeSize() {
local diskSpace="$1"
local diskDesc="$2"
local dir="$3"
local free dataSize
local spare=1073741824
# Dynamic sizes are resolved once from current free space. max reserves one
# GiB for host metadata and container activity; half uses half the space.
if [[ "${diskSpace,,}" == "max" || "${diskSpace,,}" == "half" ]]; then
free=$(df --output=avail -B 1 "$dir" | tail -n 1)
if [[ "${diskSpace,,}" == "max" ]]; then
free=$(( free - spare ))
else
free=$(( free / 2 ))
fi
(( free < spare )) && free="$spare"
local gb=$(( free / 1073741825 ))
diskSpace="${gb}G"
fi
local space="${diskSpace// /}"
[ -z "$space" ] && space="256G"
[ -z "${space//[0-9. ]}" ] && space="${space}G"
space=$(echo "${space^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
if ! numfmt --from=iec "$space" &>/dev/null; then
error "Invalid value for ${diskDesc^^}_SIZE: $diskSpace" && exit 73
fi
dataSize=$(numfmt --from=iec "$space")
if (( dataSize < 6442450944 )); then
error "Please increase the ${diskDesc^^}_SIZE variable to at least 6 GB." && exit 73
fi
echo "$space"
return 0
}
baseDir() {
local path="${1%/}"
[[ -z "$path" || "$path" == "/" ]] && {
echo "/"
return 0
}
path="${path#/}"
path="${path%%/*}"
echo "/$path"
return 0
}
freeSpace() {
local path="$1"
local base
base=$(baseDir "$path")
if ! available=$(df --output=avail -B 1 "$path" | tail -n 1); then
error "Failed to check free space in $base."
exit 76
fi
if [[ ! "$available" =~ ^[0-9]+$ ]]; then
error "Failed to check free space in $base."
exit 76
fi
return 0
}
createDisk() {
local diskFile="$1"
local diskSpace="$2"
local diskDesc="$3"
local diskFmt="$4"
local fs="$5"
local gb dir base
local attributes available
rm -f "$diskFile"
local dataSize
dataSize=$(numfmt --from=iec "$diskSpace")
if ! disabled "$ALLOCATE"; then
# Check free diskspace
dir=$(dirname "$diskFile")
base=$(baseDir "$dir")
freeSpace "$dir"
if (( dataSize > available )); then
gb=$(formatBytes "$available")
error "Not enough free space to create a $diskDesc of ${diskSpace/G/ GB} in $base, it has only $gb available..."
error "Please specify a smaller ${diskDesc^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 76
fi
fi
html "Creating a $diskDesc image..."
info "Creating a ${diskSpace/G/ GB} $DISK_STYLE $diskDesc image in $diskFmt format..."
local failure="Could not create a $DISK_STYLE $diskFmt $diskDesc image of ${diskSpace/G/ GB} ($diskFile)"
case "${diskFmt,,}" in
raw)
if isCow "$fs"; then
if ! touch "$diskFile"; then
error "$failure" && exit 77
fi
{ chattr +C "$diskFile"; } || :
fi
if ! allocateRaw "$diskFile" "$dataSize"; then
rm -f "$diskFile"
error "$failure" && exit 77
fi
;;
qcow2)
local diskParam
diskParam=$(getDiskOptions "$fs" "$diskFmt")
if ! qemu-img create -f "$diskFmt" -o "$diskParam" -- "$diskFile" "$dataSize" ; then
rm -f "$diskFile"
error "$failure" && exit 70
fi
;;
esac
if isCow "$fs"; then
attributes=$(lsattr "$diskFile")
if [[ "$attributes" != *"C"* ]]; then
error "Failed to disable COW for $diskDesc image $diskFile on ${fs^^} filesystem (returned $attributes)"
fi
fi
return 0
}
resizeDisk() {
local diskFile="$1"
local diskSpace="$2"
local diskDesc="$3"
local diskFmt="$4"
local fs="$5"
local gb dir base dataSize
local available currentSize
currentSize=$(getSize "$diskFile") || exit 71
dataSize=$(numfmt --from=iec "$diskSpace")
local required=$(( dataSize - currentSize ))
(( required < 1 )) && error "Shrinking disks is not supported yet, please increase ${diskDesc^^}_SIZE." && exit 71
if ! disabled "$ALLOCATE"; then
# Check free diskspace
dir=$(dirname "$diskFile")
base=$(baseDir "$dir")
freeSpace "$dir"
if (( required > available )); then
gb=$(formatBytes "$available")
error "Not enough free space to resize $diskDesc to ${diskSpace/G/ GB} in $base, it has only $gb available.."
error "Please specify a smaller ${diskDesc^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 74
fi
fi
gb=$(formatBytes "$currentSize")
MSG="Resizing $diskDesc from $gb to ${diskSpace/G/ GB}..."
info "$MSG" && html "$MSG"
local failure="Could not resize the $DISK_STYLE $diskFmt $diskDesc image from ${gb} to ${diskSpace/G/ GB} ($diskFile)"
case "${diskFmt,,}" in
raw)
if ! allocateRaw "$diskFile" "$dataSize"; then
error "$failure" && exit 75
fi
;;
qcow2)
if ! qemu-img resize -f "$diskFmt" "--$DISK_ALLOC" "$diskFile" "$dataSize" ; then
error "$failure" && exit 72
fi
;;
esac
return 0
}
convertDisk() {
local sourceFile="$1"
local sourceFmt="$2"
local destinationFile="$3"
local destinationFmt="$4"
local diskBase="$5"
local diskDesc="$6"
local fs="$7"
local tmpFile="$diskBase.tmp"
local gb dir base attributes
local available currentSize
[ -f "$destinationFile" ] && error "Conversion failed, destination file $destinationFile already exists?" && exit 79
[ ! -f "$sourceFile" ] && error "Conversion failed, source file $sourceFile does not exist?" && exit 79
rm -f "$tmpFile"
dir=$(dirname "$tmpFile")
base=$(baseDir "$dir")
if ! disabled "$ALLOCATE"; then
# Check free diskspace
currentSize=$(getSize "$sourceFile") || exit 79
freeSpace "$dir"
if (( currentSize > available )); then
gb=$(formatBytes "$available")
error "Not enough free space to convert $diskDesc to $destinationFmt in $base, it has only $gb available..."
error "Please free up some disk space or disable preallocation by setting ALLOCATE=N." && exit 76
fi
fi
local msg="Converting $diskDesc to $destinationFmt"
html "$msg..."
info "$msg, please wait until completed..."
local convertFlags="-p"
local diskParam
diskParam=$(getDiskOptions "$fs" "$destinationFmt")
if [[ "$destinationFmt" != "raw" ]]; then
if disabled "$ALLOCATE"; then
convertFlags+=" -c"
fi
fi
# shellcheck disable=SC2086
if ! qemu-img convert -f "$sourceFmt" $convertFlags -o "$diskParam" -O "$destinationFmt" -- "$sourceFile" "$tmpFile"; then
rm -f "$tmpFile"
error "Failed to convert $DISK_STYLE $diskDesc image to $destinationFmt format in $base, is there enough space available?" && exit 79
fi
if [[ "$destinationFmt" == "raw" ]]; then
if ! disabled "$ALLOCATE"; then
# qemu-img may leave converted raw output sparse despite requested
# preallocation, so allocate its final length explicitly afterward.
# Work around qemu-img bug
if ! currentSize=$(stat -c%s "$tmpFile"); then
error "Failed to determine converted image size: $tmpFile"
exit 79
fi
if ! fallocate -l "$currentSize" "$tmpFile" &>/dev/null; then
if ! fallocate -l -x "$currentSize" "$tmpFile"; then
error "Failed to allocate $currentSize bytes for $diskDesc image $tmpFile"
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
error "Failed to move converted $diskDesc image to $destinationFile."
exit 79
fi
if ! rm -f "$sourceFile"; then
error "Failed to remove old $diskDesc image $sourceFile."
exit 79
fi
if isCow "$fs"; then
attributes=$(lsattr "$destinationFile")
if [[ "$attributes" != *"C"* ]]; then
error "Failed to disable COW for $diskDesc image $destinationFile on ${fs^^} filesystem (returned $attributes)"
fi
fi
msg="Conversion of $diskDesc"
info "$msg to $destinationFmt completed successfully!"
return 0
}
checkFS () {
local fs="$1"
local diskFile="$2"
local diskDesc="$3"
local dir base
local attributes
dir=$(dirname "$diskFile")
base=$(baseDir "$dir")
[ ! -d "$dir" ] && return 0
if [[ "${fs,,}" == "overlay"* && "${ENGINE,,}" == "docker" ]]; then
warn "the filesystem of $base is OverlayFS, this usually means it was binded to an invalid path!"
fi
if [[ "${fs,,}" == "fuse"* ]]; then
warn "the filesystem of $base is FUSE, this extra layer will negatively affect performance!"
fi
# Filesystems without O_DIRECT support require threaded I/O and writeback
# caching; native AIO with cache=none would fail at runtime.
if ! supportsDirect "$fs"; then
warn "the filesystem of $base is $fs, which does not support O_DIRECT mode, adjusting settings..."
fi
if isCow "$fs"; then
if [ -f "$diskFile" ]; then
attributes=$(lsattr "$diskFile")
if [[ "$attributes" != *"C"* ]]; then
warn "COW (copy on write) is not disabled for $diskDesc image file $diskFile, this is recommended on ${fs^^} filesystems!"
fi
fi
fi
return 0
}
createDevice () {
local diskFile="$1"
local diskType="$2"
local diskIndex="$3"
local diskAddress="$4"
local diskFmt="$5"
local diskIo="$6"
local diskCache="$7"
local diskSerial="$8"
local diskSectors="$9"
local bus
bus=$(getPciBus)
local options=""
[ -n "$DISK_OPTIONS" ] && options=",${DISK_OPTIONS#,}"
local bootIndex=""
local diskId="data$diskIndex"
[ -n "$diskIndex" ] && bootIndex=",bootindex=$diskIndex"
local result=" -drive file=$diskFile,id=$diskId,format=$diskFmt,cache=$diskCache,aio=$diskIo,discard=$DISK_DISCARD,detect-zeroes=on"
case "${diskType,,}" in
"none" ) ;;
"auto" )
echo "$result"
;;
"usb" )
result+=",if=none \
-device usb-storage,drive=${diskId}${bootIndex}${diskSerial}${diskSectors}${options}"
echo "$result"
;;
"nvme" )
result+=",if=none \
-device nvme,drive=${diskId}${bootIndex},serial=deadbeaf${diskIndex}${diskSerial}${diskSectors}${options}"
echo "$result"
;;
"ide" | "sata" )
result+=",if=none \
-device ich9-ahci,id=ahci${diskIndex},addr=$diskAddress \
-device ide-hd,drive=${diskId},bus=ahci$diskIndex.0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}${options}"
echo "$result"
;;
"blk" | "virtio-blk" )
result+=",if=none \
-device virtio-blk-pci,drive=${diskId},bus=$bus,addr=$diskAddress,iothread=io2${bootIndex}${diskSerial}${diskSectors}${options}"
echo "$result"
;;
"scsi" | "virtio-scsi" )
result+=",if=none \
-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}${options}"
echo "$result"
;;
esac
return 0
}
finishDisks () {
case "${DISK_TYPE,,}" in
"blk" | "scsi" | "virtio-blk" | "virtio-scsi" )
# VirtIO block and SCSI devices share one dedicated I/O thread, which
# must be declared exactly once regardless of disk count.
[[ "$DISK_OPTS" != *" -object iothread,id=io2"* ]] && DISK_OPTS+=" -object iothread,id=io2" ;;
esac
return 0
}
addDisk () {
local diskBase="$1"
local diskType="$2"
local diskDesc="$3"
local diskSpace="$4"
local diskIndex="$5"
local diskAddress="$6"
local diskFmt="$7"
local diskIo="$8"
local diskCache="$9"
local fs dir used space
local diskExt dataSize
local available currentSize
local previousExt
diskExt=$(fmt2ext "$diskFmt")
local diskFile="$diskBase.$diskExt"
dir=$(dirname "$diskFile")
[ ! -d "$dir" ] && return 0
space=$(normalizeSize "$diskSpace" "$diskDesc" "$dir")
dataSize=$(numfmt --from=iec "$space")
if ! fs=$(stat -f -c %T "$dir"); then
error "Failed to determine filesystem type of \"$dir\" !"
return 1
fi
checkFS "$fs" "$diskFile" "$diskDesc" || exit $?
if ! supportsDirect "$fs"; then
diskIo="threads"
diskCache="writeback"
fi
if [ ! -f "$diskFile" ] || [ ! -s "$diskFile" ]; then
if [[ "${diskFmt,,}" != "raw" ]]; then
local previousFmt="raw"
else
local previousFmt="qcow2"
fi
previousExt=$(fmt2ext "$previousFmt")
# Treat a disk in the other supported format as the same logical disk and
# convert it automatically instead of creating an empty replacement.
if [ -f "$diskBase.$previousExt" ] &&
[ -s "$diskBase.$previousExt" ]; then
convertDisk "$diskBase.$previousExt" "$previousFmt" "$diskFile" "$diskFmt" "$diskBase" "$diskDesc" "$fs" || exit $?
fi
fi
if [ -f "$diskFile" ] && [ -s "$diskFile" ]; then
currentSize=$(getSize "$diskFile") || exit 71
if (( dataSize > currentSize )); then
resizeDisk "$diskFile" "$space" "$diskDesc" "$diskFmt" "$fs" || exit $?
else
if (( dataSize < currentSize )); then
if [[ "${diskSpace,,}" != "max" && "${diskSpace,,}" != "half" ]]; then
info "You decreased the ${diskDesc^^}_SIZE variable to ${diskSpace/G/ GB} but shrinking disks is not supported, will be ignored..."
fi
fi
fi
else
createDisk "$diskFile" "$space" "$diskDesc" "$diskFmt" "$fs" || exit $?
fi
# Sparse disks can promise more guest capacity than the host can currently
# satisfy, so report the future shortfall without blocking startup.
if [ -f "$diskFile" ] && disabled "$ALLOCATE"; then
currentSize=$(getSize "$diskFile") || exit 73
used=$(du -sB 1 "$diskFile" | cut -f1)
available=$(df --output=avail -B 1 "$dir" | tail -n 1)
local missing=$(( currentSize - used - available ))
(( missing < 0 )) && missing=0
if (( missing > 0 )); then
local gb base
gb=$(formatBytes "$available")
base=$(baseDir "$dir")
missing=$(formatBytes "$missing")
currentSize=$(formatBytes "$currentSize")
local msg="The virtual size of the ${diskDesc,,} is $currentSize"
if [ -n "$used" ] && [[ "$used" != "0" ]]; then
used=$(formatBytes "$used")
msg+=" (of which $used is used)"
fi
info "$msg, but there is only $gb of free space remaining in $base now."
info "Please consider making at least $missing more space available in $base for future expansions."
fi
fi
if [ -f "$diskFile" ]; then
if ! setOwner "$diskFile"; then
warn "failed to set the owner for \"$diskFile\" !"
fi
fi
DISK_OPTS+=$(createDevice "$diskFile" "$diskType" "$diskIndex" "$diskAddress" "$diskFmt" "$diskIo" "$diskCache" "" "")
return 0
}
addDevice () {
local diskDev="$1"
local diskType="$2"
local diskIndex="$3"
local diskAddress="$4"
local sectors=""
local devType
devType=$(lsblk -no TYPE "$diskDev" 2>/dev/null | head -n1)
[ -z "$diskDev" ] && return 0
[ ! -b "$diskDev" ] && error "Device $diskDev cannot be found! Please add it to the 'devices' section of your compose file." && exit 55
# DSM may reject whole-disk passthrough when QEMU is given explicit sector
# geometry; partitions need it to preserve non-512-byte host geometry.
# Only detect and apply sector sizes for partitions, not whole disks.
# Whole disk passthrough with explicit sector sizes causes DSM not to recognize the disk.
if [[ "$devType" == "part" ]]; then
local result
local logical="" physical=""
result=$(fdisk -l "$diskDev" 2>/dev/null | grep -m 1 -o "(logical/physical): .*" | cut -c 21- || true)
if [ -n "$result" ]; then
logical="${result%% *}"
physical=$(echo "$result" | grep -m 1 -o "/ .*" | cut -c 3- || true)
physical="${physical%% *}"
fi
if [ -z "$logical" ] || [ -z "$physical" ]; then
warn "Failed to determine the sector size for $diskDev"
elif [[ "$physical" != "512" ]]; then
sectors=",logical_block_size=$logical,physical_block_size=$physical"
fi
fi
DISK_OPTS+=$(createDevice "$diskDev" "$diskType" "$diskIndex" "$diskAddress" "raw" "$DISK_IO" "$DISK_CACHE" "" "$sectors")
return 0
}
[ -z "${DISK_OPTS:-}" ] && DISK_OPTS=""
[ -z "${DISK_TYPE:-}" ] && DISK_TYPE="scsi"
[ -z "${DISK_NAME:-}" ] && DISK_NAME="data"
[ -z "${DISK_DISABLE:-}" ] && DISK_DISABLE=""
if ! enabled "$DISK_DISABLE"; then
msg="Initializing disks..."
enabled "$DEBUG" && echo "$msg"
fi
if [[ "${DISK_IO,,}" == "native" && "${DISK_CACHE,,}" != "none" && "${DISK_CACHE,,}" != "directsync" ]]; then
warn "DISK_IO=native requires direct I/O caching, using DISK_IO=threads with DISK_CACHE=$DISK_CACHE."
DISK_IO="threads"
fi
case "${DISK_DISCARD,,}" in
"y" | "yes" | "true" | "1" | "on" | "unmap" )
DISK_DISCARD="unmap" ;;
"n" | "no" | "false" | "0" | "off" | "ignore" )
DISK_DISCARD="ignore" ;;
* )
warn "Invalid DISK_DISCARD value '$DISK_DISCARD', using 'unmap'."
DISK_DISCARD="unmap" ;;
esac
if [[ ! "$DISK_ROTATION" =~ ^[0-9]+$ ]]; then
warn "Invalid DISK_ROTATION value '$DISK_ROTATION', using 1."
DISK_ROTATION="1"
fi
DISK_FMT="${DISK_FMT,,}"
case "$DISK_FMT" in
"raw" | "qcow2" ) ;;
* ) error "Invalid DISK_FMT specified, value \"$DISK_FMT\" is not recognized!" && exit 78 ;;
esac
if ! validDiskType "$DISK_TYPE"; then
error "Invalid DISK_TYPE specified, value \"$DISK_TYPE\" is not recognized!"
exit 80
fi
if [[ "$DISK_FLAGS" =~ [[:space:]] ]]; then
error "Invalid DISK_FLAGS value '$DISK_FLAGS', spaces are not allowed."
exit 78
fi
if [[ "$DISK_OPTIONS" =~ [[:space:]] ]]; then
error "Invalid DISK_OPTIONS value '$DISK_OPTIONS', spaces are not allowed."
exit 78
fi
if [ -z "$ALLOCATE" ]; then
ALLOCATE="N"
fi
if disabled "$ALLOCATE"; then
DISK_STYLE="growable"
DISK_ALLOC="preallocation=off"
else
DISK_STYLE="preallocated"
DISK_ALLOC="preallocation=falloc"
fi
# Reserve the first two boot indexes and PCI addresses for the managed boot
# and system images; user disks begin at index 3.
DISK_OPTS+=$(createDevice "$BOOT" "$DISK_TYPE" "1" "0xa" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
DISK_OPTS+=$(createDevice "$SYSTEM" "$DISK_TYPE" "2" "0xb" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
if enabled "$DISK_DISABLE"; then
finishDisks && return 0
fi
DISK1_FILE="$STORAGE/${DISK_NAME}"
DISK2_FILE="/storage2/${DISK_NAME}2"
DISK3_FILE="/storage3/${DISK_NAME}3"
DISK4_FILE="/storage4/${DISK_NAME}4"
: "${DISK2_SIZE:=""}"
: "${DISK3_SIZE:=""}"
: "${DISK4_SIZE:=""}"
: "${DEVICE:=""}" # Docker variables to passthrough a block device, like /dev/vdc1.
: "${DEVICE2:=""}"
: "${DEVICE3:=""}"
: "${DEVICE4:=""}"
[ -z "$DEVICE" ] && [ -b "/disk" ] && DEVICE="/disk"
[ -z "$DEVICE" ] && [ -b "/disk1" ] && DEVICE="/disk1"
[ -z "$DEVICE2" ] && [ -b "/disk2" ] && DEVICE2="/disk2"
[ -z "$DEVICE3" ] && [ -b "/disk3" ] && DEVICE3="/disk3"
[ -z "$DEVICE4" ] && [ -b "/disk4" ] && DEVICE4="/disk4"
[ -z "$DEVICE" ] && [ -b "/dev/disk1" ] && DEVICE="/dev/disk1"
[ -z "$DEVICE2" ] && [ -b "/dev/disk2" ] && DEVICE2="/dev/disk2"
[ -z "$DEVICE3" ] && [ -b "/dev/disk3" ] && DEVICE3="/dev/disk3"
[ -z "$DEVICE4" ] && [ -b "/dev/disk4" ] && DEVICE4="/dev/disk4"
DISK_FILES=( "$DISK1_FILE" "$DISK2_FILE" "$DISK3_FILE" "$DISK4_FILE" )
DISK_DESCS=( "disk" "disk2" "disk3" "disk4" )
DISK_SIZES=( "$DISK_SIZE" "$DISK2_SIZE" "$DISK3_SIZE" "$DISK4_SIZE" )
DISK_DEVICES=( "$DEVICE" "$DEVICE2" "$DEVICE3" "$DEVICE4" )
DISK_INDEXES=( "3" "4" "5" "6" )
DISK_ADDRESSES=( "0xc" "0xd" "0xe" "0xf" )
# A passed-through block device takes precedence over the image-file slot
# with the same number.
for i in "${!DISK_FILES[@]}"; do
if [ -n "${DISK_DEVICES[i]}" ]; then
addDevice "${DISK_DEVICES[i]}" "$DISK_TYPE" "${DISK_INDEXES[i]}" "${DISK_ADDRESSES[i]}" || exit $?
else
addDisk "${DISK_FILES[i]}" "$DISK_TYPE" "${DISK_DESCS[i]}" "${DISK_SIZES[i]}" "${DISK_INDEXES[i]}" "${DISK_ADDRESSES[i]}" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" || exit $?
fi
done
finishDisks
return 0
-73
View File
@@ -1,73 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Docker environment variables
: "${GPU:="N"}" # GPU passthrough
: "${VGA:="virtio"}" # VGA adaptor
: "${DISPLAY:="none"}" # Display type
: "${LOSSY:="N"}" # Lossy VNC compression
: "${RENDERNODE:="/dev/dri/renderD128"}" # Render node
# Sanitize variables
VGA=$(strip "$VGA")
LOSSY=$(strip "$LOSSY")
DISPLAY=$(strip "$DISPLAY")
RENDERNODE=$(strip "$RENDERNODE")
CPU_VENDOR=$(lscpu | awk '/Vendor ID/{print $3}')
# The accelerated Intel render-node path is restricted to x86 Intel hosts;
# other platforms retain the normal QEMU display backend.
if ! enabled "$GPU" || isAmdCpu || [[ "$ARCH" != "amd64" ]]; then
# A disabled frontend also removes the emulated VGA device to keep the guest
# hardware layout headless.
[[ "${DISPLAY,,}" == "none" ]] && VGA="none"
if enabled "$LOSSY" && [[ "${DISPLAY,,}" == vnc=* ]]; then
DISPLAY+=",lossy=on"
fi
DISPLAY_OPTS="-display ${DISPLAY} -vga ${VGA}"
return 0
fi
msg="Configuring display drivers..."
html "$msg"
enabled "$DEBUG" && echo "$msg"
DISPLAY_OPTS="-display egl-headless,rendernode=${RENDERNODE}"
DISPLAY_OPTS+=" -vga $VGA"
[ ! -d /dev/dri ] && mkdir -m 755 /dev/dri
# Extract the card number from the render node
# Linux renderD128 corresponds to card0; derive both device minors because
# container device bindings may expose only the render node.
CARD_NUMBER=$(echo "$RENDERNODE" | grep -oP '(?<=renderD)\d+')
CARD_DEVICE="/dev/dri/card$((CARD_NUMBER - 128))"
if [ ! -c "$CARD_DEVICE" ]; then
if mknod "$CARD_DEVICE" c 226 $((CARD_NUMBER - 128)); then
chmod 666 "$CARD_DEVICE"
fi
fi
if [ ! -c "$RENDERNODE" ]; then
if mknod "$RENDERNODE" c 226 "$CARD_NUMBER"; then
chmod 666 "$RENDERNODE"
fi
fi
if [ ! -c "$RENDERNODE" ] || [ ! -r "$RENDERNODE" ] || [ ! -w "$RENDERNODE" ]; then
warn "render device '${RENDERNODE}' is unavailable or inaccessible."
fi
# Install acceleration packages lazily so non-GPU deployments keep the base
# image small and do not require OpenGL modules.
addPackage "xserver-xorg-video-intel" "Intel GPU drivers"
addPackage "qemu-system-modules-opengl" "OpenGL module"
return 0
+15
View File
@@ -5,6 +5,17 @@ set -Eeuo pipefail
: "${APP:="Virtual DSM"}"
: "${SUPPORT:="https://github.com/vdsm/virtual-dsm"}"
: "${USB:="N"}"
: "${AUDIO:="N"}"
: "${VGA:="none"}"
: "${DISPLAY:="none"}"
: "${WEB_PORT:="5000"}"
: "${DISK_OFFSET:="2"}"
: "${DISK_SIZE:="256G"}"
: "${RAM_MINIMUM:="1G"}"
: "${DISK_MINIMUM:="6G"}"
: "${BOOT_MODE:="legacy"}"
cd /run
. start.sh # Startup hook
@@ -12,13 +23,17 @@ cd /run
. init.sh # Initialize system
. memory.sh # Check memory
. server.sh # Start webserver
. download.sh # Load functions
. install.sh # Run installation
. disk.sh # Initialize disks
. display.sh # Initialize graphics
. prepare.sh # Prepare for launch
. network.sh # Initialize network
. boot.sh # Configure boot
. proc.sh # Initialize processor
. serial.sh # Initialize serialport
. power.sh # Configure shutdown
. balloon.sh # Initialize ballooning
. config.sh # Configure arguments
. finish.sh # Finish initialization
+7
View File
@@ -1,7 +1,14 @@
#!/usr/bin/env bash
set -Eeuo pipefail
fKill "progress.sh"
if [ -s "$QEMU_DIR/qemu.host" ] && [[ "$(<"$QEMU_DIR/qemu.host")" == "172.17."* ]]; then
warn "your container IP starts with 172.17.* which will cause conflicts when you install the Container Manager package inside DSM!"
fi
if enabled "$DEBUG"; then
echo
printf "QEMU arguments:\n\n %s\n\n" "${ARGS// -/$'\n -'}"
fi
-296
View File
@@ -1,296 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
trap 'error "Status $? while: $BASH_COMMAND (line $LINENO/$BASH_LINENO)"' ERR
enabled "${TRACE:-}" && set -o functrace && trap 'echo "# $BASH_COMMAND" >&2' DEBUG
[ ! -f "/run/entry.sh" ] && error "Script must be run inside the container!" && exit 11
[ "$(id -u)" -ne "0" ] && error "Script must be executed with root privileges." && exit 12
# Docker environment variables
: "${TZ:=""}" # System timezone
: "${KVM:="Y"}" # KVM acceleration
: "${DEBUG:="N"}" # Disable debugging mode
: "${COUNTRY:=""}" # Country code for mirror
: "${MACHINE:="q35"}" # Machine type selection
: "${ALLOCATE:=""}" # Preallocate diskspace
: "${ARGUMENTS:=""}" # Extra QEMU parameters
: "${CPU_CORES:="2"}" # Amount of CPU cores
: "${RAM_SIZE:="2G"}" # Maximum RAM amount
: "${RAM_CHECK:="Y"}" # Check available RAM
: "${DISK_SIZE:="16G"}" # Initial data disk size
: "${STORAGE:="/storage"}" # Storage folder location
detectEngine() {
if [ -f "/run/.containerenv" ]; then
ENGINE="${container:-}"
if [[ "${ENGINE,,}" == *"podman"* ]]; then
ENGINE="Podman"
else
[ -z "$ENGINE" ] && ENGINE="Kubernetes"
fi
elif [ -f "/.dockerenv" ]; then
ENGINE="Docker"
fi
return 0
}
detectRootless() {
local uid_map
# A full identity UID map indicates a rootful container; any remapping is
# treated as rootless even though the process itself runs as UID 0.
uid_map=$(awk '{$1=$1; print}' /proc/self/uid_map 2>/dev/null || true)
if [[ "$uid_map" == "0 0 4294967295" ]]; then
ROOTLESS="N"
else
ROOTLESS="Y"
fi
return 0
}
checkPrivileged() {
local cap_bnd
local last_cap
# Get the capability bounding set
cap_bnd=$(grep '^CapBnd:' /proc/$$/status | awk '{print $2}')
cap_bnd=$(printf "%d" "0x${cap_bnd}")
# Get the last capability number
last_cap=$(cat /proc/sys/kernel/cap_last_cap)
# Calculate the maximum capability value
# Compare the bounding set with every capability supported by this kernel;
# checking only a few known capabilities would misclassify newer kernels.
local max_cap=$(((1 << (last_cap + 1)) - 1))
if [ "$cap_bnd" -eq "$max_cap" ]; then
PRIVILEGED="Y"
fi
return 0
}
checkCores() {
CPU_CORES=$(strip "$CPU_CORES")
[ -z "$CPU_CORES" ] && CPU_CORES=2
[[ "${CPU_CORES,,}" == "max" ]] && CPU_CORES="$CORES"
[[ "${CPU_CORES,,}" == "half" ]] && CPU_CORES=$(( CORES / 2 ))
[ -z "${CPU_CORES##*[!0-9]*}" ] && error "Invalid amount of CPU_CORES: $CPU_CORES" && exit 15
[ "$CPU_CORES" -lt "1" ] && CPU_CORES=1
if [ "$CPU_CORES" -gt "$CORES" ]; then
warn "The amount for CPU_CORES (${CPU_CORES}) exceeds the amount of logical cores available (${CORES}) and will be limited."
CPU_CORES="$CORES"
fi
return 0
}
checkSockets() {
local lscpu_out
lscpu_out=$(lscpu 2>/dev/null || true)
if grep -qi "socket(s)" <<< "$lscpu_out"; then
SOCKETS=$(grep -m 1 -i 'socket(s)' <<< "$lscpu_out" | awk '{print $2}')
[ -z "${SOCKETS##*[!0-9]*}" ] && SOCKETS=1
[ "$SOCKETS" -lt "1" ] && SOCKETS=1
fi
return 0
}
checkStorage() {
# Check system
QEMU_DIR="/run/shm"
if [ ! -d "/dev/shm" ]; then
error "Directory /dev/shm not found!" && exit 14
else
# Keep runtime sockets and PID files on shared memory even on images where
# /run/shm is absent but /dev/shm is available.
[ ! -d "$QEMU_DIR" ] && ln -s /dev/shm "$QEMU_DIR"
fi
QEMU_PID="$QEMU_DIR/qemu.pid"
# Check folder
if [[ "${STORAGE,,}" != "/storage" ]]; then
if ! mkdir -p -- "$STORAGE"; then
error "Cannot create storage folder ($STORAGE)!" && exit 13
fi
fi
if [ ! -d "$STORAGE" ]; then
error "Storage folder ($STORAGE) not found!" && exit 13
fi
if [ ! -w "$STORAGE" ]; then
msg="Storage folder ($STORAGE) is not writeable!"
msg+=" If SELinux is active, you need to add the \":Z\" flag to the bind mount."
error "$msg" && exit 13
fi
return 0
}
checkHost() {
# Check filesystem
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then
DISK_IO="threads"
DISK_CACHE="writeback"
fi
return 0
}
checkKvm() {
# Check KVM support
if [[ "${PLATFORM,,}" == "x64" ]]; then
TARGET="amd64"
else
TARGET="arm64"
fi
if disabled "$KVM"; then
warn "KVM acceleration is disabled, this will cause the machine to run about 10 times slower!"
else
# KVM accelerates only matching host and guest instruction sets; cross-
# architecture execution must fall back to software emulation.
if [[ "${ARCH,,}" != "$TARGET" ]]; then
KVM="N"
warn "your CPU architecture is ${ARCH^^} and cannot provide KVM acceleration for ${PLATFORM^^} instructions, so the machine will run about 10 times slower."
fi
fi
if ! disabled "$KVM"; then
KVM_ERR=""
if [ ! -e /dev/kvm ]; then
KVM_ERR="(/dev/kvm is missing)"
else
if ! sh -c 'echo -n > /dev/kvm' &> /dev/null; then
KVM_ERR="(/dev/kvm is unwriteable)"
else
if [[ "${PLATFORM,,}" == "x64" ]]; then
flags=$(sed -ne '/^flags/s/^.*: //p' /proc/cpuinfo)
if ! grep -qw "vmx\|svm" <<< "$flags"; then
KVM_ERR="(not enabled in BIOS)"
fi
if ! grep -qw "sse4_2" <<< "$flags"; then
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
enabled "$DEBUG" || exit 88
fi
fi
fi
fi
if [ -n "$KVM_ERR" ]; then
KVM="N"
if [[ "$OSTYPE" =~ ^darwin ]]; then
warn "you are using macOS which has no KVM support, so the machine will run about 10 times slower."
else
kernel=$(uname -a)
case "${kernel,,}" in
*"microsoft"* )
error "Please bind '/dev/kvm' as a volume in the optional container settings when using Docker Desktop." ;;
*"synology"* )
error "Please make sure that Synology VMM (Virtual Machine Manager) is installed and that '/dev/kvm' is binded to this container." ;;
*)
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)." ;;
esac
enabled "$DEBUG" || exit 88
fi
fi
fi
return 0
}
# Sanitize variables
TZ=$(strip "$TZ")
STORAGE=$(strip "$STORAGE")
COUNTRY=$(strip "$COUNTRY")
MACHINE=$(strip "${MACHINE,,}")
DISK_SIZE=$(strip "$DISK_SIZE")
# Helper variables
ROOTLESS="N"
PRIVILEGED="N"
ENGINE="Docker"
PROCESS="${APP,,}"
PROCESS="${PROCESS// /-}"
detectEngine
detectRootless
echo " Starting $APP for $ENGINE v$(</etc/version)..."
echo " For support visit $SUPPORT"
checkPrivileged
INFO="/run/shm/msg.html"
PAGE="/run/shm/index.html"
TEMPLATE="/var/www/index.html"
FOOTER1="$APP for $ENGINE v$(</etc/version)"
FOOTER2="<a href='$SUPPORT'>$SUPPORT</a>"
SOCKETS=1
CPU=$(cpu)
SYS=$(uname -r)
ARCH=$(dpkg --print-architecture)
CORES=$(grep -c '^processor' /proc/cpuinfo)
IFS=. read -r KERNEL MINOR _ <<< "$SYS"
checkSockets
checkCores
checkStorage
getMemoryInfo
# Print system info
SYS="${SYS/-generic/}"
FS=$(stat -f -c %T "$STORAGE")
FS="${FS/UNKNOWN //}"
FS="${FS/ext2\/ext3/ext4}"
FS=$(echo "$FS" | sed 's/[)(]//g')
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
SPACE_GB=$(formatBytes "$SPACE" "down")
AVAIL_MEM=$(formatBytes "$RAM_AVAIL" "down")
TOTAL_MEM=$(formatBytes "$RAM_TOTAL" "up")
echo " CPU: ${CPU} | RAM: ${AVAIL_MEM/ GB/}/$TOTAL_MEM | DISK: $SPACE_GB (${FS}) | KERNEL: ${SYS}"
echo
checkHost
checkKvm
# Runtime state is intentionally discarded at each container start; persistent
# machine and disk identity lives under STORAGE instead.
# Cleanup files
rm -f "$QEMU_DIR"/dsm.url
rm -f "$QEMU_DIR"/{qemu.*,*.{pid,sock,pipe}}
# Cleanup dirs
rm -rf /tmp/dsm
rm -rf "$STORAGE/tmp"
return 0
+610 -321
View File
@@ -1,373 +1,662 @@
#!/usr/bin/env bash
set -Eeuo pipefail
: "${URL:=""}" # URL of the PAT file to be downloaded.
: "${TZ:=""}" # System timezone
: "${COUNTRY:=""}" # Country code for mirror
: "${URL:=""}" # URL of the PAT file to be downloaded.
# Persist the exact PAT base name so future starts reopen the matching boot,
# system, and cached installation files.
if [ -f "$STORAGE/dsm.ver" ]; then
BASE=$(<"$STORAGE/dsm.ver")
BASE="${BASE//[![:print:]]/}"
[ -z "$BASE" ] && BASE="DSM_VirtualDSM_69057"
else
# Fallback for old installs
BASE="DSM_VirtualDSM_42962"
fi
TZ=$(strip "$TZ")
COUNTRY=$(strip "$COUNTRY")
FN="boot.pat"
DIR=$(find / -maxdepth 1 -type d -iname "$FN" -print -quit)
[ ! -d "$DIR" ] && DIR=$(find "$STORAGE" -maxdepth 1 -type d -iname "$FN" -print -quit)
checkDsmFilesystem() {
# A boot.pat directory bind represents already extracted boot and system
# images and therefore takes precedence over PAT file or URL discovery.
if [ -d "$DIR" ]; then
BASE="DSM_VirtualDSM" && URL="file://$DIR"
if [[ ! -s "$STORAGE/$BASE.boot.img" || ! -s "$STORAGE/$BASE.system.img" ]]; then
error "The bind $DIR maps to a file that does not exist!" && exit 65
fi
fi
local fs="$1"
FILE=$(find / -maxdepth 1 -type f -iname "$FN" -print -quit)
[ ! -s "$FILE" ] && FILE=$(find "$STORAGE" -maxdepth 1 -type f -iname "$FN" -print -quit)
[ -s "$FILE" ] && BASE="DSM_VirtualDSM" && URL="file://$FILE"
URL=$(strip "$URL")
# Derive a filesystem-safe identity from the URL only when no local boot.pat
# source was supplied; preserve an existing system image identity if present.
if [ -n "$URL" ] && [ ! -s "$FILE" ] && [ ! -d "$DIR" ]; then
BASE=$(basename "$URL" .pat)
if [ ! -s "$STORAGE/$BASE.system.img" ]; then
BASE=$(basename "${URL%%\?*}" .pat)
printf -v BASE '%b' "${BASE//%/\\x}"
BASE="${BASE//[!A-Za-z0-9._-]/_}"
fi
if [[ "${URL,,}" != "http"* && "${URL,,}" != "file:"* ]]; then
[ ! -s "$STORAGE/$BASE.pat" ] && error "Invalid URL: $URL" && exit 65
URL="file://$STORAGE/$BASE.pat"
fi
fi
# A complete matching image pair is the installation marker; the cached PAT
# itself is optional after installation.
if [[ -s "$STORAGE/$BASE.boot.img" && -s "$STORAGE/$BASE.system.img" ]]; then
return 0 # Previous installation found
fi
html "Please wait while Virtual DSM is being installed..."
DL=""
DL_CHINA="https://cndl.synology.cn/download/DSM"
DL_GLOBAL="https://global.synologydownload.com/download/DSM"
[[ "${URL,,}" == *"cndl.synology"* ]] && DL="$DL_CHINA"
[[ "${URL,,}" == *"global.synology"* ]] && DL="$DL_GLOBAL"
# Honor an explicitly selected Synology mirror first, otherwise choose the
# China or global endpoint from the detected country.
if [ -z "$DL" ]; then
[ -z "$COUNTRY" ] && setCountry
[ -z "$COUNTRY" ] && info "Warning: could not detect country to select mirror!"
[[ "${COUNTRY^^}" == "CN" ]] && DL="$DL_CHINA" || DL="$DL_GLOBAL"
fi
if [ -z "$URL" ]; then
URL="$DL/release/7.2.2/72806/DSM_VirtualDSM_72806.pat"
fi
if [ ! -s "$FILE" ]; then
BASE=$(basename "${URL%%\?*}" .pat)
printf -v BASE '%b' "${BASE//%/\\x}"
BASE="${BASE//[!A-Za-z0-9._-]/_}"
fi
if [[ "$URL" != "file://$STORAGE/$BASE.pat" ]]; then
rm -f "$STORAGE/$BASE.pat"
fi
rm -f "$STORAGE/$BASE.agent"
rm -f "$STORAGE/$BASE.boot.img"
rm -f "$STORAGE/$BASE.system.img"
# Check filesystem
FS=$(stat -f -c %T "$STORAGE")
if [[ "${FS,,}" == "overlay"* && "${ENGINE,,}" == "docker" ]]; then
warn "the filesystem of $STORAGE is OverlayFS, this usually means it was binded to an invalid path!"
fi
if [[ "${FS,,}" == "fuse"* ]]; then
warn "the filesystem of $STORAGE is FUSE, this extra layer will negatively affect performance!"
fi
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then
warn "the filesystem of $STORAGE is $FS, which does not support O_DIRECT mode, adjusting settings..."
fi
if [[ "${FS,,}" == "fat"* || "${FS,,}" == "vfat"* || "${FS,,}" == "msdos"* ]]; then
error "Unable to install on $FS filesystems, please use a different filesystem for /storage." && exit 61
fi
# Extract beside storage on Unix filesystems to avoid container-space limits;
# use /tmp for filesystems that cannot safely host the installer workspace.
if [[ "${FS,,}" != "exfat"* && "${FS,,}" != "ntfs"* && "${FS,,}" != "unknown"* ]]; then
TMP="$STORAGE/tmp"
rm -rf "$TMP"
if ! makeDir "$TMP"; then
error "Failed to create directory \"$TMP\" !" && exit 93
fi
else
TMP="/tmp/dsm"
TMP_SPACE=2147483648
SPACE=$(df --output=avail -B 1 /tmp | tail -n 1)
SPACE_MB=$(formatBytes "$SPACE")
if (( TMP_SPACE > SPACE )); then
error "Not enough free space inside the container, have $SPACE_MB available but need at least 2 GB." && exit 93
fi
rm -rf "$TMP" && mkdir -p "$TMP"
fi
# Check free diskspace
ROOT_SPACE=536870912
SPACE=$(df --output=avail -B 1 / | tail -n 1)
SPACE_MB=$(formatBytes "$SPACE" "down")
(( ROOT_SPACE > SPACE )) && error "Not enough free space inside the container, have $SPACE_MB available but need at least 500 MB." && exit 96
MIN_SPACE=15032385536
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
SPACE_GB=$(formatBytes "$SPACE")
(( MIN_SPACE > SPACE )) && error "Not enough free space for installation in $STORAGE, have $SPACE_GB available but need at least 14 GB." && exit 94
if [[ "$URL" == "file://"* ]]; then
MSG="Copying DSM"
ERR="Failed to copy ${URL:7}"
info "Install: Copying installation image..."
else
MSG="Downloading DSM"
ERR="Failed to download $URL"
info "Install: Downloading $BASE.pat..."
fi
html "$MSG..."
PAT="/$BASE.pat"
rm -f "$PAT"
if [[ "$URL" == "file://"* ]]; then
if [ ! -f "${URL:7}" ]; then
error "File '${URL:7}' does not exist!" && exit 65
if [[ "${fs,,}" == "overlay"* && "${ENGINE,,}" == "docker" ]]; then
warn "the filesystem of $STORAGE is OverlayFS, this usually means it was binded to an invalid path!"
fi
cp "${URL:7}" "$PAT"
if [[ "${fs,,}" == "fuse"* ]]; then
warn "the filesystem of $STORAGE is FUSE, this extra layer will negatively affect performance!"
fi
else
if [[ "${fs,,}" == "ecryptfs" || "${fs,,}" == "tmpfs" ]]; then
warn "the filesystem of $STORAGE is $fs, which does not support O_DIRECT mode, adjusting settings..."
fi
SIZE=0
REASON=""
PROGRESS=()
OUTPUT=""
LOG=$(mktemp)
if [[ "${fs,,}" == "fat"* || "${fs,,}" == "vfat"* || "${fs,,}" == "msdos"* ]]; then
error "Unable to install on $fs filesystems, please use a different filesystem for /storage."
return 61
fi
[[ "${URL,,}" == *"_72806.pat" ]] && SIZE=361010261
[[ "${URL,,}" == *"_69057.pat" ]] && SIZE=363837333
[[ "${URL,,}" == *"_42218.pat" ]] && SIZE=379637760
return 0
}
checkDsmSpace() {
local rootSpace=536870912
local minSpace=15032385536
local space available
space=$(df --output=avail -B 1 / | tail -n 1) || return $?
available=$(formatBytes "$space" "down") || return $?
if (( rootSpace > space )); then
error "Not enough free space inside the container, have $available available but need at least 500 MB."
return 96
fi
space=$(df --output=avail -B 1 "$STORAGE" | tail -n 1) || return $?
available=$(formatBytes "$space") || return $?
if (( minSpace > space )); then
error "Not enough free space for installation in $STORAGE, have $available available but need at least 14 GB."
return 94
fi
return 0
}
downloadFile() {
local url="$1"
local pat="$2"
local msg="$3"
local connections="${4:-1}"
downloadToFile \
"$url" \
"$pat" \
"$msg" \
"0" \
"$connections" \
"Y"
}
downloadPat() {
local pat="$1"
local msg err
if [[ "$URL" == "file://"* ]]; then
msg="Copying DSM"
err="Failed to copy ${URL:7}"
info "Install: Copying installation image..."
html "$msg..." || return $?
rm -f "$pat" || return $?
if [ ! -f "${URL:7}" ]; then
error "File '${URL:7}' does not exist!"
return 65
fi
cp "${URL:7}" "$pat" || return $?
# Use Wget's progress bar in a terminal and progress.sh in container logs.
if [ -t 1 ]; then
PROGRESS=( --show-progress --progress=bar:noscroll )
else
OUTPUT="log"
msg="Downloading DSM"
err="Failed to download $URL"
info "Install: Downloading $BASE.pat..."
downloadRetry \
"$pat" \
"$CONNECTIONS" \
"5" \
"$BASE.pat" \
"0" \
"$URL" \
"$pat" \
"$msg" || return 69
fi
/run/progress.sh "$PAT" "$SIZE" "$MSG ([P])..." "$OUTPUT" 52428800 &
if [ ! -s "$pat" ]; then
error "$err"
return 69
fi
{
LC_ALL=C wget "$URL" -O "$PAT" --no-verbose --no-check-certificate \
--timeout=30 --no-http-keep-alive "${PROGRESS[@]}" \
--output-file="$LOG"
rc=$?
} || :
return 0
}
extractPat() {
local pat="$1"
local tmp="$2"
local size="$3"
local msg="Extracting installation image..."
local rc
info "Install: $msg" && html "$msg" || return $?
/run/progress.sh "$tmp" "$size" "$msg ([P])..." &
# Newer PAT files are normal tar archives; older encrypted/proprietary forms
# require the bundled extractor as a compatibility fallback.
if { tar tf "$pat"; } >/dev/null 2>&1; then
tar xpf "$pat" -C "$tmp/." || {
rc=$?
fKill "progress.sh"
return "$rc"
}
else
{ (cd "$tmp" && python3 /run/extract.py -i "$pat" -d 2>/run/extract.log); rc=$?; } || :
if (( rc != 0 )); then
fKill "progress.sh"
cat /run/extract.log
error "Failed to extract PAT file, reason $rc"
return 63
fi
fi
fKill "progress.sh"
return 0
}
if (( rc != 0 )); then
REASON=$(sed -n \
-e 's/^wget: //p' \
-e 's/^[0-9-]\{10\} [0-9:]\{8\} ERROR //p' \
"$LOG" | tail -n 1)
createSystemImage() {
local tmp="$1"
local fs="$2"
local msg="Preparing system partition..."
info "Install: $msg" && html "$msg" || return $?
# The PAT boot archive becomes the persistent QEMU boot disk after its
# companion system partition has been assembled.
DSM_BOOT=$(find "$tmp" -name "*.bin.zip" -print -quit) || return $?
if [ -z "$DSM_BOOT" ]; then
error "The PAT file contains no boot image."
return 67
fi
rm -f "$LOG"
if (( rc == 3 )); then
error "$ERR because the file could not be written (disk full?)."
exit 69
elif (( rc != 0 )); then
if [ -n "$REASON" ]; then
error "$ERR: ${REASON%.}."
else
error "$ERR with exit status $rc."
fi
exit 69
if [ ! -s "$DSM_BOOT" ]; then
error "The PAT boot image archive is empty."
return 67
fi
fi
local bootTmp="$tmp/boot" bootBase
bootBase="$(basename "${DSM_BOOT%.zip}")"
[ ! -s "$PAT" ] && error "$ERR" && exit 69
rm -rf "$bootTmp" || return $?
SIZE=$(stat -c%s "$PAT")
# Full Virtual DSM PAT files are substantially larger than update packs;
# reject undersized inputs before attempting destructive image preparation.
if ((SIZE<250000000)); then
error "The specified PAT file is probably an update pack as it's too small." && exit 62
fi
MSG="Extracting installation image..."
info "Install: $MSG" && html "$MSG"
# Newer PAT files are normal tar archives; older encrypted/proprietary forms
# require the bundled extractor as a compatibility fallback.
if { tar tf "$PAT"; } >/dev/null 2>&1; then
tar xpf "$PAT" -C "$TMP/."
else
{ (cd "$TMP" && python3 /run/extract.py -i "$PAT" -d 2>/run/extract.log); rc=$?; } || :
if (( rc != 0 )); then
cat /run/extract.log
error "Failed to extract PAT file, reason $rc" && exit 63
if ! makeDir "$bootTmp"; then
error "Failed to create directory \"$bootTmp\" !"
return 93
fi
fi
MSG="Preparing system partition..."
info "Install: $MSG" && html "$MSG"
# The PAT boot archive becomes the persistent QEMU boot disk after its
# companion system partition has been assembled.
BOOT=$(find "$TMP" -name "*.bin.zip" -print -quit)
[ -z "$BOOT" ] && error "The PAT file contains no boot image." && exit 67
[ ! -s "$BOOT" ] && error "The PAT boot image archive is empty." && exit 67
unzip -q -o "$BOOT" -d "$TMP"
BOOT="${BOOT%.zip}"
SYSTEM="$STORAGE/$BASE.system.img"
rm -f "$SYSTEM"
# Check free diskspace
SYSTEM_SIZE=10738466816
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
SPACE_MB=$(formatBytes "$SPACE")
if (( SYSTEM_SIZE > SPACE )); then
error "Not enough free space in $STORAGE to create a 10 GB system disk, have only $SPACE_MB available." && exit 97
fi
if ! touch "$SYSTEM"; then
error "Could not create file $SYSTEM for the system disk." && exit 98
fi
setOwner "$SYSTEM" || warn "failed to set the owner for \"$SYSTEM\" !"
if [[ "${FS,,}" == "btrfs" ]]; then
{ chattr +C "$SYSTEM"; } || :
FA=$(lsattr "$SYSTEM")
if [[ "$FA" != *"C"* ]]; then
error "Failed to disable COW for system image $SYSTEM on ${FS^^} filesystem."
if [[ "${fs,,}" == "btrfs" ]]; then
{ chattr +C "$bootTmp"; } || :
fi
fi
if ! fallocate -l "$SYSTEM_SIZE" "$SYSTEM" &>/dev/null; then
if ! fallocate -l -x "$SYSTEM_SIZE" "$SYSTEM"; then
if ! truncate -s "$SYSTEM_SIZE" "$SYSTEM"; then
rm -f "$SYSTEM"
error "Could not allocate file $SYSTEM for the system disk." && exit 98
7z x -y -o"$bootTmp" "$DSM_BOOT" >/dev/null || return $?
DSM_BOOT="$bootTmp/$bootBase"
if [ ! -s "$DSM_BOOT" ]; then
error "The PAT boot image could not be extracted."
return 67
fi
if [[ "${fs,,}" == "btrfs" ]]; then
local attrs
attrs=$(lsattr "$DSM_BOOT") || return $?
if [[ "$attrs" != *"C"* ]]; then
error "Failed to disable COW for boot image $DSM_BOOT on ${fs^^} filesystem."
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"
SYSTEM="$STORAGE/$BASE.system.img"
rm -f "$SYSTEM" || return $?
{
echo "label: dos"
echo "label-id: 0x6f9ee2e9"
echo "device: $SYSTEM"
echo "unit: sectors"
echo "sector-size: 512"
echo ""
echo "${SYSTEM}1 : start= 2048, size= 16777216, type=83"
echo "${SYSTEM}2 : start= 16779264, size= 4194304, type=82"
} > "$PART"
# Check free diskspace
local systemSize=10738466816
local space available
sfdisk -q "$SYSTEM" < "$PART"
space=$(df --output=avail -B 1 "$STORAGE" | tail -n 1) || return $?
available=$(formatBytes "$space") || return $?
MOUNT="$TMP/system"
rm -rf "$MOUNT"
if (( systemSize > space )); then
error "Not enough free space in $STORAGE to create a 10 GB system disk, have only $available available."
return 97
fi
if ! makeDir "$MOUNT"; then
error "Failed to create directory \"$MOUNT\" !" && exit 93
fi
if ! touch "$SYSTEM"; then
error "Could not create file $SYSTEM for the system disk."
return 98
fi
MSG="Extracting system partition..."
info "Install: $MSG" && html "$MSG"
setOwner "$SYSTEM" || warn "failed to set the owner for \"$SYSTEM\" !"
HDA="$TMP/hda1"
IDB="$TMP/indexdb"
PKG="$TMP/packages"
HDP="$TMP/synohdpack_img"
if [[ "${fs,,}" == "btrfs" ]]; then
{ chattr +C "$SYSTEM"; } || :
local attrs
attrs=$(lsattr "$SYSTEM") || return $?
if [[ "$attrs" != *"C"* ]]; then
error "Failed to disable COW for system image $SYSTEM on ${fs^^} filesystem."
fi
fi
[ ! -s "$HDA.tgz" ] && error "The PAT file contains no OS image." && exit 64
mv "$HDA.tgz" "$HDA.txz"
if ! fallocate -l "$systemSize" "$SYSTEM" &>/dev/null; then
if ! fallocate -l -x "$systemSize" "$SYSTEM"; then
if ! truncate -s "$systemSize" "$SYSTEM"; then
rm -f "$SYSTEM"
error "Could not allocate file $SYSTEM for the system disk."
return 98
fi
fi
fi
[ -d "$PKG" ] && mv "$PKG/" "$MOUNT/.SynoUpgradePackages/"
rm -f "$MOUNT/.SynoUpgradePackages/ActiveInsight-"*
# Recreate Synology's expected DOS partition layout inside the fixed 10 GiB
# system image before populating the ext4 root partition.
local part="$tmp/partition.fdisk"
INDEX_DB="$MOUNT/usr/syno/synoman/indexdb"
if ! {
echo "label: dos"
echo "label-id: 0x6f9ee2e9"
echo "device: $SYSTEM"
echo "unit: sectors"
echo "sector-size: 512"
echo ""
echo "${SYSTEM}1 : start= 2048, size= 16777216, type=83"
echo "${SYSTEM}2 : start= 16779264, size= 4194304, type=82"
} > "$part"; then
return 1
fi
if [ -s "$IDB.txz" ]; then
mkdir -p "$INDEX_DB"
fi
sfdisk -q "$SYSTEM" < "$part" || return $?
LABEL="1.44.1-42218"
OFFSET="1048576" # 2048 * 512
NUMBLOCKS="2097152" # (16777216 * 512) / 4096
MSG="Installing system partition..."
local mount="$tmp/system"
rm -rf "$mount" || return $?
# Build the ext4 filesystem directly from the extracted tree under fakeroot,
# preserving archive ownership without mounting a loop device.
fakeroot -- bash -c "set -Eeu;\
[ -s $HDP.txz ] && tar xpfJ $HDP.txz --absolute-names -C $MOUNT/;\
[ -s $IDB.txz ] && tar xpfJ $IDB.txz --absolute-names -C $INDEX_DB/;\
tar xpfJ $HDA.txz --absolute-names --skip-old-files -C $MOUNT/;\
printf '%b%s%b' '\E[1;34m \E[1;36m' 'Install: $MSG' '\E[0m\n';\
mke2fs -q -t ext4 -b 4096 -d $MOUNT/ -L $LABEL -F -E offset=$OFFSET $SYSTEM $NUMBLOCKS"
if ! makeDir "$mount"; then
error "Failed to create directory \"$mount\" !"
return 93
fi
rm -rf "$MOUNT"
echo "$BASE" > "$STORAGE/dsm.ver"
setOwner "$STORAGE/dsm.ver" || warn "failed to set the owner for \"$STORAGE/dsm.ver\" !"
return 0
}
# Do not keep a second copy when the source PAT already lives in storage;
# downloaded or externally mounted sources are cached for later reuse.
if [[ "$URL" == "file://$STORAGE/$BASE.pat" ]]; then
rm -f "$PAT"
else
mv -f "$PAT" "$STORAGE/$BASE.pat"
fi
installSystemPartition() {
if [ -f "$STORAGE/$BASE.pat" ]; then
setOwner "$STORAGE/$BASE.pat" || warn "failed to set the owner for \"$STORAGE/$BASE.pat\" !"
fi
local tmp="$1"
local mount="$tmp/system"
local msg="Extracting system partition..."
mv -f "$BOOT" "$STORAGE/$BASE.boot.img"
setOwner "$STORAGE/$BASE.boot.img" || warn "failed to set the owner for \"$STORAGE/$BASE.boot.img\" !"
info "Install: $msg" && html "$msg" || return $?
rm -rf "$TMP"
local hda="$tmp/hda1"
local idb="$tmp/indexdb"
local pkg="$tmp/packages"
local hdp="$tmp/synohdpack_img"
if [ ! -s "$hda.tgz" ]; then
error "The PAT file contains no OS image."
return 64
fi
mv "$hda.tgz" "$hda.txz" || return $?
if [ -d "$pkg" ]; then
mv "$pkg/" "$mount/.SynoUpgradePackages/" || return $?
fi
rm -f "$mount/.SynoUpgradePackages/ActiveInsight-"* || return $?
local indexDb="$mount/usr/syno/synoman/indexdb"
if [ -s "$idb.txz" ]; then
mkdir -p "$indexDb" || return $?
fi
local label="1.44.1-42218"
local offset="1048576" # 2048 * 512
local numBlocks="2097152" # (16777216 * 512) / 4096
local rc
msg="Installing system partition..."
# Build the ext4 filesystem directly from the extracted tree under fakeroot,
# preserving archive ownership without mounting a loop device.
if fakeroot -- bash -c "set -Eeu;\
[ -s $hdp.txz ] && tar xpfJ $hdp.txz --absolute-names -C $mount/;\
[ -s $idb.txz ] && tar xpfJ $idb.txz --absolute-names -C $indexDb/;\
tar xpfJ $hda.txz --absolute-names --skip-old-files -C $mount/;\
printf '%b%s%b' '\E[1;34m \E[1;36m' 'Install: $msg' '\E[0m\n';\
mke2fs -q -t ext4 -b 4096 -d $mount/ -L $label -F -E offset=$offset $SYSTEM $numBlocks"; then
:
else
rc=$?
return "$rc"
fi
rm -rf "$mount" || return $?
return 0
}
checkSse42() {
if disabled "$KVM" || [[ "${PLATFORM,,}" != "x64" ]]; then
return 0
fi
if ! hasFlag "sse4_2"; then
error "Your CPU does not have the SSE4.2 instruction set that Virtual DSM requires!"
enabled "$DEBUG" || return 88
fi
return 0
}
sanitizePatBase() {
local source="$1"
local base
base=$(basename "${source%%\?*}" .pat) || return 1
printf -v base '%b' "${base//%/\\x}" || return 1
base="${base//[!A-Za-z0-9._-]/_}"
printf '%s' "$base"
}
reservePorts() {
local port ports=""
local hostPorts="${HOST_PORTS:-}"
# Older configurations may reserve DSM's web ports for the container.
# They now need to be forwarded to the guest instead.
for port in ${hostPorts//,/ }; do
case "${port,,}" in
5000|5000/tcp|5001|5001/tcp)
continue ;;
esac
ports+="${ports:+,}$port"
done
HOST_PORTS="$ports"
# NAT can fall back to user-mode networking after this point,
# so always add the DSM web interface ports for non-DHCP networking.
USER_PORTS="${USER_PORTS:+$USER_PORTS,}5000/tcp,5001/tcp,80/tcp,443/tcp,445/tcp"
return 0
}
prepareMac() {
local file="$STORAGE/$PROCESS.mac"
local legacy="$STORAGE/dsm.mac"
local container mac=""
# An explicitly configured MAC remains network.sh's responsibility.
[ -n "${MAC:-}" ] && return 0
[ -s "$file" ] && return 0
# Preserve the address generated by older Virtual DSM releases.
if [ -s "$legacy" ]; then
mac=$(readFile "$legacy") || return $?
fi
if [ -z "$mac" ]; then
container=$(containerID) || return $?
mac=$(echo "$container" | md5sum |
sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:11:32:\3:\4:\5/') || return $?
fi
if ! writeState "mac" "${mac^^}"; then
error "Failed to write MAC address to \"$file\" !"
return 28
fi
return 0
}
getCountry() {
local url=$1
local query=$2
local json result
{ json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); local rc=$?; } || :
(( rc != 0 )) && return 0
{ result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || :
(( rc != 0 )) && return 0
[[ ${#result} -ne 2 ]] && return 0
[[ "${result^^}" == "XX" ]] && return 0
COUNTRY="${result^^}"
return 0
}
setCountry() {
[[ "${TZ,,}" == "asia/harbin" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/beijing" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/urumqi" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/kashgar" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/shanghai" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/chongqing" ]] && COUNTRY="CN"
# Country detection is best-effort and tries independent services in order;
# failure leaves mirror selection at its global default.
[ -z "$COUNTRY" ] && getCountry "https://api.ipapi.is" ".location.country_code"
[ -z "$COUNTRY" ] && getCountry "https://ifconfig.co/json" ".country_iso"
[ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code"
[ -z "$COUNTRY" ] && getCountry "https://ipinfo.io/json" ".country"
[ -z "$COUNTRY" ] && getCountry "https://api.ipquery.io/?format=json" ".location.country_code"
[ -z "$COUNTRY" ] && getCountry "https://api.myip.com" ".cc"
return 0
}
finishDsmInstall() {
local pat="$1"
local tmp="$2"
echo "$BASE" > "$STORAGE/dsm.ver" || return 1
setOwner "$STORAGE/dsm.ver" || warn "failed to set the owner for \"$STORAGE/dsm.ver\" !"
# Do not keep a second copy when the source PAT already lives in storage;
# downloaded or externally mounted sources are cached for later reuse.
if [[ "$URL" == "file://$STORAGE/$BASE.pat" ]]; then
rm -f "$pat" || return $?
else
mv -f "$pat" "$STORAGE/$BASE.pat" || return $?
fi
if [ -f "$STORAGE/$BASE.pat" ]; then
setOwner "$STORAGE/$BASE.pat" || warn "failed to set the owner for \"$STORAGE/$BASE.pat\" !"
fi
mv -f "$DSM_BOOT" "$STORAGE/$BASE.boot.img" || return $?
setOwner "$STORAGE/$BASE.boot.img" || warn "failed to set the owner for \"$STORAGE/$BASE.boot.img\" !"
rm -rf "$tmp" || return $?
return 0
}
installDSM() {
rm -rf /tmp/dsm || return $?
rm -f "$QEMU_DIR/dsm.url" || return $?
checkSse42 || return $?
reservePorts || return $?
prepareMac || return $?
local patName="boot.pat"
local patDir patFile
# Persist the exact PAT base name so future starts reopen the matching boot,
# system, and cached installation files.
if [ -f "$STORAGE/dsm.ver" ]; then
BASE=$(<"$STORAGE/dsm.ver") || return $?
BASE="${BASE//[![:print:]]/}"
[ -z "$BASE" ] && BASE="DSM_VirtualDSM_69057"
else
# Fallback for old installs
BASE="DSM_VirtualDSM_42962"
fi
patDir=$(find / -maxdepth 1 -type d -iname "$patName" -print -quit) || return $?
if [ ! -d "$patDir" ]; then
patDir=$(find "$STORAGE" -maxdepth 1 -type d -iname "$patName" -print -quit) || return $?
fi
# A boot.pat directory bind represents already extracted boot and system
# images and therefore takes precedence over PAT file or URL discovery.
if [ -d "$patDir" ]; then
BASE="DSM_VirtualDSM"
URL="file://$patDir"
if [[ ! -s "$STORAGE/$BASE.boot.img" || ! -s "$STORAGE/$BASE.system.img" ]]; then
error "The bind $patDir maps to a file that does not exist!"
return 65
fi
fi
patFile=$(find / -maxdepth 1 -type f -iname "$patName" -print -quit) || return $?
if [ ! -s "$patFile" ]; then
patFile=$(find "$STORAGE" -maxdepth 1 -type f -iname "$patName" -print -quit) || return $?
fi
if [ -s "$patFile" ]; then
BASE="DSM_VirtualDSM"
URL="file://$patFile"
fi
URL=$(strip "$URL") || return $?
# 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 "$patFile" ] && [ ! -d "$patDir" ]; then
BASE=$(basename "$URL" .pat) || return $?
if [ ! -s "$STORAGE/$BASE.system.img" ]; then
BASE=$(sanitizePatBase "$URL") || return $?
fi
if [[ "${URL,,}" != "http"* && "${URL,,}" != "file:"* ]]; then
if [ ! -s "$STORAGE/$BASE.pat" ]; then
error "Invalid URL: $URL"
return 65
fi
URL="file://$STORAGE/$BASE.pat"
fi
fi
# A complete matching image pair is the installation marker; the cached PAT
# itself is optional after installation.
if [[ -s "$STORAGE/$BASE.boot.img" && -s "$STORAGE/$BASE.system.img" ]]; then
return 0 # Previous installation found
fi
html "Please wait while Virtual DSM is being installed..." || return $?
local mirror=""
local chinaMirror="https://cndl.synology.cn/download/DSM"
local globalMirror="https://global.synologydownload.com/download/DSM"
[[ "${URL,,}" == *"cndl.synology"* ]] && mirror="$chinaMirror"
[[ "${URL,,}" == *"global.synology"* ]] && mirror="$globalMirror"
# Honor an explicitly selected Synology mirror first, otherwise choose the
# China or global endpoint from the detected country.
if [ -z "$mirror" ]; then
if [ -z "$COUNTRY" ]; then
setCountry || return $?
fi
[ -z "$COUNTRY" ] && info "Warning: could not detect country to select mirror!"
[[ "${COUNTRY^^}" == "CN" ]] && mirror="$chinaMirror" || mirror="$globalMirror"
fi
if [ -z "$URL" ]; then
URL="$mirror/release/7.2.2/72806/DSM_VirtualDSM_72806.pat"
fi
if [ ! -s "$patFile" ]; then
BASE=$(sanitizePatBase "$URL") || return $?
fi
if [[ "$URL" != "file://$STORAGE/$BASE.pat" ]]; then
rm -f "$STORAGE/$BASE.pat" || return $?
fi
rm -f "$STORAGE/$BASE.agent" || return $?
rm -f "$STORAGE/$BASE.boot.img" || return $?
rm -f "$STORAGE/$BASE.system.img" || return $?
# Check filesystem
local fs
fs=$(stat -f -c %T "$STORAGE") || return $?
checkDsmFilesystem "$fs" || return $?
local tmp
# Extract beside storage on Unix filesystems to avoid container-space limits;
# use /tmp for filesystems that cannot safely host the installer workspace.
if [[ "${fs,,}" != "exfat"* && "${fs,,}" != "ntfs"* && "${fs,,}" != "unknown"* ]]; then
tmp="$STORAGE/tmp"
rm -rf "$tmp" || return $?
if ! makeDir "$tmp"; then
error "Failed to create directory \"$tmp\" !"
return 93
fi
else
tmp="/tmp/dsm"
local tmpSpace=2147483648
local space available
space=$(df --output=avail -B 1 /tmp | tail -n 1) || return $?
available=$(formatBytes "$space") || return $?
if (( tmpSpace > space )); then
error "Not enough free space inside the container, have $available available but need at least 2 GB."
return 93
fi
rm -rf "$tmp" || return $?
mkdir -p "$tmp" || return $?
fi
# Check free diskspace
checkDsmSpace || return $?
local pat="/$BASE.pat"
downloadPat "$pat" || return $?
local size
size=$(stat -c%s "$pat") || return $?
# Full Virtual DSM PAT files are substantially larger than update packs;
# reject undersized inputs before attempting destructive image preparation.
if (( size < 250000000 )); then
error "The specified PAT file is probably an update pack as it's too small."
return 62
fi
extractPat "$pat" "$tmp" "$size" || return $?
createSystemImage "$tmp" "$fs" || return $?
installSystemPartition "$tmp" || return $?
finishDsmInstall "$pat" "$tmp" || return $?
return 0
}
installDSM || exit $?
return 0
-235
View File
@@ -1,235 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
normalizeMemory() {
local wanted
RAM_SPARE=500000000
RAM_MINIMUM="${RAM_MINIMUM:-1073741824}"
RAM_MINIMUM=$(strip "$RAM_MINIMUM")
RAM_MINIMUM="${RAM_MINIMUM// /}"
RAM_MINIMUM=$(echo "${RAM_MINIMUM^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
numfmt --from=iec "$RAM_MINIMUM" &>/dev/null || {
error "Invalid RAM_MINIMUM: $RAM_MINIMUM"
exit 16
}
RAM_MINIMUM=$(numfmt --from=iec "$RAM_MINIMUM")
RAM_SIZE=$(strip "$RAM_SIZE")
RAM_SIZE="${RAM_SIZE// /}"
[ -z "$RAM_SIZE" ] && RAM_SIZE="2G"
if [[ "${RAM_SIZE,,}" != "max" && "${RAM_SIZE,,}" != "half" ]]; then
# Bare values below 130 are interpreted as GiB for convenience; larger bare
# values are treated as MiB to preserve historical configurations.
if [ -z "${RAM_SIZE//[0-9. ]}" ]; then
[ "${RAM_SIZE%%.*}" -lt "130" ] && RAM_SIZE="${RAM_SIZE}G" || RAM_SIZE="${RAM_SIZE}M"
fi
RAM_SIZE=$(echo "${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
numfmt --from=iec "$RAM_SIZE" &>/dev/null || {
error "Invalid RAM_SIZE: $RAM_SIZE"
exit 16
}
wanted=$(numfmt --from=iec "$RAM_SIZE")
if [ "$wanted" -lt "$RAM_MINIMUM" ]; then
error "$(app) requires at least $(formatBytes "$RAM_MINIMUM") of RAM, but RAM_SIZE is set to $(formatBytes "$wanted")."
exit 16
fi
# QEMU requires a whole-number memory value, so convert decimal sizes to MiB.
if [[ "$RAM_SIZE" == *.* ]]; then
RAM_SIZE="$(( wanted / 1048576 ))M"
fi
fi
return 0
}
checkConfiguredMemory() {
local final="$1"
if disabled "$RAM_CHECK" || [[ "${RAM_SIZE,,}" == "max" || "${RAM_SIZE,,}" == "half" ]]; then
return 0
fi
local wanted avail_mem
wanted=$(numfmt --from=iec "$RAM_SIZE")
avail_mem=$(formatBytes "$RAM_AVAIL")
if (( (wanted + RAM_SPARE) > RAM_AVAIL )); then
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
enabled "$final" && info "$msg but since ZFS is active this will be ignored."
else
RAM_SIZE="max"
RAM_WARNING="$msg it will automatically be adjusted to a lower amount."
fi
else
if (( (wanted + (RAM_SPARE * 3)) > RAM_AVAIL )); then
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
enabled "$final" && info "$msg but since ZFS is active this will be ignored."
else
enabled "$final" && warn "$msg please consider a lower amount."
fi
fi
fi
return 0
}
configureHalfMemory() {
if [[ "${RAM_SIZE,,}" != "half" ]]; then
return 0
fi
if (( (RAM_AVAIL / 2) > RAM_SPARE )); then
local wanted=$(( RAM_AVAIL / 2 ))
# Divide by one byte more than a MiB to round down
local target=$(( wanted / 1048577 ))
RAM_SIZE="${target}M"
RAM_ALLOCATION="$wanted"
else
RAM_SIZE="max"
fi
return 0
}
configureMaxMemory() {
if [[ "${RAM_SIZE,,}" != "max" ]]; then
return 0
fi
# max keeps a host reserve when possible, but on very small systems falls back
# to half the available memory to avoid starving the container.
if (( RAM_AVAIL < (RAM_SPARE * 2) )); then
local wanted=$(( RAM_AVAIL / 2 ))
else
local wanted=$(( RAM_AVAIL - (RAM_SPARE * 3) ))
if (( wanted < (RAM_SPARE * 6) )); then
wanted=$(( RAM_AVAIL - RAM_SPARE ))
fi
fi
# Divide by one byte more than a MiB to round down
local target=$(( wanted / 1048577 ))
RAM_SIZE="${target}M"
RAM_ALLOCATION="$wanted"
return 0
}
showMemoryLimitHint() {
local kernel
kernel=$(uname -r)
if [[ "${kernel,,}" == *-wsl2* ]]; then
echo
info "Docker Desktop (WSL2) is detected, follow these instructions:"
info ""
info "Increase the memory limit in \"%UserProfile%\\.wslconfig\" by setting \"memory=<size>\" under \"[wsl2]\"."
info "Then run \"wsl --shutdown\" in PowerShell and restart Docker Desktop for the new limit to take effect."
echo
fi
return 0
}
checkMinimumMemory() {
local wanted
wanted=$(numfmt --from=iec "$RAM_SIZE")
if [ "$wanted" -lt "$RAM_MINIMUM" ]; then
error "$(app) requires at least $(formatBytes "$RAM_MINIMUM") of RAM, but only $(formatBytes "$wanted") can be allocated."
showMemoryLimitHint
exit 16
fi
return 0
}
checkMemoryAllocation() {
local final="${1:-N}"
local configured
normalizeMemory
configured="$RAM_SIZE"
RAM_WARNING=""
RAM_ALLOCATION=""
getMemoryInfo
checkConfiguredMemory "$final"
configureHalfMemory
configureMaxMemory
checkMinimumMemory
if enabled "$final"; then
[ -n "$RAM_WARNING" ] && warn "$RAM_WARNING"
[ -n "$RAM_ALLOCATION" ] && info "Allocated $(formatBytes "$RAM_ALLOCATION") of RAM for $(app)."
else
RAM_SIZE="$configured"
fi
return 0
}
checkMemoryRequirement() {
checkMemoryAllocation "N"
return 0
}
finalizeMemory() {
checkMemoryAllocation "Y"
return 0
}
checkMemoryRequirement
return 0
-2357
View File
File diff suppressed because it is too large Load Diff
+7 -194
View File
@@ -17,215 +17,28 @@ CONSOLE_PID="$QEMU_DIR/console.pid"
CONSOLE_SOCKET="$QEMU_DIR/console.sock"
QEMU_START_PID="$QEMU_DIR/qemu.start.pid"
_trap() {
local func="$1"; shift
local sig
TRAP_PID=$BASHPID
for sig; do
# Capture the local callback and signal while registering the trap.
# shellcheck disable=SC2064
trap "$func $sig" "$sig"
done
return 0
}
signalCode() {
local sig="$1"
case "$sig" in
SIGHUP) echo 129 ;;
SIGINT) echo 130 ;;
SIGQUIT) echo 131 ;;
SIGABRT) echo 134 ;;
SIGTERM) echo 143 ;;
*) echo 0 ;;
esac
return 0
}
displayReason() {
local reason="$1"
case "$reason" in
129 ) echo "SIGHUP" ;;
130 ) echo "SIGINT" ;;
131 ) echo "SIGQUIT" ;;
134 ) echo "SIGABRT" ;;
143 ) echo "SIGTERM" ;;
* ) echo "$reason" ;;
esac
return 0
}
readQemuPid() {
# Interactive startup uses a wrapper-created PID file before QEMU writes its
# own pidfile, so accept either during startup and shutdown races.
readPidFile "$1" "$QEMU_START_PID" && return 0
readPidFile "$1" "$QEMU_PID"
}
qemuPidFile() {
local -n _file="$1"
_file="$QEMU_PID"
[ -s "$QEMU_START_PID" ] && _file="$QEMU_START_PID"
return 0
}
waitQemuExit() {
local timeout="${1:-10}"
local file
qemuPidFile file
waitPidFile "$file" "$timeout"
}
waitQemuPid() {
local cnt=0
while ! readQemuPid "$1"; do
sleep 0.02
cnt=$((cnt + 1))
(( cnt >= 50 )) && return 1
done
return 0
}
forceKillQemu() {
local reason="$1"
local pid display
readQemuPid pid || return 0
isAlive "$pid" || return 0
display=$(displayReason "$reason")
error "Forcefully terminating $(app), reason: $display..."
{ disown "$pid" || :; kill -9 -- "$pid" || :; } 2>/dev/null
return 0
}
cleanupHelpers() {
local pids=( "${HOST_PID:-}" "${WSD_PID:-}" "${CONSOLE_PID:-}" \
"${WEB_PID:-}" "${PASST_PID:-}" "${DNSMASQ_PID:-}" )
mKill "${pids[@]}"
fKill "print.sh"
rm -f -- "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET"
closeNetwork
return 0
}
startConsole() {
local output="${1:-/dev/tty}"
local cnt=0
rm -f -- "$CONSOLE_SOCKET" "$CONSOLE_PID"
if ! stty -icanon -echo isig -ixon min 1 time 0 </dev/tty; then
error "Failed to configure serial console terminal!"
return 1
fi
(
trap '' INT QUIT
exec nc -lU "$CONSOLE_SOCKET" </dev/tty >"$output"
) &
local pid="$!"
echo "$pid" > "$CONSOLE_PID"
while [ ! -S "$CONSOLE_SOCKET" ]; do
if ! isAlive "$pid"; then
rm -f -- "$CONSOLE_PID"
error "Serial console relay exited unexpectedly!"
return 1
fi
sleep 0.02
cnt=$((cnt + 1))
if (( cnt > 100 )); then
error "Failed to start serial console relay!"
return 1
fi
done
return 0
}
stopConsole() {
mKill "$CONSOLE_PID"
return 0
}
startQemu() {
rm -f -- "$QEMU_START_PID"
# Launch QEMU in a separate session while recording the real child PID;
# setsid's wrapper PID is not suitable for guest shutdown or forced cleanup.
(
trap '' INT QUIT
# shellcheck disable=SC2016
exec setsid -f -w sh -c '
file=$1
shift
"$@" &
pid=$!
printf "%s\n" "$pid" > "$file" || exit 1
rc=0
wait "$pid" 2>/dev/null || rc=$?
exit "$rc"
' sh "$QEMU_START_PID" "$@"
) </dev/null &
return 0
}
finish() {
local reason=$1 failed=0
# A nonzero exit is unexpected only when QEMU_END is missing.
if [ ! -f "$QEMU_END" ] && (( reason != 0 )); then
failed=1
fi
touch "$QEMU_END"
touch "$QEMU_END" || :
forceKillQemu "$reason"
cleanupHelpers
cleanupHelpers "$HOST_PID"
fKill "print.sh"
rm -f -- "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET"
if ! waitQemuExit 10; then
warn "Timed out while waiting for $(app) to exit!"
fi
stopConsole
echo
if (( failed == 0 )); then
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -Eeuo pipefail
addSystemDisks() {
local boot="$STORAGE/$BASE.boot.img"
local system="$STORAGE/$BASE.system.img"
[ ! -s "$boot" ] && error "Virtual DSM boot-image does not exist ($boot)" && return 81
[ ! -s "$system" ] && error "Virtual DSM system-image does not exist ($system)" && return 82
setOwner "$boot" || warn "failed to set the owner for \"$boot\" !"
setOwner "$system" || warn "failed to set the owner for \"$system\" !"
DISK_OPTS+=$(createDevice "$boot" "$DISK_TYPE" "1" "0xa" "raw" "$DISK_IO" "$DISK_CACHE" "" "" "synoboot")
DISK_OPTS+=$(createDevice "$system" "$DISK_TYPE" "2" "0xb" "raw" "$DISK_IO" "$DISK_CACHE" "" "" "synosys")
return 0
}
closeWebserver() {
disabled "${NETWORK:-Y}" && return 0
MSG="Booting DSM instance..."
html "$MSG" || return $?
if enabled "${DHCP:-N}" || disabled "${WEB:-}"; then
return 0
fi
writeAtomic "$WSD_COMMAND" "portal" || return $?
sleep 1.2
stopAllServers
WEB="N"
return 0
}
addSystemDisks || return $?
closeWebserver || return $?
return 0
+3 -1
View File
@@ -18,8 +18,9 @@ file="/run/shm/dsm.url"
msgs="/run/shm/msg.html"
driver="/run/shm/qemu.nic"
page="/run/shm/index.html"
address="/run/shm/qemu.ip"
address="/run/shm/qemu.host"
shutdown="/run/shm/qemu.end"
command="/run/shm/status.cmd"
socket="/run/shm/qemu-host-api.sock"
template="/var/www/index.html"
url="http://localhost/read?command=10"
@@ -175,6 +176,7 @@ writeDhcpPage() {
echo "$html" > "$page"
echo "$body" > "$msgs"
writeAtomic "$command" "portal http://$location"
return 0
}
-209
View File
@@ -1,209 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Docker environment variables
: "${HOST_CPU:=""}"
: "${CPU_FLAGS:=""}"
: "${CPU_MODEL:=""}"
HOST_CPU=$(strip "$HOST_CPU")
CPU_FLAGS=$(strip "$CPU_FLAGS")
CPU_MODEL=$(strip "$CPU_MODEL")
selectClocksource() {
CLOCKSOURCE="tsc"
[[ "${ARCH,,}" == "arm64" ]] && CLOCKSOURCE="arch_sys_counter"
CLOCK="/sys/devices/system/clocksource/clocksource0/current_clocksource"
return 0
}
checkClocksource() {
local result
if [ ! -f "$CLOCK" ]; then
warn "file \"$CLOCK\" cannot be found?"
return 0
fi
result=$(<"$CLOCK")
result="${result//[![:print:]]/}"
case "${result,,}" in
"${CLOCKSOURCE,,}" ) ;;
"kvm-clock" ) info "Nested KVM virtualization detected.." ;;
"hyperv_clocksource_tsc_page" ) info "Nested Hyper-V virtualization detected.." ;;
"hpet" ) warn "unsupported clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;;
*) warn "unexpected clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;;
esac
return 0
}
checkSse42() {
if ! hasFlag "sse4_2"; then
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
enabled "$DEBUG" || exit 88
fi
return 0
}
trimSpaces() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
echo "$value"
return 0
}
removeCpuArgument() {
# CPU configuration has dedicated variables. Remove raw -cpu arguments so
# option ordering cannot silently override the validated model and flags.
local args=" ${ARGUMENTS:-} "
while [[ "$args" =~ [[:space:]]-cpu([[:space:]][^[:space:]]+|=[^[:space:]]+)? ]]; do
local cpu="${BASH_REMATCH[0]}"
args="${args/$cpu/ }"
warn "Ignoring '${cpu#" "}' from ARGUMENTS, use CPU_MODEL and CPU_FLAGS instead."
done
ARGUMENTS=$(trimSpaces "$args")
return 0
}
configureKvmCpuModel() {
CPU_FEATURES="kvm=on,l3-cache=on,+hypervisor"
KVM_OPTS=",accel=kvm -enable-kvm -global kvm-pit.lost_tick_policy=discard"
if [ -z "$CPU_MODEL" ]; then
CPU_MODEL="host"
CPU_FEATURES+=",migratable=no"
fi
return 0
}
appendKvmInvtscFeature() {
# invtsc is safe only when the active accelerator can scale the host TSC;
# AMD and Intel expose that capability through different host flags.
if hasFlag "svm"; then
# AMD processor
if hasFlag "tsc_scale"; then
CPU_FEATURES+=",+invtsc"
fi
else
# Intel processor
local vmx
vmx=$(sed -ne '/^vmx flags/s/^.*: //p' /proc/cpuinfo)
if grep -qw "tsc_scaling" <<< "$vmx"; then
CPU_FEATURES+=",+invtsc"
fi
fi
return 0
}
configureKvm() {
configureKvmCpuModel
checkSse42
appendKvmInvtscFeature
return 0
}
configureTcgCpuModel() {
if [ -n "$CPU_MODEL" ]; then
return 0
fi
# TCG uses the broad max model on native x86, but qemu64 is the compatible
# cross-architecture fallback.
if [[ "$ARCH" == "amd64" ]]; then
CPU_MODEL="max"
CPU_FEATURES+=",migratable=no"
else
CPU_MODEL="qemu64"
fi
return 0
}
configureTcg() {
KVM_OPTS=""
CPU_FEATURES="l3-cache=on,+hypervisor"
if [[ "$ARCH" == "amd64" ]]; then
KVM_OPTS=" -accel tcg,thread=multi"
fi
configureTcgCpuModel
CPU_FEATURES+=",+ssse3,+sse4.1,+sse4.2"
return 0
}
composeCpuFlags() {
# Compose one -cpu value in precedence order: model, required features,
# then user-provided overrides.
CPU_FLAGS="${CPU_MODEL}${CPU_FEATURES:+,$CPU_FEATURES}${CPU_FLAGS:+,$CPU_FLAGS}"
return 0
}
configureHostCpuName() {
if [ -z "$HOST_CPU" ]; then
[[ "${CPU,,}" != "unknown" ]] && HOST_CPU="$CPU"
fi
if [ -n "$HOST_CPU" ]; then
# qemu-host expects a comma-separated CPU description with empty family
# and suffix fields, not QEMU's -cpu syntax.
HOST_CPU="${HOST_CPU%%,*},,"
else
HOST_CPU="QEMU, Virtual CPU,"
if [ "$ARCH" == "amd64" ]; then
HOST_CPU+=" X86_64"
else
HOST_CPU+=" $ARCH"
fi
fi
return 0
}
selectClocksource
checkClocksource
if ! disabled "$KVM"; then
configureKvm
else
configureTcg
fi
removeCpuArgument
composeCpuFlags
configureHostCpuName
return 0
-310
View File
@@ -1,310 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
info="/run/shm/msg.html"
info_tmp="${info}.${BASHPID}.tmp"
escape() {
local s
s=${1//&/\&amp;}
s=${s//</\&lt;}
s=${s//>/\&gt;}
s=${s//'"'/\&quot;}
s=${s//"'"/\&#39;}
printf '%s' "$s"
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() {
local path="$1"
local mode="$2"
local bytes="0"
if [[ "$mode" == "counter" ]]; then
if [ -r "$path" ]; then
read -r bytes < "$path" || bytes="0"
fi
[[ "$bytes" =~ ^[0-9]+$ ]] || bytes="0"
printf '%s\n' "$bytes"
return 0
fi
if [ ! -s "$path" ] && [ ! -d "$path" ]; then
printf '0\n'
return 0
fi
if [[ "$mode" == "allocated" ]]; then
bytes=$(du -sB1 -- "$path" 2>/dev/null | cut -f1) || bytes="0"
else
bytes=$(du -sb -- "$path" 2>/dev/null | cut -f1) || bytes="0"
fi
printf '%s\n' "$bytes"
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() {
local bytes="$1"
local size
size=$(numfmt --to=iec --suffix=B "$bytes" |
sed -r 's/([A-Z])/ \1/') ||
size="${bytes} bytes"
printf '%s' "$size"
return 0
}
printPercentProgress() {
local percent="$1"
while (( next_percent <= percent && next_percent <= 100 )); do
if [[ "$printed" == "Y" ]]; then
printf ' → %s%%' "$next_percent"
else
printf '%s%%' "$next_percent"
fi
printed="Y"
next_percent=$((next_percent + 10))
done
return 0
}
printCurrentSize() {
local bytes="$1"
local size
size=$(formatSize "$bytes")
if [[ "$printed" == "Y" ]]; then
printf ' → %s' "$size"
else
printf '%s' "$size"
fi
printed="Y"
return 0
}
printSizeProgress() {
local bytes="$1"
local size
while (( bytes >= next_bytes )); do
size=$(formatSize "$next_bytes")
if [[ "$printed" == "Y" ]]; then
printf ' → %s' "$size"
else
printf '%s' "$size"
fi
printed="Y"
next_bytes=$((next_bytes + step_bytes))
done
return 0
}
stopProgress() {
if [ -z "$status_file" ]; then
exit 0
fi
stopping="Y"
return 0
}
finishProgress() {
rm -f -- "$info_tmp"
if [[ "$output" == "log" && "$printed" == "Y" ]]; then
printf '\n'
fi
return 0
}
path="$1"
total="$2"
body=$(escape "$3")
output="${4:-}"
step_bytes="${5:-536870912}"
mode="${6:-apparent}"
status_file="${7:-}"
if [[ -n "$total" && ! "$total" =~ ^(0|[1-9][0-9]*)$ ]]; then
printf 'Invalid total size: %s\n' "$total" >&2
exit 2
fi
if [[ ! "$step_bytes" =~ ^[1-9][0-9]*$ ]]; then
printf 'Invalid progress interval: %s\n' "$step_bytes" >&2
exit 2
fi
case "$mode" in
apparent | allocated | counter ) ;;
* )
printf 'Invalid progress mode: %s\n' "$mode" >&2
exit 2
;;
esac
case "$output" in
"" | log ) ;;
* )
printf 'Invalid progress output: %s\n' "$output" >&2
exit 2
;;
esac
printed="N"
next_percent=10
next_bytes="$step_bytes"
log_mode="percent"
stopping="N"
if [ -z "$total" ] || [[ "$total" == "0" ]]; then
log_mode="size"
fi
trap finishProgress EXIT
trap 'exit 0' HUP INT QUIT
# SIGTERM requests one final measurement and web update rather than
# terminating between progress samples.
trap stopProgress TERM
if [[ "$body" == *"..." ]]; then
body="<p class=\"loading\">${body::-3}</p>"
fi
while true; do
final_pass="${stopping:-}"
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
write_html="Y"
if [ -z "$effective_total" ] ||
[[ "$effective_total" == "0" ]] ||
(( bytes > effective_total )); then
size=$(formatSize "$bytes")
if [[ "$output" == "log" ]]; then
if [[ "$log_mode" == "percent" ]]; then
printCurrentSize "$bytes"
next_bytes=$(((bytes / step_bytes + 1) * step_bytes))
log_mode="size"
else
printSizeProgress "$bytes"
fi
fi
else
# Floor the percentage rather than rounding so displayed completion
# never gets ahead of bytes actually written.
# Truncate to one decimal so progress is never reported early.
progress=$((bytes * 1000 / effective_total))
(( progress > 1000 )) && progress=1000
percent=$((progress / 10))
printf -v size '%d.%d%%' \
"$((progress / 10))" \
"$((progress % 10))"
if [[ "$output" == "log" ]]; then
if [[ "$log_mode" == "size" ]]; then
printSizeProgress "$bytes"
else
printPercentProgress "$percent"
fi
fi
# Do not update the web viewer until at least 0.1% is reached.
(( progress == 0 )) && write_html="N"
fi
if [[ "$write_html" == "Y" ]]; then
writeInfo "${body//(\[P\])/($size)}"
fi
fi
[[ "$final_pass" == "Y" ]] && break
sleep 1 &
wait $! || :
done
+28 -3
View File
@@ -4,6 +4,7 @@ set -Eeuo pipefail
# Docker environment variables
: "${HOST_MAC:=""}"
: "${HOST_CPU:=""}"
: "${HOST_DEBUG:=""}"
: "${HOST_MODEL:=""}"
: "${HOST_SERIAL:=""}"
@@ -11,6 +12,7 @@ set -Eeuo pipefail
# Sanitize variables
HOST_MAC=$(strip "$HOST_MAC")
HOST_CPU=$(strip "$HOST_CPU")
HOST_MODEL=$(strip "$HOST_MODEL")
HOST_SERIAL=$(strip "$HOST_SERIAL")
GUEST_SERIAL=$(strip "$GUEST_SERIAL")
@@ -20,7 +22,6 @@ HOST_API_SOCKET="$QEMU_DIR/qemu-host-api.sock"
HOST_AGENT_SOCKET="$QEMU_DIR/qemu-host-agent.sock"
validateHostMac() {
local m
if [ -z "$HOST_MAC" ]; then
return 0
@@ -29,12 +30,35 @@ validateHostMac() {
HOST_MAC="${HOST_MAC//-/:}"
if [[ ${#HOST_MAC} == 12 ]]; then
m="$HOST_MAC"
local m="$HOST_MAC"
HOST_MAC="${m:0:2}:${m:2:2}:${m:4:2}:${m:6:2}:${m:8:2}:${m:10:2}"
fi
if [[ ${#HOST_MAC} != 17 ]]; then
error "Invalid HOST_MAC address: '$HOST_MAC', should be 12 or 17 digits long!" && exit 28
error "Invalid HOST_MAC address: '$HOST_MAC', should be 12 or 17 digits long!"
exit 28
fi
return 0
}
configureHostCpuName() {
if [ -z "$HOST_CPU" ]; then
[[ "${CPU,,}" != "unknown" ]] && HOST_CPU="$CPU"
fi
if [ -n "$HOST_CPU" ]; then
# qemu-host expects a comma-separated CPU description with empty family
# and suffix fields, not QEMU's -cpu syntax.
HOST_CPU="${HOST_CPU%%,*},,"
else
HOST_CPU="QEMU, Virtual CPU,"
if [ "$ARCH" == "amd64" ]; then
HOST_CPU+=" X86_64"
else
HOST_CPU+=" $ARCH"
fi
fi
return 0
@@ -141,6 +165,7 @@ configureSerialPorts() {
}
validateHostMac
configureHostCpuName
buildHostArguments
startHostBinary
-185
View File
@@ -1,185 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
: "${WEB_PORT:="5000"}" # Webserver port
# Sanitize port variables
WEB_PORT=$(strip "$WEB_PORT")
WEB_PID="/run/nginx.pid"
WSD_LOG="/var/log/websocketd.log"
WSD_PID="$QEMU_DIR/websocketd.pid"
WSD_SOCKET="$QEMU_DIR/status-ws.sock"
prepareWebFiles() {
cp -r /var/www/* "$QEMU_DIR" || return 1
rm -f -- "$WSD_PID" "$WSD_SOCKET" "$WEB_PID" "$WSD_LOG" || return 1
return 0
}
configureWebPorts() {
if ! sed -i \
-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
}
configureIpv6Listen() {
# Use one dual-stack listener when IPv6 is active, avoiding separate IPv4
# and IPv6 sockets that can conflict on the same port.
if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]]; then
if ! sed -i \
"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
return 0
}
configureWebServer() {
configureNginx || return 1
configureWebPorts || return 1
configureIpv6Listen || return 1
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() {
# Start webserver
nginx -e stderr || return 1
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() {
# Start websocket server
websocketd \
--unixsocket="$WSD_SOCKET" \
/run/socket.sh \
>"$WSD_LOG" 2>&1 &
local pid=$!
if ! echo "$pid" > "$WSD_PID"; then
kill "$pid" 2>/dev/null || :
rm -f -- "$WSD_PID"
return 1
fi
local i
for (( i = 1; i <= 50; i++ )); do
if ! isAlive "$pid"; then
rm -f -- "$WSD_PID" "$WSD_SOCKET"
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
error "Failed to start websocket server!"
return 1
fi
[ -S "$WSD_SOCKET" ] && return 0
sleep 0.1
done
pKill "$pid" 2
if isAlive "$pid"; then
kill -9 -- "$pid" 2>/dev/null || :
fi
rm -f -- "$WSD_PID" "$WSD_SOCKET"
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
error "Websocket server did not create its socket!"
return 1
}
prepareWebFiles
html "Starting $APP for $ENGINE..."
disabled "${WEB:-}" && return 0
configureWebServer
if startWebServer && startWebsocketServer; then
return 0
fi
stopWebsocketServer || :
stopWebServer || :
return 1
+43 -20
View File
@@ -2,47 +2,70 @@
set -Eeuo pipefail
lastmsg=""
path="/run/shm/msg.html"
dir=$(dirname -- "$path")
name=$(basename -- "$path")
lastcmd=""
status="/run/shm/msg.html"
command="/run/shm/status.cmd"
dir=$(dirname -- "$status")
status_name=$(basename -- "$status")
command_name=$(basename -- "$command")
refresh() {
# websocketd clients use s: for status updates and c: for control commands.
refreshStatus() {
[ ! -f "$path" ] && return 0
[ ! -s "$path" ] && return 0
[ ! -f "$status" ] && return 0
[ ! -s "$status" ] && return 0
msg=$(< "$path") || return 0
msg=$(< "$status") || return 0
msg="${msg%$'\n'}"
[ -z "$msg" ] && return 0
[[ "$msg" == "$lastmsg" ]] && return 0
lastmsg="$msg"
# websocketd clients interpret s: as a status update and c: as a command;
# suppress unchanged status to avoid redundant browser work.
echo "s: $msg"
return 0
}
refresh
refreshCommand() {
[ ! -f "$command" ] && return 0
[ ! -s "$command" ] && return 0
cmd=$(< "$command") || return 0
cmd="${cmd%$'\n'}"
[ -z "$cmd" ] && return 0
[[ "$cmd" == "$lastcmd" ]] && return 0
lastcmd="$cmd"
echo "c: $cmd"
return 0
}
refreshStatus
refreshCommand
# Watch the directory because status and command updates use atomic rename.
inotifywait \
-m -q \
-e close_write,moved_to,delete \
-e close_write,moved_to \
--format '%e %f' \
"$dir" |
while read -r event file; do
[[ "$file" == "$name" ]] || continue
case "${event,,}" in
"delete"* )
echo "c: vnc" ;;
# moved_to covers the atomic replacement used by html()/writeAtomic(),
# while close_write handles direct writers.
"close_write"* | "moved_to"* )
refresh ;;
case "$file" in
"$status_name" )
case "${event,,}" in
"close_write"* | "moved_to"* )
refreshStatus ;;
esac ;;
"$command_name" )
case "${event,,}" in
"close_write"* | "moved_to"* )
refreshCommand ;;
esac ;;
esac
done
-6
View File
@@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# You can override this hook to execute a script before startup!
return 0
-646
View File
@@ -1,646 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Helper functions
info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "${1:-}" "\E[0m\n"; }
error () { printf "%b%s%b" "\E[1;31m " "ERROR: ${1:-}" "\E[0m\n" >&2; }
warn () { printf "%b%s%b" "\E[1;31m " "Warning: ${1:-}" "\E[0m\n" >&2; }
app() {
echo "Virtual DSM"
return 0
}
readPidFile() {
local -n _pid="$1"
_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() {
# Match a whitespace-delimited token in /proc/cpuinfo
grep -m1 '^flags[[:space:]]*:' /proc/cpuinfo | grep -Fqw -- "$1"
}
hasFeature() {
# Match a whitespace-delimited token in /proc/cpuinfo
grep -m1 '^Features[[:space:]]*:' /proc/cpuinfo | grep -Fqw -- "$1"
}
isAmdCpu() {
local vendor
vendor=$(awk -F ': *' '/^vendor_id/{print $2; exit}' /proc/cpuinfo)
[[ "$vendor" == "AuthenticAMD" ]]
}
getPciBus() {
local machine="${1:-${MACHINE:-q35}}"
if [ -n "${PCI_BUS:-}" ]; then
echo "$PCI_BUS"
return 0
fi
case "${machine,,}" in
pc|pc-i440fx*) echo "pci.0" ;;
*) echo "pcie.0" ;;
esac
return 0
}
interactive() {
# A TTY on stdin is insufficient when /dev/tty is unavailable; require both
# before enabling interactive console handling.
[ -t 0 ] && : 2>/dev/null </dev/tty >/dev/tty
}
strip() {
local value="${1:-}"
# Remove surrounding whitespace
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
# Remove leading/trailing single/double quotes
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
# Remove surrounding whitespace again
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
enabled() {
local value
value=$(strip "${1:-}")
case "${value,,}" in
y|yes|true|1|on|enable|enabled) return 0 ;;
*) return 1 ;;
esac
}
disabled() {
local value
value=$(strip "${1:-}")
case "${value,,}" in
n|no|none|false|0|off|disable|disabled) return 0 ;;
*) return 1 ;;
esac
}
formatBytes() {
local result
if ! result=$(numfmt --to=iec --suffix=B "$1" | sed -r 's/([A-Z])/ \1/' | sed 's/ B/ bytes/g;'); then
return 1
fi
local unit="${result//[0-9. ]}"
result="${result//[a-zA-Z ]/}"
if [[ "${2:-}" == "up" ]]; then
if [[ "$result" == *"."* ]]; then
result="${result%%.*}"
result=$((result+1))
fi
else
if [[ "${2:-}" == "down" ]]; then
result="${result%%.*}"
fi
fi
echo "$result $unit"
return 0
}
isAlive() {
local pid="$1"
[ -z "$pid" ] && return 1
if kill -0 "$pid" 2>/dev/null; then
return 0
fi
return 1
}
waitPid() {
local pid="$1"
local timeout="${2:-10}"
local deadline=$((SECONDS + timeout))
while [ -n "$pid" ] && isAlive "$pid"; do
(( SECONDS >= deadline )) && return 1
sleep 0.2
done
return 0
}
waitPidFile() {
local pid
local file="$1"
local timeout="${2:-10}"
local deadline=$((SECONDS + timeout))
readPidFile pid "$file" || return 0
while [ -s "$file" ] && isAlive "$pid"; do
(( SECONDS >= deadline )) && return 1
sleep 0.2
done
rm -f -- "$file"
return 0
}
pKill() {
local pid="$1"
local timeout="${2:-10}"
{ kill -15 -- "$pid" || :; } 2>/dev/null
if ! waitPid "$pid" "$timeout"; then
warn "Timed out while waiting for PID $pid"
fi
return 0
}
fWait() {
local name="$1"
local timeout="${2:-10}"
local deadline=$((SECONDS + timeout))
[ -z "$name" ] && return 0
while pgrep -f -l "$name" >/dev/null; do
if (( SECONDS >= deadline )); then
warn "Timed out while waiting for process: $name"
break
fi
sleep 0.2
done
return 0
}
fKill() {
local name="$1"
local timeout="${2:-10}"
[ -z "$name" ] && return 0
{ pkill -f "$name" || :; } 2>/dev/null
fWait "$name" "$timeout"
return 0
}
sKill() {
local pid
local file="$1"
readPidFile pid "$file" || return 0
if isAlive "$pid"; then
{ kill -15 -- "$pid" || :; } 2>/dev/null
fi
return 0
}
mKill() {
local timeout=10
local files=("$@")
for file in "${files[@]}"; do
sKill "$file"
done
for file in "${files[@]}"; do
if ! waitPidFile "$file" "$timeout"; then
warn "Timed out while waiting for PID file: $file"
fi
done
return 0
}
setOwner() {
local file="$1"
local dir uid gid
[ ! -f "$file" ] && return 1
# Match generated files to the owner of their bind-mounted parent directory
# instead of assuming a fixed container or host UID.
dir=$(dirname -- "$file")
uid=$(stat -c '%u' "$dir") || return 1
gid=$(stat -c '%g' "$dir") || return 1
chown "$uid:$gid" "$file" || return 1
return 0
}
makeDir() {
local path="$1"
local dir uid gid
[ -d "$path" ] && return 0
mkdir -p "$path" || return 1
dir=$(dirname -- "$path")
if ! uid=$(stat -c '%u' "$dir") || ! gid=$(stat -c '%g' "$dir"); then
warn "failed to determine the owner for \"$path\"."
return 0
fi
if ! chown "$uid:$gid" "$path"; then
warn "failed to set the owner for \"$path\"."
return 0
fi
return 0
}
finiteMemoryLimit() {
local limit="$1"
# cgroup v1 commonly reports this enormous sentinel for an unlimited memory
# limit; compare as decimal strings to avoid shell integer overflow.
local sentinel="4611686018427387904"
local i
[[ "$limit" =~ ^[0-9]+$ ]] || return 1
(( ${#limit} < ${#sentinel} )) && return 0
(( ${#limit} > ${#sentinel} )) && return 1
for (( i=0; i<${#sentinel}; i++ )); do
local left="${limit:i:1}"
local right="${sentinel:i:1}"
(( left < right )) && return 0
(( left > right )) && return 1
done
return 1
}
getMemoryInfo() {
local host_total
local host_avail
local limit=""
local current=""
host_total=$(free -b | awk '/^Mem:/ {print $2; exit}')
host_avail=$(free -b | awk '/^Mem:/ {print $7; exit}')
RAM_TOTAL="$host_total"
RAM_AVAIL="$host_avail"
if [ -r /sys/fs/cgroup/memory.max ] && [ -r /sys/fs/cgroup/memory.current ]; then
limit=$(< /sys/fs/cgroup/memory.max)
current=$(< /sys/fs/cgroup/memory.current)
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ] && [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then
limit=$(< /sys/fs/cgroup/memory/memory.limit_in_bytes)
current=$(< /sys/fs/cgroup/memory/memory.usage_in_bytes)
fi
# Use the tighter of host availability and the container's remaining cgroup
# allowance so RAM sizing cannot exceed either boundary.
if finiteMemoryLimit "$limit" && [[ "$current" =~ ^[0-9]+$ ]]; then
(( limit < RAM_TOTAL )) && RAM_TOTAL="$limit"
local available=$(( limit - current ))
(( available < 0 )) && available=0
(( available < RAM_AVAIL )) && RAM_AVAIL="$available"
fi
return 0
}
stateFile() {
local name="$1"
local prefix="${2:-$PROCESS}"
[[ "$name" == */* ]] && printf '%s\n' "$name" && return 0
printf '%s/%s.%s\n' "$STORAGE" "$prefix" "$name"
return 0
}
writeFile() {
local txt="$1"
local path="$2"
if ! printf '%s\n' "$txt" > "$path"; then
error "Failed to write file \"$path\" !"
return 1
fi
if ! setOwner "$path"; then
warn "failed to set the owner for \"$path\"."
fi
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() {
local path="$1"
local value
[ -s "$path" ] || return 0
value=$(<"$path") || return 1
value="${value//[![:print:]]/}"
printf '%s\n' "$value"
return 0
}
writeState() {
local name="$1"
local value="$2"
local prefix="${3:-$PROCESS}"
local path
[ -z "$value" ] && return 0
path=$(stateFile "$name" "$prefix") || return 1
writeFile "$value" "$path"
return $?
}
readState() {
local name="$1"
local prefix="${2:-$PROCESS}"
local path
path=$(stateFile "$name" "$prefix") || return 1
readFile "$path"
return $?
}
restoreState() {
local var="$1"
local name="$2"
local force="${3:-N}"
local prefix="${4:-$PROCESS}"
local value
# Persistent state fills only unset variables unless force is requested,
# preserving explicit environment overrides.
if ! enabled "$force"; then
[ -z "${!var:-}" ] || return 0
fi
value=$(readState "$name" "$prefix") || return 1
[ -n "$value" ] || return 0
printf -v "$var" '%s' "$value" || return 1
return 0
}
escape () {
local s=${1//&/\&amp;}
s=${s//</\&lt;}
s=${s//>/\&gt;}
s=${s//'"'/\&quot;}
printf -- %s "$s"
return 0
}
escapeXML() {
printf '%s' "$1" | sed \
-e 's/&/\&amp;/g' \
-e 's/</\&lt;/g' \
-e 's/>/\&gt;/g' \
-e 's/"/\&quot;/g' \
-e "s/'/\&apos;/g"
return 0
}
html() {
local title
local body
local script="${2:-}"
local footer
title=$(escape "$APP")
title="<title>$title</title>"
footer=$(escape "$FOOTER1")
body=$(escape "$1")
if [[ "$body" == *"..." ]]; then
body="<p class=\"loading\">${body/.../}</p>"
fi
local HTML
HTML=$(<"$TEMPLATE")
HTML="${HTML/\[1\]/$title}"
HTML="${HTML/\[2\]/$script}"
HTML="${HTML/\[3\]/$body}"
HTML="${HTML/\[4\]/$footer}"
HTML="${HTML/\[5\]/$FOOTER2}"
# Publish both the full page and websocket fragment atomically because nginx
# and websocketd may read them concurrently.
writeAtomic "$PAGE" "$HTML" || return 1
writeAtomic "$INFO" "$body" || return 1
return 0
}
cpu() {
local ret
local cpu=""
ret=$(lscpu)
if grep -qi "model name" <<< "$ret"; then
cpu=$(echo "$ret" | grep -m 1 -i 'model name' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
fi
if [ -z "${cpu// /}" ] && grep -qi "model:" <<< "$ret"; then
cpu=$(echo "$ret" | grep -m 1 -i 'model:' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
fi
cpu="${cpu// CPU/}"
cpu="${cpu// [0-9][0-9][0-9] Core}"
cpu="${cpu// [0-9][0-9] Core}"
cpu="${cpu// [0-9] Core}"
cpu="${cpu//[0-9][0-9]th Gen }"
cpu="${cpu//[0-9]th Gen }"
cpu="${cpu// Processor/}"
cpu="${cpu// Quad core/}"
cpu="${cpu// Dual core/}"
cpu="${cpu// Octa core/}"
cpu="${cpu// Hexa core/}"
cpu="${cpu// Core TM/ Core}"
cpu="${cpu// with Radeon Graphics/}"
cpu="${cpu// with Radeon Vega Graphics/}"
cpu="${cpu// with Radeon Vega Mobile Gfx/}"
cpu="${cpu// w Radeon [0-9][0-9][0-9]M Graphics/}"
[ -z "${cpu// /}" ] && cpu="Unknown"
echo "$cpu"
return 0
}
getCountry() {
local url=$1
local query=$2
local json result
{ json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); local rc=$?; } || :
(( rc != 0 )) && return 0
{ result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || :
(( rc != 0 )) && return 0
[[ ${#result} -ne 2 ]] && return 0
[[ "${result^^}" == "XX" ]] && return 0
COUNTRY="${result^^}"
return 0
}
setCountry() {
[[ "${TZ,,}" == "asia/harbin" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/beijing" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/urumqi" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/kashgar" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/shanghai" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/chongqing" ]] && COUNTRY="CN"
# Country detection is best-effort and tries independent services in order;
# failure leaves mirror selection at its global default.
[ -z "$COUNTRY" ] && getCountry "https://api.ipapi.is" ".location.country_code"
[ -z "$COUNTRY" ] && getCountry "https://ifconfig.co/json" ".country_iso"
[ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code"
[ -z "$COUNTRY" ] && getCountry "https://ipinfo.io/json" ".country"
[ -z "$COUNTRY" ] && getCountry "https://api.ipquery.io/?format=json" ".location.country_code"
[ -z "$COUNTRY" ] && getCountry "https://api.myip.com" ".cc"
return 0
}
addPackage() {
local pkg=$1
local desc=$2
if apt-mark showinstall | grep -qx "$pkg"; then
return 0
fi
MSG="Installing $desc..."
info "$MSG" && html "$MSG"
[ -z "$COUNTRY" ] && setCountry
# Use a mainland mirror only for on-demand package installation, avoiding
# slow or inaccessible Debian endpoints in that region.
if [[ "${COUNTRY^^}" == "CN" ]]; then
sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
fi
DEBIAN_FRONTEND=noninteractive apt-get -qq update || return 1
DEBIAN_FRONTEND=noninteractive apt-get -qq --no-install-recommends -y install "$pkg" > /dev/null || return 1
return 0
}
return 0
+11 -1
View File
@@ -1,4 +1,5 @@
server {
listen 5000 default_server;
autoindex on;
@@ -9,6 +10,9 @@ server {
error_log /dev/null;
access_log /dev/null;
auth_basic off;
auth_basic_user_file /etc/nginx/.htpasswd;
include /etc/nginx/mime.types;
gzip on;
@@ -19,7 +23,13 @@ server {
gzip_disable "msie6";
gzip_types text/css text/javascript text/xml text/plain text/x-component application/javascript application/json application/xml application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
add_header Cache-Control "no-cache";
set $cache_control "no-cache";
if ($uri = /msg.html) {
set $cache_control "no-store";
}
add_header Cache-Control $cache_control always;
location / {
-167
View File
@@ -1,167 +0,0 @@
body {
color: white;
background-color: #125bdb;
font-smoothing: antialiased;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: Verdana, Geneva, sans-serif;
}
#info {
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.25);
}
#content {
text-align: center;
padding: 20px;
margin-top: 50px;
}
footer {
width: 98%;
position: fixed;
bottom: 0px;
height: 40px;
text-align: center;
color: #0c8aeb;
text-shadow: 0 0 1px #0c8aeb;
}
#empty {
height: 40px;
/* Same height as footer */
}
a,
a:hover,
a:active,
a:visited {
color: white;
}
footer a:link,
footer a:visited,
footer a:active {
color: #0c8aeb;
}
footer a:hover {
color: #73e6ff;
}
.loading:after {
content: " .";
animation: dots 1s steps(5, end) infinite;
}
@keyframes dots {
0%,
20% {
color: rgba(0, 0, 0, 0);
text-shadow: 0.25em 0 0 rgba(0, 0, 0, 0), 0.5em 0 0 rgba(0, 0, 0, 0);
}
40% {
color: white;
text-shadow: 0.25em 0 0 rgba(0, 0, 0, 0), 0.5em 0 0 rgba(0, 0, 0, 0);
}
60% {
text-shadow: 0.25em 0 0 white, 0.5em 0 0 rgba(0, 0, 0, 0);
}
80%,
100% {
text-shadow: 0.25em 0 0 white, 0.5em 0 0 white;
}
}
.spinner_LWk7 {
animation: spinner_GWy6 1.2s linear infinite, spinner_BNNO 1.2s linear infinite
}
.spinner_yOMU {
animation: spinner_GWy6 1.2s linear infinite, spinner_pVqn 1.2s linear infinite;
animation-delay: .15s
}
.spinner_KS4S {
animation: spinner_GWy6 1.2s linear infinite, spinner_6uKB 1.2s linear infinite;
animation-delay: .3s
}
.spinner_zVee {
animation: spinner_GWy6 1.2s linear infinite, spinner_Qw4x 1.2s linear infinite;
animation-delay: .45s
}
@keyframes spinner_GWy6 {
0%,
50% {
width: 9px;
height: 9px
}
10% {
width: 11px;
height: 11px
}
}
@keyframes spinner_BNNO {
0%,
50% {
x: 1.5px;
y: 1.5px
}
10% {
x: .5px;
y: .5px
}
}
@keyframes spinner_pVqn {
0%,
50% {
x: 13.5px;
y: 1.5px
}
10% {
x: 12.5px;
y: .5px
}
}
@keyframes spinner_6uKB {
0%,
50% {
x: 13.5px;
y: 13.5px
}
10% {
x: 12.5px;
y: 12.5px
}
}
@keyframes spinner_Qw4x {
0%,
50% {
x: 1.5px;
y: 13.5px
}
10% {
x: .5px;
y: 12.5px
}
}
-1
View File
@@ -1 +0,0 @@
<svg id="Capa_1" enable-background="new 0 0 511.962 511.962" height="512" viewBox="0 0 511.962 511.962" width="512" xmlns="http://www.w3.org/2000/svg"><g><path d="m489.965 120.063c0-5.77-3.31-11.028-8.512-13.524l-218.984-105.063c-4.102-1.967-8.875-1.967-12.977 0l-218.985 105.063c-5.202 2.496-8.511 7.755-8.511 13.524l-.003 271.834c0 5.77 3.31 11.028 8.512 13.524l218.989 105.064c2.051.983 4.27 1.476 6.488 1.476 2.219 0 4.438-.492 6.488-1.476l218.989-105.064c5.202-2.496 8.512-7.755 8.512-13.524z" fill="#4e6ba6"/><path d="m489.965 120.063c0-5.77-3.31-11.028-8.512-13.524l-218.984-105.063c-2.051-.984-4.269-1.476-6.488-1.476v511.962c2.219 0 4.438-.492 6.488-1.476l218.989-105.064c5.202-2.496 8.512-7.755 8.512-13.524z" fill="#28487a"/><path d="m425.812 160.441c0-2.27-.519-4.457-1.457-6.432l-336.701-.095c-.967 1.999-1.504 4.22-1.504 6.526l-.002 191.081c0 5.769 3.31 11.028 8.512 13.524l154.833 74.285c2.051.983 4.27 1.476 6.488 1.476 2.219 0 4.438-.492 6.488-1.476l154.834-74.285c5.202-2.496 8.512-7.755 8.512-13.524z" fill="#8dc2eb"/><path d="m424.354 154.009h-168.373v286.798c2.219 0 4.438-.492 6.488-1.476l154.834-74.285c5.202-2.496 8.512-7.755 8.512-13.524l-.003-191.081c0-2.27-.52-4.458-1.458-6.432z" fill="#5e9ff6"/><path d="m417.3 146.916-154.831-74.284c-4.102-1.967-8.875-1.967-12.977 0l-154.831 74.284c-3.122 1.498-5.555 3.996-7.007 6.998l168.328 80.812 168.374-80.717c-1.448-3.044-3.9-5.579-7.056-7.093z" fill="#ecf9fd"/><path d="m417.3 146.916-154.831-74.284c-2.051-.983-4.27-1.476-6.488-1.476v163.569l168.374-80.717c-1.447-3.043-3.899-5.578-7.055-7.092z" fill="#d9f3fc"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

-34
View File
@@ -1,34 +0,0 @@
<!DOCTYPE html>
<html>
<head>
[1]
<meta http-equiv="Cache-Control" content="no-cache" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="css/style.css" />
<link rel="icon" href="img/favicon.svg" type="image/x-icon">
[2]
</head>
<body>
<div id="page">
<div id="content">
<svg id="spinner" width="64" height="64" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect class="spinner_LWk7" fill="#0c8aeb" x="1.5" y="1.5" rx="1" width="9" height="9"/>
<rect class="spinner_yOMU" fill="#0c8aeb" x="13.5" y="1.5" rx="1" width="9" height="9"/>
<rect class="spinner_KS4S" fill="#0c8aeb" x="13.5" y="13.5" rx="1" width="9" height="9"/>
<rect class="spinner_zVee" fill="#0c8aeb" x="1.5" y="13.5" rx="1" width="9" height="9"/>
</svg>
<h1 id="info">[3]</h1>
</div>
<div id="empty">
</div>
<footer id="footer">
[4]<br />
[5]
</footer>
</div>
<script type="text/javascript" src="js/script.js"></script>
</body>
</html>
+488 -106
View File
@@ -1,7 +1,12 @@
var timer;
var request;
var failureTimer;
var navigationTimer;
var booting = false;
var portal = false;
var portalUrl = "";
var interval = 1000;
var lastStatus = "";
function abortRequest() {
@@ -16,6 +21,64 @@ function abortRequest() {
return true;
}
function clearFailure() {
if (!failureTimer) {
return false;
}
clearTimeout(failureTimer);
failureTimer = null;
return true;
}
function connectionLost() {
if (booting || (portal && portalUrl.length == 0) ||
document.hidden || failureTimer) {
return false;
}
failureTimer = setTimeout(function() {
if (booting || (portal && portalUrl.length == 0) ||
document.hidden) {
failureTimer = null;
return;
}
setStopped();
}, interval * 3);
return true;
}
function clearNavigation() {
if (!navigationTimer) {
return false;
}
clearTimeout(navigationTimer);
navigationTimer = null;
return true;
}
function visibilityChanged() {
clearFailure();
clearNavigation();
if (document.hidden) {
return false;
}
getInfo();
return true;
}
function getInfo() {
var url = "msg.html";
@@ -46,77 +109,126 @@ function getURL() {
return protocol + "//" + window.location.host + path;
}
function redirect(url) {
if (document.hidden || navigationTimer) {
return false;
}
navigationTimer = setTimeout(function() {
navigationTimer = null;
window.location.assign(url);
}, 3000);
return true;
}
function beginPortal(url) {
portal = true;
portalUrl = url || "";
booting = false;
clearFailure();
setInfo("Connecting to web portal", true);
schedule();
if (portalUrl.length > 0) {
redirect(portalUrl);
}
return true;
}
function processCommand(msg) {
if (msg == "portal") {
return beginPortal("");
}
if (msg.indexOf("portal ") == 0) {
return beginPortal(msg.substring(7));
}
console.warn("Unknown command: " + msg);
return false;
}
function processMsg(msg) {
rememberStatus(msg);
setInfo(msg);
if (msg.toLowerCase().indexOf("href=") !== -1) {
var div = document.createElement("div");
div.innerHTML = msg;
var url = div.querySelector("a").href;
setTimeout(() => {
window.location.assign(url);
}, 3000);
return beginPortal(div.querySelector("a").href);
}
setInfo(msg);
return true;
}
function processInfo() {
try {
if (request.readyState != 4) {
return true;
}
if (request.readyState != 4) {
return true;
}
var response = request;
request = null;
var response = request;
request = null;
var status = response.status;
var status = response.status;
if (status == 502 || status == 503 || status == 504) {
schedule();
return true;
}
if (status == 502 || status == 503 || status == 504) {
connectionLost();
schedule();
return true;
}
var msg = response.responseText;
if (msg == null || msg.length == 0) {
if (booting) {
schedule();
return true;
}
window.location.reload();
return false;
}
var notFound = (status == 404);
if (status == 200) {
if (msg.toLowerCase().indexOf("<html>") !== -1) {
notFound = true;
} else {
processMsg(msg);
if (msg.toLowerCase().indexOf("href=") == -1) {
schedule();
}
return true;
}
}
if (notFound) {
setInfo("Connecting to web portal", true);
reload();
return true;
}
setError("Error: Received statuscode " + status);
return false;
} catch (e) {
setError("Error: " + e.message);
var msg = response.responseText;
if (msg == null || msg.length == 0) {
connectionLost();
schedule();
return false;
}
var notFound = (status == 404);
if (status == 200) {
if (msg.toLowerCase().indexOf("<html>") !== -1) {
notFound = true;
} else {
clearFailure();
processMsg(msg);
if (portalUrl.length > 0) {
setInfo("Connecting to web portal", true);
redirect(portalUrl);
} else {
schedule();
}
return true;
}
}
if (notFound) {
clearFailure();
booting = false;
setInfo("Connecting to web portal", true);
if (portalUrl.length > 0) {
redirect(portalUrl);
} else {
portal = true;
reload();
}
return true;
}
setError("Error: Received statuscode " + status);
return false;
}
function extractContent(s) {
@@ -125,52 +237,277 @@ function extractContent(s) {
return span.textContent || span.innerText;
};
function setInfo(msg, loading, error) {
try {
function escapeContent(s) {
var span = document.createElement('span');
span.textContent = s;
return span.innerHTML;
}
if (msg == null || msg.length == 0) {
return false;
}
function rememberStatus(msg) {
if (msg.includes("Booting ")) {
booting = true;
}
var el = document.getElementById("info");
if (el.innerText == msg || el.innerHTML == msg) {
return true;
}
var spin = document.getElementById("spinner");
error = !!error;
if (!error) {
spin.style.visibility = 'visible';
} else {
spin.style.visibility = 'hidden';
}
var p = "<p class=\"loading\">";
loading = !!loading;
if (loading) {
msg = p + msg + "</p>";
}
if (msg.includes(p)) {
if (el.innerHTML.includes(p)) {
el.getElementsByClassName('loading')[0].textContent = extractContent(msg);
return true;
}
}
el.innerHTML = msg;
return true;
} catch (e) {
console.log("Error: " + e.message);
var text = extractContent(msg).trim();
if (text.length == 0) {
return false;
}
if (text.includes("Booting ")) {
booting = true;
}
lastStatus = text;
return true;
}
function parseSize(value, unit) {
var powers = {
"B": 0,
"KB": 1,
"KIB": 1,
"MB": 2,
"MIB": 2,
"GB": 3,
"GIB": 3,
"TB": 4,
"TIB": 4,
"PB": 5,
"PIB": 5,
"EB": 6,
"EIB": 6
};
unit = unit.toUpperCase();
if (!Object.prototype.hasOwnProperty.call(powers, unit)) {
return null;
}
var bytes = Number(value) * Math.pow(1024, powers[unit]);
if (!Number.isFinite(bytes) || bytes < 0) {
return null;
}
return bytes;
}
function estimateProgress(bytes) {
var boundary = 512 * 1024 * 1024;
var previousBoundary = 0;
while (bytes > boundary) {
previousBoundary = boundary;
boundary *= 2;
}
if (previousBoundary === 0) {
return Math.min(bytes / boundary * 100, 99.9);
}
var rangeProgress =
(bytes - previousBoundary) /
(boundary - previousBoundary);
return Math.min(30 + Math.pow(rangeProgress, 2) * 70, 99.9);
}
function parseProgress(msg) {
var container = document.createElement("div");
container.innerHTML = msg;
var walker = document.createTreeWalker(
container,
NodeFilter.SHOW_TEXT
);
var node;
var lastNode = null;
while ((node = walker.nextNode())) {
if (node.nodeValue.trim() !== "") {
lastNode = node;
}
}
if (!lastNode) {
return {
message: msg,
progress: null,
size: null
};
}
var percentMatch = lastNode.nodeValue.match(
/\s+\((\d+(?:\.\d+)?)%\)\s*$/
);
if (percentMatch) {
var progress = Number(percentMatch[1]);
if (Number.isFinite(progress) && progress >= 0 && progress <= 100) {
lastNode.nodeValue = lastNode.nodeValue.slice(
0,
percentMatch.index
);
return {
message: container.innerHTML,
progress: progress,
size: null
};
}
}
var sizeMatch = lastNode.nodeValue.match(
/\s+\((\d+(?:\.\d+)?)\s+([KMGTPE]?i?B)\)\s*$/i
);
if (sizeMatch) {
var bytes = parseSize(sizeMatch[1], sizeMatch[2]);
if (bytes != null) {
var size = sizeMatch[1] + " " + sizeMatch[2];
return {
message: msg,
progress: estimateProgress(bytes),
size: size
};
}
}
return {
message: msg,
progress: null,
size: null
};
}
function resizeProgress() {
var info = document.getElementById("info");
var progress = document.getElementById("progress");
if (!info || !progress || progress.hidden) {
return false;
}
var style = window.getComputedStyle(info);
var measurement = document.createElement("span");
measurement.textContent = info.innerText;
measurement.style.position = "fixed";
measurement.style.left = "-9999px";
measurement.style.top = "-9999px";
measurement.style.visibility = "hidden";
measurement.style.whiteSpace = "nowrap";
measurement.style.fontFamily = style.fontFamily;
measurement.style.fontSize = style.fontSize;
measurement.style.fontWeight = style.fontWeight;
measurement.style.fontStyle = style.fontStyle;
measurement.style.letterSpacing = style.letterSpacing;
measurement.style.textTransform = style.textTransform;
document.body.appendChild(measurement);
var textWidth = measurement.getBoundingClientRect().width;
var loading = info.getElementsByClassName("loading").length > 0;
var dotsWidth = 0;
if (loading) {
dotsWidth = parseFloat(style.fontSize) * 0.75;
}
measurement.remove();
var maximumWidth = window.innerWidth * 0.8;
var width = Math.min(textWidth + dotsWidth + 100, maximumWidth);
progress.style.width = width + "px";
progress.style.transform = loading
? "translateX(" + (dotsWidth / 2) + "px)"
: "";
return true;
}
function setProgress(value, size) {
var progress = document.getElementById("progress");
var fill = document.getElementById("progress-fill");
if (value == null) {
progress.hidden = true;
progress.removeAttribute("title");
progress.removeAttribute("aria-valuenow");
progress.removeAttribute("aria-valuetext");
fill.style.width = "0%";
return true;
}
progress.hidden = false;
progress.title = size != null ? size : value + "%";
progress.setAttribute("aria-valuenow", value);
if (size != null) {
progress.setAttribute("aria-valuetext", size);
} else {
progress.removeAttribute("aria-valuetext");
}
fill.style.width = value + "%";
return true;
}
function setInfo(msg, loading, error) {
if (msg == null || msg.length == 0) {
return false;
}
var parsed = parseProgress(msg);
msg = parsed.message;
setProgress(parsed.progress, parsed.size);
var el = document.getElementById("info");
if (el.innerText == msg || el.innerHTML == msg) {
var progressEl = document.getElementById("progress");
if (progressEl && !progressEl.hidden && !progressEl.style.width) {
resizeProgress();
}
return true;
}
var spin = document.getElementById("spinner");
error = !!error;
if (!error) {
spin.style.visibility = 'visible';
} else {
spin.style.visibility = 'hidden';
}
var p = "<p class=\"loading\">";
loading = !!loading;
if (loading) {
msg = p + msg + "</p>";
}
if (msg.includes(p)) {
if (el.innerHTML.includes(p)) {
el.getElementsByClassName('loading')[0].textContent = extractContent(msg);
resizeProgress();
return true;
}
}
el.innerHTML = msg;
resizeProgress();
return true;
}
function setError(text) {
@@ -178,6 +515,18 @@ function setError(text) {
return setInfo(text, false, true);
}
function setStopped() {
var msg = "<span style=\"display:block\">The container has stopped.</span>";
msg += "<span style=\"display:block; margin-top:1em; font-size:0.85em; font-weight:normal\">Check the container log for more details.</span>";
if (lastStatus.length > 0) {
msg += "<span style=\"display:block; margin-top:1em; font-size:0.75em; font-weight:normal; color:rgba(255,255,255,0.65)\">[ Last status: " + escapeContent(lastStatus) + " ]</span>";
}
return setError(msg);
}
function schedule() {
clearTimeout(timer);
@@ -185,9 +534,19 @@ function schedule() {
}
function reload() {
setTimeout(() => {
window.location.reload();
if (document.hidden) {
return false;
}
clearNavigation();
navigationTimer = setTimeout(function() {
navigationTimer = null;
window.location.replace(window.location.href);
}, 3000);
return true;
}
function connect() {
@@ -195,8 +554,19 @@ function connect() {
var wsUrl = getURL() + "/status";
var ws = new WebSocket(wsUrl);
ws.onopen = function(e) {
clearFailure();
if (portalUrl.length > 0) {
redirect(portalUrl);
}
};
ws.onmessage = function(e) {
clearFailure();
var pos = e.data.indexOf(":");
var cmd = e.data.substring(0, pos);
var msg = e.data.substring(pos + 2);
@@ -204,15 +574,17 @@ function connect() {
switch (cmd) {
case "s":
var aborted = abortRequest();
processMsg(msg);
if (aborted &&
msg.toLowerCase().indexOf("href=") == -1) {
if (abortRequest()) {
schedule();
}
processMsg(msg);
break;
case "c":
abortRequest();
processCommand(msg);
break;
case "e":
@@ -221,6 +593,7 @@ function connect() {
schedule();
}
rememberStatus(msg);
setError(msg);
break;
@@ -231,18 +604,27 @@ function connect() {
};
ws.onclose = function(e) {
connectionLost();
if (portal && portalUrl.length == 0) {
return;
}
setTimeout(function() {
connect();
}, interval);
};
ws.onerror = function(e) {
connectionLost();
ws.close();
if (!booting) {
window.location.reload();
}
};
}
window.addEventListener("resize", resizeProgress);
document.addEventListener("visibilitychange", visibilityChanged);
rememberStatus(document.getElementById("info").innerHTML);
schedule();
connect();