Compare commits

...
70 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
KroeseandGitHub c70b93c2b6 docs: Readme (#1358) 2026-08-07 22:21:14 +02:00
KroeseandGitHub 4c700e87fe fix: Improve nginx configuration handling (#1357) 2026-08-07 22:20:28 +02:00
KroeseandGitHub 030255004f fix: Handle privileged user-mode ports (#1356) 2026-08-07 22:19:18 +02:00
KroeseandGitHub e64f9f2c39 fix: Wait for websocket sockets during startup (#1355) 2026-08-07 12:59:56 +02:00
KroeseandGitHub 537fc97d53 feat: Use Unix sockets for internal services (#1354) 2026-08-07 03:30:03 +02:00
KroeseandGitHub 438e992653 feat: Check available memory before installation starts (#1353) 2026-08-07 02:52:21 +02:00
KroeseandGitHub 615fad2a0d feat: Use dynamic PCI bus configuration (#1352) 2026-08-06 14:44:03 +02:00
KroeseandGitHub aac543e5c7 fix: Restrict minimal configured RAM amount to 1 GB (#1351) 2026-08-06 14:38:10 +02:00
KroeseandGitHub fec4f876e7 fix: Race between signal handler and cleanup (#1350) 2026-08-05 21:09:01 +02:00
KroeseandGitHub dced815c06 docs: Improve commenting (#1349) 2026-08-03 16:40:10 +02:00
KroeseandGitHub 51d6f3aeca fix: Reserve internal ports for user-mode forwarding (#1348) 2026-08-02 23:01:52 +02:00
KroeseandGitHub 4544620d48 feat: Increase indentation of printed QEMU arguments (#1347) 2026-08-01 05:49:50 +02:00
KroeseandGitHub de8468161f feat: Refactor negated command conditions (#1346) 2026-07-30 21:22:21 +02:00
renovate[bot]andGitHub 18d6be8210 chore(deps): update hadolint/hadolint-action action to v3.4.0 (#1345) 2026-07-30 21:06:27 +02:00
KroeseandGitHub e5b8cf3bf8 build: Update Passt to v2026_07_28 (#1344) 2026-07-30 10:10:41 +02:00
KroeseandGitHub ec12039f43 feat: Use deadline-based process timeouts (#1343) 2026-07-28 15:22:47 +02:00
KroeseandGitHub 3c7c3ca1b1 fix: Prevent race when reading PID files (#1342) 2026-07-28 14:22:08 +02:00
KroeseandGitHub 2b27f32cd4 fix: Disk options were applied to the controller (#1341) 2026-07-28 03:30:55 +02:00
KroeseandGitHub 55d1d50284 fix: Retry transient gateway errors in web status (#1340) 2026-07-27 23:49:22 +02:00
KroeseandGitHub 784b73b5b5 feat: Add DISK_OPTIONS support to disk devices (#1339) 2026-07-27 23:48:21 +02:00
KroeseandGitHub 85f00bb9cc docs: Added new DISK_OPTIONS variable (#1338) 2026-07-27 12:04:25 +02:00
KroeseandGitHub efe8732e47 fix: Avoid shadowing PID output variables (#1337) 2026-07-27 12:03:24 +02:00
KroeseandGitHub f6871dd61b fix: Clear invalid PID values in shared reader (#1336) 2026-07-26 17:52:14 +02:00
KroeseandGitHub a713b728ff fix: Use shared PID reader for helper processes (#1335) 2026-07-26 17:41:46 +02:00
KroeseandGitHub 0d74c6ac80 build: Update dependabot config (#1334) 2026-07-26 00:58:36 +02:00
KroeseandGitHub 63e4529eff fix: Add cleanup for failed web server startup (#1333) 2026-07-25 13:47:20 +02:00
KroeseandGitHub afd848cebb fix: Make healthcheck succeed during download (#1332) 2026-07-25 12:16:41 +02:00
KroeseandGitHub 695b075130 feat: Provide host access through system.lan (#1331) 2026-07-25 03:38:36 +02:00
KroeseandGitHub 7eff53e722 feat: Improve disk error handling (#1328) 2026-07-24 13:51:59 +02:00
KroeseandGitHub bcaf0e5980 fix: Improve network error handling (#1327) 2026-07-24 13:27:09 +02:00
KroeseandGitHub faa820c2cc feat: Inline local variable declarations (#1326) 2026-07-24 13:11:40 +02:00
KroeseandGitHub 81e477fcc4 feat: Warn when DSM and container share an IP address (#1325) 2026-07-24 12:45:54 +02:00
KroeseandGitHub abd3a1c3df fix: Network mode shown for DHCP mode (#1324) 2026-07-24 12:03:57 +02:00
KroeseandGitHub 3b59bcd284 fix: Ensure progress reaches 100% (#1322) 2026-07-23 23:03:54 +02:00
30 changed files with 1473 additions and 5638 deletions
+7
View File
@@ -1,4 +1,5 @@
version: 2
updates:
- package-ecosystem: docker
directory: /
@@ -6,9 +7,15 @@ updates:
interval: weekly
cooldown:
default-days: 7
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
ignore:
- dependency-name: "*"
update-types:
- version-update:semver-minor
- version-update:semver-patch
+2 -2
View File
@@ -27,10 +27,10 @@ jobs:
-e SC2153
-
name: Lint Dockerfile
uses: hadolint/hadolint-action@v3.3.0
uses: hadolint/hadolint-action@v3.4.0
with:
dockerfile: Dockerfile
ignore: DL3008
ignore: DL3008,DL3067
failure-threshold: warning
-
name: Validate JSON and YML files
+7 -48
View File
@@ -1,14 +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_CSTRUCT="4.7"
ARG VERSION_PASST="2026_07_16"
ARG DEBCONF_NOWARNINGS="yes"
ARG DEBIAN_FRONTEND="noninteractive"
@@ -19,54 +17,15 @@ RUN <<EOF
apt-get update
apt-get --no-install-recommends -y install \
jq \
tini \
curl \
wget \
fdisk \
unzip \
nginx \
procps \
ipcalc \
ethtool \
python3 \
python3-pip \
python3-msgpack \
python3-pysodium \
xz-utils \
iptables \
iproute2 \
dnsmasq \
fakeroot \
apt-utils \
net-tools \
e2fsprogs \
diffutils \
qemu-utils \
websocketd \
iputils-ping \
inotify-tools \
ca-certificates \
netcat-openbsd \
qemu-system-x86
# 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
python3-msgpack \
python3-pysodium
apt-get clean
# Install Python dependencies
pip3 install --no-cache-dir --break-system-packages --root-user-action=ignore "dissect.cstruct==$VERSION_CSTRUCT"
# Configure QEMU
mkdir -p /etc/qemu
echo "allow br0" > /etc/qemu/bridge.conf
# Configure nginx
unlink /etc/nginx/sites-enabled/default
sed -i 's/^worker_processes.*/worker_processes 1;/' /etc/nginx/nginx.conf
# Set version file
echo "$VERSION_ARG" > /etc/version
@@ -75,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"
+22 -8
View File
@@ -33,12 +33,13 @@ 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. |
| `DISK_ROTATION` | `1` | Rotation rate reported to the guest. Use `1` to identify the disk as an SSD. |
| `DISK_FLAGS` | | Additional options used when creating `qcow2` disks. |
| `DISK_OPTIONS` | | Additional options appended to QEMU disk devices. |
| `ALLOCATE` | `N` | Preallocates space for the data disks. |
| `STORAGE` | `/storage` | Storage directory used for disks, settings, and downloads. |
@@ -55,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. |
@@ -70,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
@@ -97,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. |
+12 -6
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 🐳
@@ -68,8 +69,8 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
- Docker or Podman on a Linux host with KVM support.
- Docker Desktop or Podman (Desktop) on Windows 11 with nested virtualization enabled.
- At least 2 GB of available RAM.
- At least 32 GB of free disk space.
- At least 1 GB of available RAM.
- At least 16 GB of free disk space.
> [!NOTE]
> Docker Desktop on Linux, macOS, and Windows 10 does not currently provide KVM access to containers and is therefore not supported.
@@ -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?
+11 -3
View File
@@ -8,19 +8,27 @@ 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"
[ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 1
# dsm.url is written only after the guest agent reports both the DSM
# address and its configured HTTP port.
[ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 0
location=$(<"$file")
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.
if enabled "$DHCP"; then
ip=$(<"$address")
echo "Failed to reach DSM at http://$location"
+62 -11
View File
@@ -1,11 +1,23 @@
#!/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() {
# Expose one thread per core in a single socket; DSM licensing and topology
# reporting are more predictable with this fixed layout.
CPU_OPTS="-cpu $CPU_FLAGS"
CPU_OPTS+=" -smp $CPU_CORES,sockets=1,dies=1,cores=$CPU_CORES,threads=1"
@@ -21,42 +33,81 @@ 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() {
MAC_OPTS="-machine type=$MACHINE,smm=off,usb=off"
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=$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() {
DEV_OPTS="-device virtio-balloon-pci,id=balloon0,bus=pcie.0,addr=0x4"
DEV_OPTS+=" -object rng-random,id=objrng0,filename=/dev/urandom"
DEV_OPTS+=" -device virtio-rng-pci,rng=objrng0,id=rng0,bus=pcie.0,addr=0x1c"
local bus
bus=$(getPciBus)
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 ' ')
return 0
}
finalizeMemory
configureMemory
configureMonitor
configureMachine
configureProcessor
configureVirtioDevices
configureDevices
buildArguments
-833
View File
@@ -1,833 +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_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_CACHE=$(strip "$DISK_CACHE")
DISK_DISCARD=$(strip "$DISK_DISCARD")
DISK_ROTATION=$(strip "$DISK_ROTATION")
BOOT="$STORAGE/$BASE.boot.img"
SYSTEM="$STORAGE/$BASE.system.img"
[ ! -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
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 gb free space
local dataSize spare=1073741824
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"
gb=$(( free / 1073741825 ))
diskSpace="${gb}G"
fi
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
# 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
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
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="${PCI_BUS:-pcie.0}"
[[ -z "${PCI_BUS:-}" && ( "${MACHINE,,}" == pc || "${MACHINE,,}" == pc-i440fx* ) ]] && bus="pci.0"
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}"
echo "$result"
;;
"nvme" )
result+=",if=none \
-device nvme,drive=${diskId}${bootIndex},serial=deadbeaf${diskIndex}${diskSerial}${diskSectors}"
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}"
echo "$result"
;;
"blk" | "virtio-blk" )
result+=",if=none \
-device virtio-blk-pci,drive=${diskId},bus=$bus,addr=$diskAddress,iothread=io2${bootIndex}${diskSerial}${diskSectors}"
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}"
echo "$result"
;;
esac
return 0
}
finishDisks () {
case "${DISK_TYPE,,}" in
"blk" | "scsi" | "virtio-blk" | "virtio-scsi" )
[[ "$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 diskFile
local dataSize missing
local available currentSize
local previousExt previousFmt
diskExt=$(fmt2ext "$diskFmt")
diskFile="$diskBase.$diskExt"
dir=$(dirname "$diskFile")
[ ! -d "$dir" ] && return 0
space=$(normalizeSize "$diskSpace" "$diskDesc" "$dir")
dataSize=$(numfmt --from=iec "$space")
fs=$(stat -f -c %T "$dir")
checkFS "$fs" "$diskFile" "$diskDesc" || exit $?
if ! supportsDirect "$fs"; then
diskIo="threads"
diskCache="writeback"
fi
if [ ! -s "$diskFile" ] ; then
if [[ "${diskFmt,,}" != "raw" ]]; then
previousFmt="raw"
else
previousFmt="qcow2"
fi
previousExt=$(fmt2ext "$previousFmt")
if [ -s "$diskBase.$previousExt" ] ; then
convertDisk "$diskBase.$previousExt" "$previousFmt" "$diskFile" "$diskFmt" "$diskBase" "$diskDesc" "$fs" || exit $?
fi
fi
if [ -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
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)
missing=$(( currentSize - used - available ))
(( missing < 0 )) && missing=0
if (( missing > 0 )); then
local gb base msg
gb=$(formatBytes "$available")
base=$(baseDir "$dir")
missing=$(formatBytes "$missing")
currentSize=$(formatBytes "$currentSize")
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
# 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 [ -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
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" )
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
-65
View File
@@ -1,65 +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}')
if ! enabled "$GPU" || isAmdCpu || [[ "$ARCH" != "amd64" ]]; then
[[ "${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
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
addPackage "xserver-xorg-video-intel" "Intel GPU drivers"
addPackage "qemu-system-modules-opengl" "OpenGL module"
return 0
+17 -2
View File
@@ -5,20 +5,35 @@ 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
. utils.sh # Load functions
. reset.sh # Initialize system
. 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
. memory.sh # Check available memory
. balloon.sh # Initialize ballooning
. config.sh # Configure arguments
. finish.sh # Finish initialization
+12 -2
View File
@@ -1,8 +1,18 @@
#!/usr/bin/env bash
set -Eeuo pipefail
if enabled "$DEBUG"; then
printf "QEMU arguments:\n\n%s\n\n" "${ARGS// -/$'\n-'}"
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
# Must always remain the very last command
enableTrap
return 0
+610 -297
View File
@@ -1,349 +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.
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() {
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")
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
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"
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
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")
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"
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"
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
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 $?
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
}
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
-112
View File
@@ -1,112 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
msg="Checking memory..."
enabled "$DEBUG" && echo "$msg"
app() {
echo "Virtual DSM"
return 0
}
checkConfiguredMemory() {
if disabled "$RAM_CHECK" || [[ "${RAM_SIZE,,}" == "max" || "${RAM_SIZE,,}" == "half" ]]; then
return 0
fi
local wanted msg avail_mem
wanted=$(numfmt --from=iec "$RAM_SIZE")
avail_mem=$(formatBytes "$RAM_AVAIL")
if (( (wanted + RAM_SPARE) > RAM_AVAIL )); then
msg="Your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is too high for the $avail_mem of free memory available,"
if [[ "${FS,,}" == "zfs" ]]; then
info "$msg but since ZFS is active this will be ignored."
else
RAM_SIZE="max"
warn "$msg it will automatically be adjusted to a lower amount."
fi
else
if (( (wanted + (RAM_SPARE * 3)) > RAM_AVAIL )); then
msg="your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is very close to the $avail_mem of free memory available,"
if [[ "${FS,,}" == "zfs" ]]; then
info "$msg but since ZFS is active this will be ignored."
else
warn "$msg please consider a lower amount."
fi
fi
fi
return 0
}
configureHalfMemory() {
local wanted
if [[ "${RAM_SIZE,,}" != "half" ]]; then
return 0
fi
if (( (RAM_AVAIL / 2) > RAM_SPARE )); then
wanted=$(( (RAM_AVAIL / 2) / 1048577 ))
RAM_SIZE="${wanted}M"
info "Allocated $wanted MB of RAM for $(app)."
else
RAM_SIZE="max"
fi
return 0
}
configureMaxMemory() {
local wanted
if [[ "${RAM_SIZE,,}" != "max" ]]; then
return 0
fi
if (( RAM_AVAIL < (RAM_SPARE * 2) )); then
wanted=$(( RAM_AVAIL / 2 ))
else
wanted=$(( RAM_AVAIL - (RAM_SPARE * 3) ))
if (( wanted < (RAM_SPARE * 6) )); then
wanted=$(( RAM_AVAIL - RAM_SPARE ))
fi
fi
wanted=$(( wanted / 1048577 ))
RAM_SIZE="${wanted}M"
info "Allocated $wanted MB of RAM for $(app)."
return 0
}
checkMinimumMemory() {
local wanted
wanted=$(numfmt --from=iec "$RAM_SIZE")
if [ "$wanted" -lt "$RAM_MINIMUM" ]; then
wanted=$(( wanted / 1048577 ))
error "Not enough memory available, there is only $wanted MB left!"
exit 16
fi
return 0
}
getMemoryInfo
checkConfiguredMemory
configureHalfMemory
configureMaxMemory
checkMinimumMemory
return 0
-2187
View File
File diff suppressed because it is too large Load Diff
+40 -214
View File
@@ -17,221 +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() {
local -n _pid="$1"
local file
for file in "$QEMU_START_PID" "$QEMU_PID"; do
if [ -s "$file" ] && read -r _pid < "$file"; then
return 0
fi
done
return 1
}
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 -n _pid="$1"
local cnt=0 value=""
while ! readQemuPid value; do
sleep 0.02
cnt=$((cnt + 1))
(( cnt >= 50 )) && return 1
done
_pid="$value"
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 pid=""
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"
) &
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"
(
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
@@ -246,14 +53,16 @@ finish() {
sendGuestShutdown() {
local pid="$1"
local response url
local response
# Virtual DSM ignores ACPI powerdown, so graceful shutdown must go through
# the qemu-host guest API exposed on the Unix socket.
# Don't send the powerdown signal because vDSM ignores ACPI signals
# nc -q 1 -w 1 -U "$QEMU_DIR/monitor.sock" &> /dev/null <<<'system_powerdown' || :
# Send shutdown command to guest agent via serial port
API_TIMEOUT=$(strip "$API_TIMEOUT")
url="http://localhost/read?command=$API_CMD&timeout=$API_TIMEOUT"
local url="http://localhost/read?command=$API_CMD&timeout=$API_TIMEOUT"
response=$(curl --unix-socket "$HOST_API_SOCKET" -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1)
if [[ "$response" =~ "\"success\"" ]]; then
@@ -275,6 +84,8 @@ sendGuestShutdown() {
normalizeTimeout() {
# Divide the remaining timeout into guest wait, SIGTERM grace, and final
# cleanup instead of allowing the API call to consume the entire budget.
local term_grace=3 # seconds before loop ends to send SIGTERM
local cleanup_grace=3 # seconds reserved after the loop for cleanup
@@ -315,8 +126,10 @@ waitForShutdown() {
local slp=$!
# Stop waiting if the process has exited
! isAlive "$pid" && break
isAlive "$pid" || break
# The process state is authoritative, but disappearance of both pidfiles
# also ends the wait when a wrapper exits before process reaping completes.
# Workaround for stale/zombie QEMU pid file
[ ! -s "$QEMU_START_PID" ] && [ ! -s "$QEMU_PID" ] && break
@@ -335,17 +148,19 @@ waitForShutdown() {
return 0
}
graceful_shutdown() {
gracefulShutdown() {
local sig="$1"
local pid="" code=0
local pid code
[[ $BASHPID != "$TRAP_PID" ]] && return
code=$(signalCode "$sig")
if [ -f "$QEMU_END" ]; then
if (( SHUTDOWN_SIGNAL != 0 )); then
# A second Ctrl-C is the explicit user request to skip the remaining
# graceful-shutdown wait and proceed to forced cleanup.
if (( code == 130 && SHUTDOWN_SIGNAL == code )); then
SHUTDOWN_SKIP=1
echo && info "Received SIGINT again, forcing shutdown..."
@@ -356,11 +171,14 @@ graceful_shutdown() {
return
fi
set +e
start=$SECONDS
SHUTDOWN_SIGNAL=$code
# Shutdown handlers must continue through missing processes and failed cleanup
# commands instead of being aborted by errexit.
set +e
touch "$QEMU_END"
echo && info "Received $sig signal, sending shutdown command..."
if ! readQemuPid pid; then
@@ -382,13 +200,21 @@ graceful_shutdown() {
finish "$code"
}
! enabled "$SHUTDOWN" && return 0
enableTrap() {
enabled "$SHUTDOWN" || return 0
# Keep Ctrl-C available to interactive users without installing an unnecessary
# SIGINT handler for background/container execution.
if interactive; then
_trap gracefulShutdown SIGINT
fi
_trap gracefulShutdown SIGTERM SIGHUP SIGABRT SIGQUIT
return 0
}
[ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT"
if interactive; then
_trap graceful_shutdown SIGINT
fi
_trap graceful_shutdown SIGTERM SIGHUP SIGABRT SIGQUIT
return 0
+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
+36 -14
View File
@@ -9,6 +9,7 @@ cd /run
. utils.sh # Load functions
info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "$1" "\E[0m\n" >&2; }
warn () { printf "%b%s%b" "\E[1;33m " "WARNING: $1" "\E[0m\n" >&2; }
error () { printf "%b%s%b" "\E[1;31m " "ERROR: $1" "\E[0m\n" >&2; }
disabled "$NETWORK" && exit 0
@@ -17,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"
@@ -36,9 +38,9 @@ exitIfShuttingDown() {
queryGuest() {
local rc
{ json=$(curl --unix-socket "$socket" -m 20 -sk "$url"); rc=$?; } || :
# Query DSM through the qemu-host sidecar rather than the guest network,
# which may not be configured yet.
{ json=$(curl --unix-socket "$socket" -m 20 -sk "$url"); local rc=$?; } || :
exitIfShuttingDown
@@ -54,9 +56,8 @@ readJsonField() {
local query="$1"
local result
local rc
{ result=$(jq -r "$query" <<< "$json"); rc=$?; } || :
{ result=$(jq -r "$query" <<< "$json"); local rc=$?; } || :
if (( rc != 0 )); then
error "$jq_err $rc ( $json )"
@@ -74,12 +75,12 @@ readJsonField() {
readGuestStatus() {
local result msg rc
local result msg
result=$(readJsonField '.status') || return 1
if [[ "$result" != "success" ]]; then
{ msg=$(jq -r '.message // empty' <<< "$json"); rc=$?; } || :
{ msg=$(jq -r '.message // empty' <<< "$json"); local rc=$?; } || :
if (( rc != 0 )); then
error "$jq_err $rc ( $json )"
@@ -118,6 +119,8 @@ writeDsmLocation() {
pollGuestLocation() {
# Keep polling until the guest reports a usable address, but stop promptly
# when container shutdown begins.
while [ ! -s "$file" ]; do
# Check if not shutting down
@@ -140,14 +143,29 @@ pollGuestLocation() {
return 0
}
checkAddressConflict() {
local guest_ip="${location%:*}"
local container_ip=""
[ -s "$address" ] && container_ip=$(<"$address")
[ -z "$container_ip" ] && return 0
[[ "$guest_ip" != "$container_ip" ]] && return 0
warn "DSM is using the same IP as the container, this will cause connectivity issues."
warn "change the container's macvlan IP or assign DSM a different address in your router."
return 0
}
writeDhcpPage() {
local title body script html
local html
msg="http://$location"
title="<title>Virtual DSM</title>"
body="The location of DSM is <a href='http://$location'>http://$location</a>"
script="<script>setTimeout(function(){ window.location.assign('http://$location'); }, 3000);</script>"
local title="<title>Virtual DSM</title>"
local body="The location of DSM is <a href='http://$location'>http://$location</a>"
local script="<script>setTimeout(function(){ window.location.assign('http://$location'); }, 3000);</script>"
html=$(<"$template")
html="${html/\[1\]/$title}"
@@ -158,18 +176,21 @@ writeDhcpPage() {
echo "$html" > "$page"
echo "$body" > "$msgs"
writeAtomic "$command" "portal http://$location"
return 0
}
buildStaticMessage() {
local nic ip port
local nic ip
nic=$(<"$driver")
ip=$(<"$address")
port="${location##*:}"
local port="${location##*:}"
# NAT and user-mode networking are reached through a forwarded host port;
# macvlan exposes DSM directly on the container-facing LAN address.
if [[ "${nic,,}" != "macvlan" ]]; then
msg="port $port"
else
@@ -196,6 +217,7 @@ exitIfShuttingDown
location=$(<"$file")
if enabled "$DHCP"; then
checkAddressConflict
writeDhcpPage
else
buildStaticMessage
-199
View File
@@ -1,199 +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() {
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() {
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
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() {
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
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
-286
View File
@@ -1,286 +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"
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
}
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"
if [ -z "$total" ] || [[ "$total" == "0" ]]; then
log_mode="size"
fi
trap finishProgress EXIT
trap 'exit 0' HUP INT QUIT TERM
if [[ "$body" == *"..." ]]; then
body="<p class=\"loading\">${body::-3}</p>"
fi
while true; do
bytes=$(getBytes "$path" "$mode")
effective_total="$total"
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
# 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
sleep 1 & wait $!
done
-362
View File
@@ -1,362 +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=""
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
local max_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
max_cap=$(((1 << (last_cap + 1)) - 1))
if [ "$cap_bnd" -eq "$max_cap" ]; then
PRIVILEGED="Y"
fi
return 0
}
normalizeCpuCores() {
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
}
checkStorage() {
# Check system
QEMU_DIR="/run/shm"
if [ ! -d "/dev/shm" ]; then
error "Directory /dev/shm not found!" && exit 14
else
[ ! -d "$QEMU_DIR" ] && ln -s /dev/shm "$QEMU_DIR"
fi
QEMU_PID="$QEMU_DIR/qemu.pid"
# Check folder
if [[ "${STORAGE,,}" != "/storage" ]]; then
mkdir -p "$STORAGE"
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
}
checkFilesystem() {
# Check filesystem
FS=$(stat -f -c %T "$STORAGE")
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then
DISK_IO="threads"
DISK_CACHE="writeback"
fi
return 0
}
finiteMemoryLimit() {
local limit="$1"
local sentinel="4611686018427387904"
local i=0
local left=""
local right=""
[[ "$limit" =~ ^[0-9]+$ ]] || return 1
(( ${#limit} < ${#sentinel} )) && return 0
(( ${#limit} > ${#sentinel} )) && return 1
for (( i=0; i<${#sentinel}; i++ )); do
left="${limit:i:1}"
right="${sentinel:i:1}"
(( left < right )) && return 0
(( left > right )) && return 1
done
return 1
}
getMemoryInfo() {
local host_total=""
local host_avail=""
local limit=""
local current=""
local available=""
host_total=$(free -b | awk '/^Mem:/ {print $2; exit}')
host_avail=$(free -b | awk '/^Mem:/ {print $7; exit}')
RAM_TOTAL="$host_total"
RAM_AVAIL="$host_avail"
if [ -r /sys/fs/cgroup/memory.max ] && [ -r /sys/fs/cgroup/memory.current ]; then
limit=$(< /sys/fs/cgroup/memory.max)
current=$(< /sys/fs/cgroup/memory.current)
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ] && [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then
limit=$(< /sys/fs/cgroup/memory/memory.limit_in_bytes)
current=$(< /sys/fs/cgroup/memory/memory.usage_in_bytes)
fi
if finiteMemoryLimit "$limit" && [[ "$current" =~ ^[0-9]+$ ]]; then
(( limit < RAM_TOTAL )) && RAM_TOTAL="$limit"
available=$(( limit - current ))
(( available < 0 )) && available=0
(( available < RAM_AVAIL )) && RAM_AVAIL="$available"
fi
return 0
}
normalizeRamSize() {
# Read host and container memory limits.
getMemoryInfo
RAM_SPARE=500000000
RAM_MINIMUM=136314880
RAM_SIZE=$(strip "$RAM_SIZE")
RAM_SIZE="${RAM_SIZE// /}"
[ -z "$RAM_SIZE" ] && RAM_SIZE="2G"
if [[ "${RAM_SIZE,,}" != "max" && "${RAM_SIZE,,}" != "half" ]]; then
if [ -z "${RAM_SIZE//[0-9. ]}" ]; then
[ "${RAM_SIZE%%.*}" -lt "130" ] && RAM_SIZE="${RAM_SIZE}G" || RAM_SIZE="${RAM_SIZE}M"
fi
RAM_SIZE=$(echo "${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
! numfmt --from=iec "$RAM_SIZE" &>/dev/null && error "Invalid RAM_SIZE: $RAM_SIZE" && exit 16
wanted=$(numfmt --from=iec "$RAM_SIZE")
[ "$wanted" -lt "$RAM_MINIMUM" ] && error "RAM_SIZE is too low: $RAM_SIZE" && exit 16
fi
return 0
}
checkKvm() {
# 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
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")
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)
IFS=. read -r KERNEL MINOR _ <<< "$SYS"
CORES=$(grep -c '^processor' /proc/cpuinfo)
if grep -qi "socket(s)" <<< "$(lscpu)"; then
SOCKETS=$(lscpu | grep -m 1 -i 'socket(s)' | awk '{print $2}')
[ -z "${SOCKETS##*[!0-9]*}" ] && SOCKETS=1
[ "$SOCKETS" -lt "1" ] && SOCKETS=1
fi
normalizeCpuCores
checkStorage
checkFilesystem
normalizeRamSize
# Print system info
SYS="${SYS/-generic/}"
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
checkKvm
# 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
+46 -11
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
@@ -42,6 +66,8 @@ validateHostMac() {
buildHostArguments() {
# qemu-host is a sidecar that bridges DSM's proprietary serial agent to
# Unix sockets used by shutdown and post-boot discovery helpers.
HOST_ARGS=()
HOST_ARGS+=("-cpu=$CPU_CORES")
HOST_ARGS+=("-cpu_arch=$HOST_CPU")
@@ -60,6 +86,8 @@ startHostBinary() {
local pid
# Remove stale sockets and pid state before starting the sidecar; a Unix
# socket path cannot be rebound while an old filesystem entry remains.
rm -f -- "$HOST_PID" "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET" || return 1
if enabled "$HOST_DEBUG"; then
@@ -73,7 +101,7 @@ startHostBinary() {
pid=$!
fi
echo "$pid" > "$HOST_PID"
printf '%s\n' "$pid" > "$HOST_PID"
return 0
}
@@ -82,23 +110,24 @@ waitForSocket() {
local socket="$1"
local exit_code="$2"
local pid cnt=0
local timeout=5 pid
local deadline=$((SECONDS + timeout))
# Do not start QEMU until both sidecar sockets are ready; otherwise the
# VirtIO serial channel or API client may race initial creation.
while [ ! -S "$socket" ]; do
if ! read -r pid < "$HOST_PID" || ! isAlive "$pid"; then
if ! readPidFile pid "$HOST_PID" || ! isAlive "$pid"; then
error "qemu-host exited unexpectedly!"
exit "$exit_code"
fi
sleep 0.1
cnt=$((cnt + 1))
if (( cnt > 50 )); then
if (( SECONDS >= deadline )); then
error "Failed to create qemu-host socket: $socket"
exit "$exit_code"
fi
sleep 0.1
done
return 0
@@ -106,6 +135,11 @@ waitForSocket() {
configureSerialPorts() {
local bus
bus=$(getPciBus)
# Managed interactive mode separates the console and QEMU monitor into
# reconnecting sockets; other runs keep the simple combined stdio monitor.
if enabled "${SHUTDOWN:-Y}" && interactive; then
CONSOLE_SOCKET="$QEMU_DIR/console.sock"
@@ -123,7 +157,7 @@ configureSerialPorts() {
fi
SERIAL_OPTS+=" \
-device virtio-serial-pci,id=virtio-serial0,bus=pcie.0,addr=0x3 \
-device virtio-serial-pci,id=virtio-serial0,bus=$bus,addr=0x3 \
-chardev socket,id=charchannel0,path=$HOST_AGENT_SOCKET,reconnect-ms=1000 \
-device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=vchannel"
@@ -131,6 +165,7 @@ configureSerialPorts() {
}
validateHostMac
configureHostCpuName
buildHostArguments
startHostBinary
-110
View File
@@ -1,110 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
: "${WEB_PORT:="5000"}" # Webserver port
: "${WSD_PORT:="8004"}" # Websockets port
# Sanitize port variables
WEB_PORT=$(strip "$WEB_PORT")
WSD_PORT=$(strip "$WSD_PORT")
WEB_PID="/run/nginx.pid"
WSD_LOG="/var/log/websocketd.log"
WSD_PID="$QEMU_DIR/websocketd.pid"
prepareWebFiles() {
cp -r /var/www/* "$QEMU_DIR" || return 1
rm -f "$WSD_PID" "$WEB_PID" "$WSD_LOG" || return 1
return 0
}
configureWebPorts() {
if ! sed -i \
-e "s|listen 5000 default_server;|listen $WEB_PORT default_server;|g" \
-e "s|proxy_pass http://127.0.0.1:8004/;|proxy_pass http://127.0.0.1:$WSD_PORT/;|g" \
/etc/nginx/sites-enabled/web.conf; then
error "Failed to configure webserver ports!"
return 1
fi
return 0
}
configureIpv6Listen() {
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
}
configureWebServer() {
mkdir -p /etc/nginx/sites-enabled
cp /etc/nginx/default.conf /etc/nginx/sites-enabled/web.conf
configureWebPorts || return 1
configureIpv6Listen || return 1
return 0
}
startWebServer() {
# Start webserver
nginx -e stderr || return 1
return 0
}
startWebsocketServer() {
# Start websocket server
websocketd \
--address 127.0.0.1 \
--port="$WSD_PORT" \
/run/socket.sh \
>"$WSD_LOG" 2>&1 &
local pid=$!
if ! echo "$pid" > "$WSD_PID"; then
kill "$pid" 2>/dev/null || :
return 1
fi
sleep 0.1
if ! isAlive "$pid"; then
rm -f "$WSD_PID"
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
error "Failed to start websocket server!"
return 1
fi
return 0
}
prepareWebFiles
html "Starting $APP for $ENGINE..."
disabled "${WEB:-}" && return 0
configureWebServer
startWebServer
startWebsocketServer
return 0
+43 -16
View File
@@ -2,16 +2,20 @@
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
@@ -23,22 +27,45 @@ refresh() {
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" ;;
"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
-540
View File
@@ -1,540 +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; }
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" ]]
}
interactive() {
[ -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 i=0
local pid="$1"
local timeout="${2:-10}"
while [ -n "$pid" ] && isAlive "$pid"; do
sleep 0.2
i=$((i + 1))
(( i >= timeout * 5 )) && return 1
done
return 0
}
waitPidFile() {
local i=0
local pid=""
local file="$1"
local timeout="${2:-10}"
[ ! -s "$file" ] && return 0
! read -r pid <"$file" && return 0
[ -z "$pid" ] && return 0
while [ -s "$file" ] && isAlive "$pid"; do
sleep 0.2
i=$((i + 1))
(( i >= timeout * 5 )) && return 1
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 i=0
local name="$1"
local timeout="${2:-10}"
[ -z "$name" ] && return 0
while pgrep -f -l "$name" >/dev/null; do
sleep 0.2
i=$((i + 1))
if (( i >= timeout * 5 )); then
warn "Timed out while waiting for process: $name"
break
fi
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"
[ ! -s "$file" ] && return 0
! read -r pid <"$file" && return 0
[ -z "$pid" ] && 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
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
}
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"
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
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
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
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
[ -n "${2:-}" ] && script="$2" || script=""
local HTML
HTML=$(<"$TEMPLATE")
HTML="${HTML/\[1\]/$title}"
HTML="${HTML/\[2\]/$script}"
HTML="${HTML/\[3\]/$body}"
HTML="${HTML/\[4\]/$footer}"
HTML="${HTML/\[5\]/$FOOTER2}"
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 rc json result
{ json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); 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"
[ -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
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
+13 -3
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 / {
@@ -39,6 +49,6 @@ server {
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_pass http://127.0.0.1:8004/;
}
proxy_pass http://unix:/run/shm/status-ws.sock:/;
}
}
-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 -99
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,70 +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 msg = response.responseText;
if (msg == null || msg.length == 0) {
var status = response.status;
if (booting) {
schedule();
return true;
}
if (status == 502 || status == 503 || status == 504) {
connectionLost();
schedule();
return true;
}
window.location.reload();
return false;
}
var notFound = (response.status == 404);
if (response.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 " + response.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) {
@@ -118,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) {
@@ -171,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);
@@ -178,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() {
@@ -188,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);
@@ -197,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":
@@ -214,6 +593,7 @@ function connect() {
schedule();
}
rememberStatus(msg);
setError(msg);
break;
@@ -224,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();