Compare commits

...
85 Commits
Author SHA1 Message Date
KroeseandGitHub c70b93c2b6 docs: Readme (#1358) 2026-08-07 22:21:14 +02:00
KroeseandGitHub 4c700e87fe fix: Improve nginx configuration handling (#1357) 2026-08-07 22:20:28 +02:00
KroeseandGitHub 030255004f fix: Handle privileged user-mode ports (#1356) 2026-08-07 22:19:18 +02:00
KroeseandGitHub e64f9f2c39 fix: Wait for websocket sockets during startup (#1355) 2026-08-07 12:59:56 +02:00
KroeseandGitHub 537fc97d53 feat: Use Unix sockets for internal services (#1354) 2026-08-07 03:30:03 +02:00
KroeseandGitHub 438e992653 feat: Check available memory before installation starts (#1353) 2026-08-07 02:52:21 +02:00
KroeseandGitHub 615fad2a0d feat: Use dynamic PCI bus configuration (#1352) 2026-08-06 14:44:03 +02:00
KroeseandGitHub aac543e5c7 fix: Restrict minimal configured RAM amount to 1 GB (#1351) 2026-08-06 14:38:10 +02:00
KroeseandGitHub fec4f876e7 fix: Race between signal handler and cleanup (#1350) 2026-08-05 21:09:01 +02:00
KroeseandGitHub dced815c06 docs: Improve commenting (#1349) 2026-08-03 16:40:10 +02:00
KroeseandGitHub 51d6f3aeca fix: Reserve internal ports for user-mode forwarding (#1348) 2026-08-02 23:01:52 +02:00
KroeseandGitHub 4544620d48 feat: Increase indentation of printed QEMU arguments (#1347) 2026-08-01 05:49:50 +02:00
KroeseandGitHub de8468161f feat: Refactor negated command conditions (#1346) 2026-07-30 21:22:21 +02:00
renovate[bot]andGitHub 18d6be8210 chore(deps): update hadolint/hadolint-action action to v3.4.0 (#1345) 2026-07-30 21:06:27 +02:00
KroeseandGitHub e5b8cf3bf8 build: Update Passt to v2026_07_28 (#1344) 2026-07-30 10:10:41 +02:00
KroeseandGitHub ec12039f43 feat: Use deadline-based process timeouts (#1343) 2026-07-28 15:22:47 +02:00
KroeseandGitHub 3c7c3ca1b1 fix: Prevent race when reading PID files (#1342) 2026-07-28 14:22:08 +02:00
KroeseandGitHub 2b27f32cd4 fix: Disk options were applied to the controller (#1341) 2026-07-28 03:30:55 +02:00
KroeseandGitHub 55d1d50284 fix: Retry transient gateway errors in web status (#1340) 2026-07-27 23:49:22 +02:00
KroeseandGitHub 784b73b5b5 feat: Add DISK_OPTIONS support to disk devices (#1339) 2026-07-27 23:48:21 +02:00
KroeseandGitHub 85f00bb9cc docs: Added new DISK_OPTIONS variable (#1338) 2026-07-27 12:04:25 +02:00
KroeseandGitHub efe8732e47 fix: Avoid shadowing PID output variables (#1337) 2026-07-27 12:03:24 +02:00
KroeseandGitHub f6871dd61b fix: Clear invalid PID values in shared reader (#1336) 2026-07-26 17:52:14 +02:00
KroeseandGitHub a713b728ff fix: Use shared PID reader for helper processes (#1335) 2026-07-26 17:41:46 +02:00
KroeseandGitHub 0d74c6ac80 build: Update dependabot config (#1334) 2026-07-26 00:58:36 +02:00
KroeseandGitHub 63e4529eff fix: Add cleanup for failed web server startup (#1333) 2026-07-25 13:47:20 +02:00
KroeseandGitHub afd848cebb fix: Make healthcheck succeed during download (#1332) 2026-07-25 12:16:41 +02:00
KroeseandGitHub 695b075130 feat: Provide host access through system.lan (#1331) 2026-07-25 03:38:36 +02:00
KroeseandGitHub 7eff53e722 feat: Improve disk error handling (#1328) 2026-07-24 13:51:59 +02:00
KroeseandGitHub bcaf0e5980 fix: Improve network error handling (#1327) 2026-07-24 13:27:09 +02:00
KroeseandGitHub faa820c2cc feat: Inline local variable declarations (#1326) 2026-07-24 13:11:40 +02:00
KroeseandGitHub 81e477fcc4 feat: Warn when DSM and container share an IP address (#1325) 2026-07-24 12:45:54 +02:00
KroeseandGitHub abd3a1c3df fix: Network mode shown for DHCP mode (#1324) 2026-07-24 12:03:57 +02:00
KroeseandGitHub 3b59bcd284 fix: Ensure progress reaches 100% (#1322) 2026-07-23 23:03:54 +02:00
KroeseandGitHub f1b56f394a build: Create a detailed .gitignore file (#1321) 2026-07-23 15:42:59 +02:00
KroeseandGitHub 1a983ebcd1 feat: Improve error handling for webserver config (#1320) 2026-07-23 04:17:26 +02:00
KroeseandGitHub 41c76198fa feat: Use atomic writes for progress updates (#1319) 2026-07-23 04:10:09 +02:00
KroeseandGitHub 222b5649b0 fix: Prevent stale polling responses for web status (#1318) 2026-07-23 04:00:12 +02:00
KroeseandGitHub e14fd4b711 docs: Environment variables (#1317) 2026-07-21 13:14:32 +02:00
KroeseandGitHub 6fe19a8af4 feat: Improve download progress reporting (#1316) 2026-07-21 06:00:58 +02:00
KroeseandGitHub 072b5c3070 build: Update workflow (#1315) 2026-07-21 01:31:29 +02:00
KroeseandGitHub 89dbaeb2a4 feat: Improve download progress reporting (#1314) 2026-07-21 00:13:53 +02:00
KroeseandGitHub 02f76b44cd fix: Resolve shellcheck error (#1313) 2026-07-20 17:00:32 +02:00
KroeseandGitHub 99d6b8f830 feat: Preserve graceful shutdown for interactive console (#1312) 2026-07-20 16:31:21 +02:00
KroeseandGitHub e6b68b2f08 fix: Avoid shellcheck warning (#1311) 2026-07-20 14:32:03 +02:00
KroeseandGitHub 7a8f0041e7 build: Update workflow (#1310) 2026-07-20 12:31:13 +02:00
KroeseandGitHub 64809bb4d4 feat: Support progress tracking for segmented downloads (#1309) 2026-07-20 05:48:16 +02:00
KroeseandGitHub f08b2cbd99 fix: Decrease default timeout (#1307) 2026-07-19 09:17:51 +02:00
KroeseandGitHub 18d9ecd434 fix: Decrease default timeout (#1308) 2026-07-19 08:57:09 +02:00
KroeseandGitHub 6944293a32 fix: Add Recreate strategy to Kubernetes deployment (#1306) 2026-07-19 03:15:10 +02:00
KroeseandGitHub d807a365ff feat: Added optional lossy VNC compression (#1305) 2026-07-19 03:09:35 +02:00
KroeseandGitHub 066af83d07 fix: Decrease default timeout (#1304) 2026-07-19 01:03:21 +02:00
KroeseandGitHub 81536818df feat: Add escapeHTML function (#1303) 2026-07-18 22:00:45 +02:00
KroeseandGitHub 949c288565 fix: Set correct MTU option for dnsmasq (#1302) 2026-07-18 20:40:52 +02:00
KroeseandGitHub 5d5bbbcf4d feat: Improve NAT networking (#1300) 2026-07-18 15:34:46 +02:00
KroeseandGitHub 82a62c0240 build: Update workflow (#1301) 2026-07-18 15:32:07 +02:00
KroeseandGitHub 5399e1d463 feat: Use Unix sockets for qemu-host communication (#1299) 2026-07-18 11:31:20 +02:00
KroeseandGitHub c7e7b60f01 fix: Improve IP tables cleanup verification (#1298) 2026-07-18 10:54:20 +02:00
KroeseandGitHub f1defa6890 docs: Environment variables (#1297) 2026-07-18 00:28:24 +02:00
KroeseandGitHub a7fb161974 feat: Improve download progress readability (#1296) 2026-07-17 22:59:07 +02:00
KroeseandGitHub dcdf5e5293 fix: Improve interactive terminal detection (#1295) 2026-07-17 22:45:07 +02:00
KroeseandGitHub 1eb0db66ba feat: Improve download progress in container logs (#1294) 2026-07-17 22:29:10 +02:00
KroeseandGitHub 3eec8e03ed feat: Improve AMD detection (#1293) 2026-07-17 20:54:41 +02:00
KroeseandGitHub 5483981ed4 feat: Refactor config code (#1292) 2026-07-17 20:37:54 +02:00
KroeseandGitHub 62330a5bf4 fix: Cleanup stale pipe files (#1291) 2026-07-17 20:01:53 +02:00
KroeseandGitHub 2b0b59dfba feat: Preserve graceful shutdown for interactive console (#1290) 2026-07-17 19:56:05 +02:00
KroeseandGitHub b680ff9dd8 feat: Preserve graceful shutdown for interactive console (#1289) 2026-07-17 19:16:30 +02:00
KroeseandGitHub 5c889b272d fix: Store pid for serial debugging (#1288) 2026-07-17 14:44:49 +02:00
KroeseandGitHub 585ebb3f53 build: Update Passt to v2026_07_16 (#1287) 2026-07-17 13:49:26 +02:00
KroeseandGitHub 401307b981 build: Update QEMU host to v2.06 (#1286) 2026-07-17 13:24:34 +02:00
KroeseandGitHub 46820fe6eb build: Set dependabot cooldown period (#1284) 2026-07-17 13:20:19 +02:00
KroeseandGitHub 9452d48694 feat: Make machine type configurable (#1283) 2026-07-17 11:33:05 +02:00
KroeseandGitHub 5c3fed1387 fix: Clear service logs on startup (#1281) 2026-07-17 06:26:25 +02:00
KroeseandGitHub 797411ae7f fix: Show QEMU errors on unexpected exit (#1280) 2026-07-17 05:54:04 +02:00
KroeseandGitHub e81c509208 feat: Check RENDERNODE permissions (#1279) 2026-07-17 02:11:38 +02:00
KroeseandGitHub ace8614a50 feat: Refactor disk code (#1278) 2026-07-17 01:52:47 +02:00
KroeseandGitHub 8263b3a737 feat: Improve iptable backend selection (#1277) 2026-07-17 01:51:39 +02:00
KroeseandGitHub 61c6142988 feat: Remove unused functions (#1276) 2026-07-17 00:10:59 +02:00
KroeseandGitHub 5d26338bb3 feat: Improve detection of valid data disk (#1275) 2026-07-16 21:41:29 +02:00
KroeseandGitHub 084d475cde build: Add diffutils package (#1274) 2026-07-16 20:55:47 +02:00
KroeseandGitHub c36b0dcb00 docs: Environment variables (#1273) 2026-07-16 20:50:17 +02:00
KroeseandGitHub 6e9993742b feat: Limit wget progress output (#1272) 2026-07-15 23:01:39 +02:00
KroeseandGitHub 6946af0212 build: Update workflow (#1271) 2026-07-15 21:55:07 +02:00
KroeseandGitHub 0a9361d62f fix: Localize available memory formatting (#1270) 2026-07-15 21:13:51 +02:00
KroeseandGitHub 7cbfcd44a6 fix: Remove unnecessary guards (#1269) 2026-07-15 21:00:50 +02:00
28 changed files with 2959 additions and 1133 deletions
+11
View File
@@ -1,10 +1,21 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: docker - package-ecosystem: docker
directory: / directory: /
schedule: schedule:
interval: weekly interval: weekly
cooldown:
default-days: 7
- package-ecosystem: github-actions - package-ecosystem: github-actions
directory: / directory: /
schedule: schedule:
interval: weekly interval: weekly
cooldown:
default-days: 7
ignore:
- dependency-name: "*"
update-types:
- version-update:semver-minor
- version-update:semver-patch
+27 -20
View File
@@ -1,5 +1,7 @@
on: [workflow_call] on: [workflow_call]
name: "Check" name: "Check"
permissions: {} permissions: {}
jobs: jobs:
@@ -7,24 +9,29 @@ jobs:
name: shellcheck name: shellcheck
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
-
name: Checkout
uses: actions/checkout@v7
- parallel:
- -
name: Run ShellCheck name: Checkout
uses: ludeeus/action-shellcheck@master uses: actions/checkout@v7
env: - parallel:
SHELLCHECK_OPTS: -x --source-path=src -e SC2001 -e SC2034 -e SC2064 -e SC2317 -e SC2153 -e SC2028 -
- name: Run ShellCheck
name: Lint Dockerfile uses: ludeeus/action-shellcheck@master
uses: hadolint/hadolint-action@v3.3.0 env:
with: SHELLCHECK_OPTS: >-
dockerfile: Dockerfile -x
ignore: DL3008,DL3003,DL3006,DL3013 --source-path=src
failure-threshold: warning -e SC1091
- -e SC2001
name: Validate JSON and YML files -e SC2034
uses: GrantBirki/json-yaml-validate@v5.0.0 -e SC2317
with: -e SC2153
yaml_exclude_regex: ".*\\kubernetes\\.yml$" -
name: Lint Dockerfile
uses: hadolint/hadolint-action@v3.4.0
with:
dockerfile: Dockerfile
ignore: DL3008
failure-threshold: warning
-
name: Validate JSON and YML files
uses: GrantBirki/json-yaml-validate@v5
+1 -58
View File
@@ -11,61 +11,4 @@ permissions:
jobs: jobs:
review: review:
name: review name: review
runs-on: ubuntu-latest uses: action-pack/.github/.github/workflows/review.yml@master
steps:
-
name: Checkout
uses: actions/checkout@v7
- parallel:
-
name: Spelling
uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: warning
pattern: |
*.md
*.sh
reporter: github-pr-review
github_token: ${{ secrets.GITHUB_TOKEN }}
-
name: Hadolint
uses: reviewdog/action-hadolint@v1
with:
level: warning
fail_level: error
reporter: github-pr-review
hadolint_ignore: DL3008 DL3003 DL3006 DL3013
github_token: ${{ secrets.GITHUB_TOKEN }}
-
name: YamlLint
uses: reviewdog/action-yamllint@v1
with:
level: warning
reporter: github-pr-review
github_token: ${{ secrets.GITHUB_TOKEN }}
-
name: ActionLint
uses: reviewdog/action-actionlint@v1
with:
level: warning
reporter: github-pr-review
github_token: ${{ secrets.GITHUB_TOKEN }}
-
name: Shellformat
uses: reviewdog/action-shfmt@v1
if: false
with:
level: warning
fail_on_error: "true"
shfmt_flags: "-i 2 -ci -bn"
github_token: ${{ secrets.GITHUB_TOKEN }}
-
name: Shellcheck
uses: reviewdog/action-shellcheck@v1
with:
level: warning
fail_level: error
reporter: github-pr-review
shellcheck_flags: -x -e SC1091 -e SC2001 -e SC2034 -e SC2064 -e SC2317 -e SC2153 -e SC2028
github_token: ${{ secrets.GITHUB_TOKEN }}
+268 -1
View File
@@ -1 +1,268 @@
build.sh ##############################
# Operating System Files
##############################
.DS_Store
.AppleDouble
.LSOverride
Thumbs.db
ehthumbs.db
Desktop.ini
Icon?
$RECYCLE.BIN/
.Spotlight-V100/
.Trashes/
.fseventsd
##############################
# IDEs
##############################
.vscode/
.idea/
*.iml
*.ipr
*.iws
##############################
# VS Code
##############################
.history/
*.code-workspace
##############################
# Vim
##############################
*.swp
*.swo
Session.vim
##############################
# Sublime
##############################
*.sublime-workspace
*.sublime-project
##############################
# Temporary Files
##############################
*.tmp
*.temp
*.bak
*.old
*.orig
*.rej
*.save
##############################
# Logs
##############################
*.log
logs/
log/
*.out
*.err
*.trace
##############################
# Runtime
##############################
*.pid
*.seed
*.pid.lock
##############################
# Secrets
##############################
.env
.env.*
!.env.example
*.pem
*.key
*.crt
*.cer
*.p12
*.pfx
*.kdbx
*.secret
*.token
##############################
# SSH
##############################
.ssh/
##############################
# Docker
##############################
docker-compose.override.yml
docker-compose.local.yml
##############################
# VM Storage
##############################
storage/
windows/
downloads/
##############################
# Disk Images
##############################
*.qcow2
*.qcow
*.vhd
*.vhdx
*.vdi
*.raw
*.img
*.iso
*.bin
##############################
# QEMU
##############################
*.nvram
*.fd
*.efi
*.sock
*.monitor
*.serial
##############################
# Samba
##############################
shared/
share/
##############################
# Backups
##############################
backup/
backups/
*.backup
##############################
# Cache
##############################
.cache/
.cache-loader/
.tmp/
temp/
tmp/
##############################
# Python
##############################
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.venv/
venv/
##############################
# Node
##############################
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
##############################
# Build
##############################
dist/
build/
out/
release/
##############################
# Coverage
##############################
coverage/
.coverage
coverage.xml
##############################
# Archives
##############################
*.zip
*.tar
*.tar.gz
*.tgz
*.7z
*.rar
##############################
# Generated Config
##############################
config.local.*
settings.local.*
local.env
##############################
# Test Files
##############################
test-output/
playwright-report/
##############################
# macOS
##############################
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
##############################
# Linux
##############################
*~
.nfs*
##############################
# Windows
##############################
*.stackdump
##############################
# Misc
##############################
*.cache
*.lock
*.lock.json
*.bak.*
##############################
# Keep Examples
##############################
!.gitkeep
!.env.example
+16 -17
View File
@@ -1,12 +1,15 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM qemux/qemu-host:2.05 AS builder FROM qemux/qemu-host:2.06 AS builder
FROM debian:trixie-slim FROM debian:trixie-slim
ARG TARGETARCH ARG TARGETARCH
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG VERSION_ARG="0.0" ARG VERSION_ARG="0.0"
ARG VERSION_PASST="2026_06_11" ARG VERSION_WSD="0.4.2"
ARG VERSION_CSTRUCT="4.7"
ARG VERSION_PASST="2026_07_28"
ARG DEBCONF_NOWARNINGS="yes" ARG DEBCONF_NOWARNINGS="yes"
ARG DEBIAN_FRONTEND="noninteractive" ARG DEBIAN_FRONTEND="noninteractive"
@@ -27,10 +30,6 @@ RUN <<EOF
procps \ procps \
ipcalc \ ipcalc \
ethtool \ ethtool \
python3 \
python3-pip \
python3-msgpack \
python3-pysodium \
xz-utils \ xz-utils \
iptables \ iptables \
iproute2 \ iproute2 \
@@ -39,30 +38,30 @@ RUN <<EOF
apt-utils \ apt-utils \
net-tools \ net-tools \
e2fsprogs \ e2fsprogs \
diffutils \
qemu-utils \ qemu-utils \
websocketd \
iputils-ping \ iputils-ping \
inotify-tools \ inotify-tools \
ca-certificates \ ca-certificates \
netcat-openbsd \ netcat-openbsd \
qemu-system-x86 qemu-system-x86 \
python3 \
python3-pip \
python3-msgpack \
python3-pysodium
# Install Passt package # Install Passt package
wget "https://github.com/qemus/passt/releases/download/v${VERSION_PASST}/passt_${VERSION_PASST}_${TARGETARCH}.deb" -O /tmp/passt.deb -q --timeout=10 wget "https://github.com/qemus/passt/releases/download/v${VERSION_PASST}/passt_${VERSION_PASST}_${TARGETARCH}.deb" -O /tmp/passt.deb -q --timeout=10
dpkg -i /tmp/passt.deb dpkg -i /tmp/passt.deb
# Install Websocketd package
wget "https://github.com/qemus/websocketd/releases/download/v${VERSION_WSD}/websocketd-${VERSION_WSD}_${TARGETARCH}.deb" -O /tmp/wsd.deb -q --timeout=10
dpkg -i /tmp/wsd.deb
apt-get clean apt-get clean
# Install Python dependencies # Install Python dependencies
pip3 install --no-cache-dir --break-system-packages --root-user-action=ignore dissect.cstruct pip3 install --no-cache-dir --break-system-packages --root-user-action=ignore "dissect.cstruct==$VERSION_CSTRUCT"
# Configure QEMU
mkdir -p /etc/qemu
echo "allow br0" > /etc/qemu/bridge.conf
# Configure nginx
unlink /etc/nginx/sites-enabled/default
sed -i 's/^worker_processes.*/worker_processes 1;/' /etc/nginx/nginx.conf
# Set version file # Set version file
echo "$VERSION_ARG" > /etc/version echo "$VERSION_ARG" > /etc/version
+15 -8
View File
@@ -8,7 +8,8 @@ An empty default means the variable is unset and its value is determined automat
| Variable | Default | Description | | Variable | Default | Description |
|---|---|---| |---|---|---|
| `URL` | | URL or local path to the DSM `.pat` installation file. Downloads the default Virtual DSM image automatically when unset. | | `URL` | | URL or local path to a custom `.pat` installation file. |
| `COUNTRY` | | Country code used to select the Synology download mirror. |
| `HOST_MAC` | | MAC address reported to DSM. | | `HOST_MAC` | | MAC address reported to DSM. |
| `HOST_MODEL` | | Synology host model reported to DSM. | | `HOST_MODEL` | | Synology host model reported to DSM. |
| `HOST_SERIAL` | | Synology host serial number reported to DSM. | | `HOST_SERIAL` | | Synology host serial number reported to DSM. |
@@ -38,6 +39,7 @@ An empty default means the variable is unset and its value is determined automat
| `DISK_DISCARD` | `unmap` | Discard/TRIM mode for the primary disk. | | `DISK_DISCARD` | `unmap` | Discard/TRIM mode for the primary disk. |
| `DISK_ROTATION` | `1` | Rotation rate reported to the guest. Use `1` to identify the disk as an SSD. | | `DISK_ROTATION` | `1` | Rotation rate reported to the guest. Use `1` to identify the disk as an SSD. |
| `DISK_FLAGS` | | Additional options used when creating `qcow2` disks. | | `DISK_FLAGS` | | Additional options used when creating `qcow2` disks. |
| `DISK_OPTIONS` | | Additional options appended to QEMU disk devices. |
| `ALLOCATE` | `N` | Preallocates space for the data disks. | | `ALLOCATE` | `N` | Preallocates space for the data disks. |
| `STORAGE` | `/storage` | Storage directory used for disks, settings, and downloads. | | `STORAGE` | `/storage` | Storage directory used for disks, settings, and downloads. |
@@ -45,15 +47,15 @@ An empty default means the variable is unset and its value is determined automat
| Variable | Default | Description | | Variable | Default | Description |
|---|---|---| |---|---|---|
| `NETWORK` | | Network mode, such as `nat`, `passt`, `slirp`, or `N` to disable networking. | | `NETWORK` | | Network mode, such as `nat`, `user`, or `N` to disable networking. |
| `DHCP` | `N` | Enables macvtap networking so DSM receives an address from the external LAN through DHCP. | | `DHCP` | `N` | Enables macvtap networking so DSM receives a DHCP address. |
| `HOST` | `VirtualDSM` | Hostname assigned to DSM. | | `HOST` | | Hostname assigned to the machine on the network. |
| `IP` | | Overrides the automatically selected guest IPv4 address. | | `IP` | | Overrides the automatically selected guest IPv4 address. |
| `MAC` | | Guest network adapter MAC address. | | `MAC` | | Guest network adapter MAC address. |
| `ADAPTER` | `virtio-net-pci` | QEMU network adapter model. | | `ADAPTER` | `virtio-net-pci` | QEMU network adapter model. |
| `DEV` | `eth0` | Container network interface used as the uplink. | | `DEV` | `eth0` | Container network interface used as the uplink. |
| `MTU` | | MTU assigned to the guest network interface. | | `MTU` | | MTU assigned to the guest network interface. |
| `MASK` | `255.255.255.0` | IPv4 netmask. | | `MASK` | `255.255.255.0` | IPv4 netmask for guest network. |
| `TAP` | `dsm` | TAP or macvtap interface name. | | `TAP` | `dsm` | TAP or macvtap interface name. |
| `BRIDGE` | `docker` | Bridge name used for NAT networking. | | `BRIDGE` | `docker` | Bridge name used for NAT networking. |
| `HOST_PORTS` | | Ports excluded from guest forwarding. | | `HOST_PORTS` | | Ports excluded from guest forwarding. |
@@ -69,10 +71,18 @@ An empty default means the variable is unset and its value is determined automat
| Variable | Default | Description | | Variable | Default | Description |
|---|---|---| |---|---|---|
| `DISPLAY` | `none` | Display backend, such as `vnc`, `disabled`, or `none`. | | `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. | | `VGA` | `none` | QEMU video adapter model. |
| `GPU` | `N` | Enables Intel iGPU acceleration. | | `GPU` | `N` | Enables Intel iGPU acceleration. |
| `RENDERNODE` | `/dev/dri/renderD128` | Render node used for GPU acceleration. | | `RENDERNODE` | `/dev/dri/renderD128` | Render node used for GPU acceleration. |
## ⚙️ System
| Variable | Default | Description |
|---|---|---|
| `MACHINE` | `q35` | QEMU machine type. |
| `ARGUMENTS` | | Additional raw arguments appended to the QEMU command line. |
## 🔌 Shutdown ## 🔌 Shutdown
| Variable | Default | Description | | Variable | Default | Description |
@@ -87,7 +97,4 @@ An empty default means the variable is unset and its value is determined automat
|---|---|---| |---|---|---|
| `DEBUG` | `N` | Enables verbose debug output. | | `DEBUG` | `N` | Enables verbose debug output. |
| `TRACE` | `N` | Enables shell command tracing. | | `TRACE` | `N` | Enables shell command tracing. |
| `COM_PORT` | `2210` | Internal communication port used by the DSM host helper. |
| `CHR_PORT` | `12345` | Internal character device port used by the DSM host helper. |
| `HOST_DEBUG` | `N` | Enables debug output for the DSM host helper. | | `HOST_DEBUG` | `N` | Enables debug output for the DSM host helper. |
| `ARGUMENTS` | | Additional raw arguments appended to the QEMU command line. |
+2
View File
@@ -18,6 +18,8 @@ metadata:
name: dsm name: dsm
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app: dsm app: dsm
+2 -2
View File
@@ -68,8 +68,8 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
- Docker or Podman on a Linux host with KVM support. - Docker or Podman on a Linux host with KVM support.
- Docker Desktop or Podman (Desktop) on Windows 11 with nested virtualization enabled. - Docker Desktop or Podman (Desktop) on Windows 11 with nested virtualization enabled.
- At least 2 GB of available RAM. - At least 1 GB of available RAM.
- At least 32 GB of free disk space. - At least 16 GB of free disk space.
> [!NOTE] > [!NOTE]
> Docker Desktop on Linux, macOS, and Windows 10 does not currently provide KVM access to containers and is therefore not supported. > Docker Desktop on Linux, macOS, and Windows 10 does not currently provide KVM access to containers and is therefore not supported.
+5 -1
View File
@@ -15,12 +15,16 @@ file="/run/shm/dsm.url"
address="/run/shm/qemu.ip" address="/run/shm/qemu.ip"
gateway="/run/shm/qemu.gw" gateway="/run/shm/qemu.gw"
[ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 1 # dsm.url is written only after the guest agent reports both the DSM
# address and its configured HTTP port.
[ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 0
location=$(<"$file") location=$(<"$file")
if ! curl -m 20 -ILfSs "http://$location/" > /dev/null; then if ! curl -m 20 -ILfSs "http://$location/" > /dev/null; then
# In DHCP mode the firewall must allow the container address; with port
# forwarding it must allow the internal gateway used to reach the guest.
if enabled "$DHCP"; then if enabled "$DHCP"; then
ip=$(<"$address") ip=$(<"$address")
echo "Failed to reach DSM at http://$location" echo "Failed to reach DSM at http://$location"
+67 -9
View File
@@ -2,15 +2,73 @@
set -Eeuo pipefail set -Eeuo pipefail
DEF_OPTS="-nodefaults -boot strict=on" DEF_OPTS="-nodefaults -boot strict=on"
RAM_OPTS=$(echo "-m ${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g') DEV_OPTS=""
MON_OPTS="-name $PROCESS,process=$PROCESS,debug-threads=on -pidfile $QEMU_PID"
CPU_OPTS="-cpu $CPU_FLAGS -smp $CPU_CORES,sockets=1,dies=1,cores=$CPU_CORES,threads=1"
MAC_OPTS="-machine type=q35,smm=off,usb=off,vmport=off,dump-guest-core=off,hpet=off${KVM_OPTS}"
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"
ARGS="$DEF_OPTS $CPU_OPTS $RAM_OPTS $MAC_OPTS $DISPLAY_OPTS $MON_OPTS $SERIAL_OPTS $NET_OPTS $DISK_OPTS $DEV_OPTS $ARGUMENTS" configureProcessor() {
ARGS=$(echo "$ARGS" | sed 's/\t/ /g' | tr -s ' ')
# 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"
return 0
}
configureMemory() {
RAM_OPTS=$(echo "-m ${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
return 0
}
configureMonitor() {
MON_OPTS="-name $PROCESS,process=$PROCESS,debug-threads=on"
MON_OPTS+=" -pidfile $QEMU_PID"
return 0
}
configureMachine() {
# Disable firmware and chipset features that Virtual DSM does not use and
# that can introduce extra devices or timing differences.
MAC_OPTS="-machine type=$MACHINE,smm=off,usb=off"
MAC_OPTS+=",vmport=off,dump-guest-core=off,hpet=off${KVM_OPTS}"
return 0
}
configureVirtioDevices() {
local bus
bus=$(getPciBus)
DEV_OPTS="-device virtio-balloon-pci,id=balloon0,bus=$bus,addr=0x4"
DEV_OPTS+=" -object rng-random,id=objrng0,filename=/dev/urandom"
DEV_OPTS+=" -device virtio-rng-pci,rng=objrng0,id=rng0,bus=$bus,addr=0x1c"
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"
# 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
buildArguments
return 0 return 0
+373 -302
View File
File diff suppressed because it is too large Load Diff
+22 -3
View File
@@ -6,19 +6,30 @@ set -Eeuo pipefail
: "${GPU:="N"}" # GPU passthrough : "${GPU:="N"}" # GPU passthrough
: "${VGA:="virtio"}" # VGA adaptor : "${VGA:="virtio"}" # VGA adaptor
: "${DISPLAY:="none"}" # Display type : "${DISPLAY:="none"}" # Display type
: "${LOSSY:="N"}" # Lossy VNC compression
: "${RENDERNODE:="/dev/dri/renderD128"}" # Render node : "${RENDERNODE:="/dev/dri/renderD128"}" # Render node
# Sanitize variables # Sanitize variables
VGA=$(strip "$VGA") VGA=$(strip "$VGA")
LOSSY=$(strip "$LOSSY")
DISPLAY=$(strip "$DISPLAY") DISPLAY=$(strip "$DISPLAY")
RENDERNODE=$(strip "$RENDERNODE") RENDERNODE=$(strip "$RENDERNODE")
CPU_VENDOR=$(lscpu | awk '/Vendor ID/{print $3}') CPU_VENDOR=$(lscpu | awk '/Vendor ID/{print $3}')
if ! enabled "$GPU" || [[ "$CPU_VENDOR" != "GenuineIntel" || "$ARCH" != "amd64" ]]; then # The accelerated Intel render-node path is restricted to x86 Intel hosts;
# other platforms retain the normal QEMU display backend.
if ! enabled "$GPU" || isAmdCpu || [[ "$ARCH" != "amd64" ]]; then
# A disabled frontend also removes the emulated VGA device to keep the guest
# hardware layout headless.
[[ "${DISPLAY,,}" == "none" ]] && VGA="none" [[ "${DISPLAY,,}" == "none" ]] && VGA="none"
DISPLAY_OPTS="-display $DISPLAY -vga $VGA"
if enabled "$LOSSY" && [[ "${DISPLAY,,}" == vnc=* ]]; then
DISPLAY+=",lossy=on"
fi
DISPLAY_OPTS="-display ${DISPLAY} -vga ${VGA}"
return 0 return 0
fi fi
@@ -27,12 +38,14 @@ msg="Configuring display drivers..."
html "$msg" html "$msg"
enabled "$DEBUG" && echo "$msg" enabled "$DEBUG" && echo "$msg"
DISPLAY_OPTS="-display egl-headless,rendernode=$RENDERNODE" DISPLAY_OPTS="-display egl-headless,rendernode=${RENDERNODE}"
DISPLAY_OPTS+=" -vga $VGA" DISPLAY_OPTS+=" -vga $VGA"
[ ! -d /dev/dri ] && mkdir -m 755 /dev/dri [ ! -d /dev/dri ] && mkdir -m 755 /dev/dri
# Extract the card number from the render node # Extract the card number from the render node
# Linux renderD128 corresponds to card0; derive both device minors because
# container device bindings may expose only the render node.
CARD_NUMBER=$(echo "$RENDERNODE" | grep -oP '(?<=renderD)\d+') CARD_NUMBER=$(echo "$RENDERNODE" | grep -oP '(?<=renderD)\d+')
CARD_DEVICE="/dev/dri/card$((CARD_NUMBER - 128))" CARD_DEVICE="/dev/dri/card$((CARD_NUMBER - 128))"
@@ -48,6 +61,12 @@ if [ ! -c "$RENDERNODE" ]; then
fi fi
fi fi
if [ ! -c "$RENDERNODE" ] || [ ! -r "$RENDERNODE" ] || [ ! -w "$RENDERNODE" ]; then
warn "render device '${RENDERNODE}' is unavailable or inaccessible."
fi
# Install acceleration packages lazily so non-GPU deployments keep the base
# image small and do not require OpenGL modules.
addPackage "xserver-xorg-video-intel" "Intel GPU drivers" addPackage "xserver-xorg-video-intel" "Intel GPU drivers"
addPackage "qemu-system-modules-opengl" "OpenGL module" addPackage "qemu-system-modules-opengl" "OpenGL module"
+8 -5
View File
@@ -9,7 +9,8 @@ cd /run
. start.sh # Startup hook . start.sh # Startup hook
. utils.sh # Load functions . utils.sh # Load functions
. reset.sh # Initialize system . init.sh # Initialize system
. memory.sh # Check memory
. server.sh # Start webserver . server.sh # Start webserver
. install.sh # Run installation . install.sh # Run installation
. disk.sh # Initialize disks . disk.sh # Initialize disks
@@ -18,7 +19,6 @@ cd /run
. proc.sh # Initialize processor . proc.sh # Initialize processor
. serial.sh # Initialize serialport . serial.sh # Initialize serialport
. power.sh # Configure shutdown . power.sh # Configure shutdown
. memory.sh # Check available memory
. config.sh # Configure arguments . config.sh # Configure arguments
. finish.sh # Finish initialization . finish.sh # Finish initialization
@@ -32,14 +32,17 @@ if ! enabled "$SHUTDOWN"; then
exec "${cmd[@]}" ${ARGS:+ $ARGS} exec "${cmd[@]}" ${ARGS:+ $ARGS}
fi fi
if [ ! -t 1 ] || [ ! -c /dev/tty ]; then if ! interactive; then
"${cmd[@]}" ${ARGS:+ $ARGS} & "${cmd[@]}" ${ARGS:+ $ARGS} &
else else
"${cmd[@]}" ${ARGS:+ $ARGS} </dev/tty >/dev/tty & startConsole
startQemu "${cmd[@]}" ${ARGS:+ $ARGS}
fi fi
pid=$!
rc=0 rc=0
wait $! || rc=$?
wait "$pid" || rc=$?
[ -f "$QEMU_END" ] && exit "$rc" [ -f "$QEMU_END" ] && exit "$rc"
sleep 1 & wait $! sleep 1 & wait $!
+4 -1
View File
@@ -2,7 +2,10 @@
set -Eeuo pipefail set -Eeuo pipefail
if enabled "$DEBUG"; then if enabled "$DEBUG"; then
printf "QEMU arguments:\n\n%s\n\n" "${ARGS// -/$'\n-'}" printf "QEMU arguments:\n\n %s\n\n" "${ARGS// -/$'\n -'}"
fi fi
# Must always remain the very last command
enableTrap
return 0 return 0
+43 -109
View File
@@ -13,6 +13,7 @@ enabled "${TRACE:-}" && set -o functrace && trap 'echo "# $BASH_COMMAND" >&2' DE
: "${KVM:="Y"}" # KVM acceleration : "${KVM:="Y"}" # KVM acceleration
: "${DEBUG:="N"}" # Disable debugging mode : "${DEBUG:="N"}" # Disable debugging mode
: "${COUNTRY:=""}" # Country code for mirror : "${COUNTRY:=""}" # Country code for mirror
: "${MACHINE:="q35"}" # Machine type selection
: "${ALLOCATE:=""}" # Preallocate diskspace : "${ALLOCATE:=""}" # Preallocate diskspace
: "${ARGUMENTS:=""}" # Extra QEMU parameters : "${ARGUMENTS:=""}" # Extra QEMU parameters
: "${CPU_CORES:="2"}" # Amount of CPU cores : "${CPU_CORES:="2"}" # Amount of CPU cores
@@ -40,8 +41,10 @@ detectEngine() {
detectRootless() { detectRootless() {
local uid_map="" local uid_map
# A full identity UID map indicates a rootful container; any remapping is
# treated as rootless even though the process itself runs as UID 0.
uid_map=$(awk '{$1=$1; print}' /proc/self/uid_map 2>/dev/null || true) uid_map=$(awk '{$1=$1; print}' /proc/self/uid_map 2>/dev/null || true)
if [[ "$uid_map" == "0 0 4294967295" ]]; then if [[ "$uid_map" == "0 0 4294967295" ]]; then
@@ -57,8 +60,6 @@ checkPrivileged() {
local cap_bnd local cap_bnd
local last_cap local last_cap
local max_cap
# Get the capability bounding set # Get the capability bounding set
cap_bnd=$(grep '^CapBnd:' /proc/$$/status | awk '{print $2}') cap_bnd=$(grep '^CapBnd:' /proc/$$/status | awk '{print $2}')
cap_bnd=$(printf "%d" "0x${cap_bnd}") cap_bnd=$(printf "%d" "0x${cap_bnd}")
@@ -67,7 +68,9 @@ checkPrivileged() {
last_cap=$(cat /proc/sys/kernel/cap_last_cap) last_cap=$(cat /proc/sys/kernel/cap_last_cap)
# Calculate the maximum capability value # Calculate the maximum capability value
max_cap=$(((1 << (last_cap + 1)) - 1)) # Compare the bounding set with every capability supported by this kernel;
# checking only a few known capabilities would misclassify newer kernels.
local max_cap=$(((1 << (last_cap + 1)) - 1))
if [ "$cap_bnd" -eq "$max_cap" ]; then if [ "$cap_bnd" -eq "$max_cap" ]; then
PRIVILEGED="Y" PRIVILEGED="Y"
@@ -76,15 +79,15 @@ checkPrivileged() {
return 0 return 0
} }
normalizeCpuCores() { checkCores() {
CPU_CORES=$(strip "$CPU_CORES") CPU_CORES=$(strip "$CPU_CORES")
[ -z "$CPU_CORES" ] && CPU_CORES=2 [ -z "$CPU_CORES" ] && CPU_CORES=2
[[ "${CPU_CORES,,}" == "max" ]] && CPU_CORES="$CORES" [[ "${CPU_CORES,,}" == "max" ]] && CPU_CORES="$CORES"
[[ "${CPU_CORES,,}" == "half" ]] && CPU_CORES=$(( CORES / 2 )) [[ "${CPU_CORES,,}" == "half" ]] && CPU_CORES=$(( CORES / 2 ))
[ -z "${CPU_CORES##*[!0-9]*}" ] && error "Invalid amount of CPU_CORES: $CPU_CORES" && exit 15 [ -z "${CPU_CORES##*[!0-9]*}" ] && error "Invalid amount of CPU_CORES: $CPU_CORES" && exit 15
[ "$CPU_CORES" -lt "1" ] && CPU_CORES=1 [ "$CPU_CORES" -lt "1" ] && CPU_CORES=1
if [ "$CPU_CORES" -gt "$CORES" ]; then if [ "$CPU_CORES" -gt "$CORES" ]; then
warn "The amount for CPU_CORES (${CPU_CORES}) exceeds the amount of logical cores available (${CORES}) and will be limited." warn "The amount for CPU_CORES (${CPU_CORES}) exceeds the amount of logical cores available (${CORES}) and will be limited."
CPU_CORES="$CORES" CPU_CORES="$CORES"
@@ -93,6 +96,20 @@ normalizeCpuCores() {
return 0 return 0
} }
checkSockets() {
local lscpu_out
lscpu_out=$(lscpu 2>/dev/null || true)
if grep -qi "socket(s)" <<< "$lscpu_out"; then
SOCKETS=$(grep -m 1 -i 'socket(s)' <<< "$lscpu_out" | awk '{print $2}')
[ -z "${SOCKETS##*[!0-9]*}" ] && SOCKETS=1
[ "$SOCKETS" -lt "1" ] && SOCKETS=1
fi
return 0
}
checkStorage() { checkStorage() {
# Check system # Check system
@@ -102,6 +119,8 @@ checkStorage() {
if [ ! -d "/dev/shm" ]; then if [ ! -d "/dev/shm" ]; then
error "Directory /dev/shm not found!" && exit 14 error "Directory /dev/shm not found!" && exit 14
else else
# Keep runtime sockets and PID files on shared memory even on images where
# /run/shm is absent but /dev/shm is available.
[ ! -d "$QEMU_DIR" ] && ln -s /dev/shm "$QEMU_DIR" [ ! -d "$QEMU_DIR" ] && ln -s /dev/shm "$QEMU_DIR"
fi fi
@@ -110,7 +129,9 @@ checkStorage() {
# Check folder # Check folder
if [[ "${STORAGE,,}" != "/storage" ]]; then if [[ "${STORAGE,,}" != "/storage" ]]; then
mkdir -p "$STORAGE" if ! mkdir -p -- "$STORAGE"; then
error "Cannot create storage folder ($STORAGE)!" && exit 13
fi
fi fi
if [ ! -d "$STORAGE" ]; then if [ ! -d "$STORAGE" ]; then
@@ -126,11 +147,9 @@ checkStorage() {
return 0 return 0
} }
checkFilesystem() { checkHost() {
# Check filesystem # Check filesystem
FS=$(stat -f -c %T "$STORAGE")
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then
DISK_IO="threads" DISK_IO="threads"
DISK_CACHE="writeback" DISK_CACHE="writeback"
@@ -139,91 +158,6 @@ checkFilesystem() {
return 0 return 0
} }
finiteMemoryLimit() {
local limit="$1"
local sentinel="4611686018427387904"
local i=0
local left=""
local right=""
[[ "$limit" =~ ^[0-9]+$ ]] || return 1
(( ${#limit} < ${#sentinel} )) && return 0
(( ${#limit} > ${#sentinel} )) && return 1
for (( i=0; i<${#sentinel}; i++ )); do
left="${limit:i:1}"
right="${sentinel:i:1}"
(( left < right )) && return 0
(( left > right )) && return 1
done
return 1
}
getMemoryInfo() {
local host_total=""
local host_avail=""
local limit=""
local current=""
local available=""
host_total=$(free -b | awk '/^Mem:/ {print $2; exit}')
host_avail=$(free -b | awk '/^Mem:/ {print $7; exit}')
RAM_TOTAL="$host_total"
RAM_AVAIL="$host_avail"
if [ -r /sys/fs/cgroup/memory.max ] && [ -r /sys/fs/cgroup/memory.current ]; then
limit=$(< /sys/fs/cgroup/memory.max)
current=$(< /sys/fs/cgroup/memory.current)
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ] && [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then
limit=$(< /sys/fs/cgroup/memory/memory.limit_in_bytes)
current=$(< /sys/fs/cgroup/memory/memory.usage_in_bytes)
fi
if finiteMemoryLimit "$limit" && [[ "$current" =~ ^[0-9]+$ ]]; then
(( limit < RAM_TOTAL )) && RAM_TOTAL="$limit"
available=$(( limit - current ))
(( available < 0 )) && available=0
(( available < RAM_AVAIL )) && RAM_AVAIL="$available"
fi
return 0
}
normalizeRamSize() {
# Read host and container memory limits.
getMemoryInfo
RAM_SPARE=500000000
RAM_MINIMUM=136314880
RAM_SIZE=$(strip "$RAM_SIZE")
RAM_SIZE="${RAM_SIZE// /}"
[ -z "$RAM_SIZE" ] && RAM_SIZE="2G"
if [[ "${RAM_SIZE,,}" != "max" && "${RAM_SIZE,,}" != "half" ]]; then
if [ -z "${RAM_SIZE//[0-9. ]}" ]; then
[ "${RAM_SIZE%%.*}" -lt "130" ] && RAM_SIZE="${RAM_SIZE}G" || RAM_SIZE="${RAM_SIZE}M"
fi
RAM_SIZE=$(echo "${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
! numfmt --from=iec "$RAM_SIZE" &>/dev/null && error "Invalid RAM_SIZE: $RAM_SIZE" && exit 16
wanted=$(numfmt --from=iec "$RAM_SIZE")
[ "$wanted" -lt "$RAM_MINIMUM" ] && error "RAM_SIZE is too low: $RAM_SIZE" && exit 16
fi
return 0
}
checkKvm() { checkKvm() {
# Check KVM support # Check KVM support
@@ -237,6 +171,8 @@ checkKvm() {
if disabled "$KVM"; then if disabled "$KVM"; then
warn "KVM acceleration is disabled, this will cause the machine to run about 10 times slower!" warn "KVM acceleration is disabled, this will cause the machine to run about 10 times slower!"
else else
# KVM accelerates only matching host and guest instruction sets; cross-
# architecture execution must fall back to software emulation.
if [[ "${ARCH,,}" != "$TARGET" ]]; then if [[ "${ARCH,,}" != "$TARGET" ]]; then
KVM="N" KVM="N"
warn "your CPU architecture is ${ARCH^^} and cannot provide KVM acceleration for ${PLATFORM^^} instructions, so the machine will run about 10 times slower." warn "your CPU architecture is ${ARCH^^} and cannot provide KVM acceleration for ${PLATFORM^^} instructions, so the machine will run about 10 times slower."
@@ -260,7 +196,7 @@ checkKvm() {
fi fi
if ! grep -qw "sse4_2" <<< "$flags"; then if ! grep -qw "sse4_2" <<< "$flags"; then
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!" error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
! enabled "$DEBUG" && exit 88 enabled "$DEBUG" || exit 88
fi fi
fi fi
fi fi
@@ -281,7 +217,7 @@ checkKvm() {
error "KVM acceleration is not available $KVM_ERR, this will cause the machine to run about 10 times slower." error "KVM acceleration is not available $KVM_ERR, this will cause the machine to run about 10 times slower."
error "See the FAQ for possible causes, or disable acceleration by adding the \"KVM=N\" variable (not recommended)." ;; error "See the FAQ for possible causes, or disable acceleration by adding the \"KVM=N\" variable (not recommended)." ;;
esac esac
! enabled "$DEBUG" && exit 88 enabled "$DEBUG" || exit 88
fi fi
fi fi
@@ -294,6 +230,7 @@ checkKvm() {
TZ=$(strip "$TZ") TZ=$(strip "$TZ")
STORAGE=$(strip "$STORAGE") STORAGE=$(strip "$STORAGE")
COUNTRY=$(strip "$COUNTRY") COUNTRY=$(strip "$COUNTRY")
MACHINE=$(strip "${MACHINE,,}")
DISK_SIZE=$(strip "$DISK_SIZE") DISK_SIZE=$(strip "$DISK_SIZE")
# Helper variables # Helper variables
@@ -320,24 +257,18 @@ FOOTER2="<a href='$SUPPORT'>$SUPPORT</a>"
SOCKETS=1 SOCKETS=1
CPU=$(cpu) CPU=$(cpu)
SYS=$(uname -r) SYS=$(uname -r)
KERNEL=$(echo "$SYS" | cut -b 1)
MINOR=$(echo "$SYS" | cut -d '.' -f2)
ARCH=$(dpkg --print-architecture) ARCH=$(dpkg --print-architecture)
CORES=$(grep -c '^processor' /proc/cpuinfo) CORES=$(grep -c '^processor' /proc/cpuinfo)
IFS=. read -r KERNEL MINOR _ <<< "$SYS"
if grep -qi "socket(s)" <<< "$(lscpu)"; then checkSockets
SOCKETS=$(lscpu | grep -m 1 -i 'socket(s)' | awk '{print $2}') checkCores
[ -z "${SOCKETS##*[!0-9]*}" ] && SOCKETS=1
[ "$SOCKETS" -lt "1" ] && SOCKETS=1
fi
normalizeCpuCores
checkStorage checkStorage
checkFilesystem getMemoryInfo
normalizeRamSize
# Print system info # Print system info
SYS="${SYS/-generic/}" SYS="${SYS/-generic/}"
FS=$(stat -f -c %T "$STORAGE")
FS="${FS/UNKNOWN //}" FS="${FS/UNKNOWN //}"
FS="${FS/ext2\/ext3/ext4}" FS="${FS/ext2\/ext3/ext4}"
FS=$(echo "$FS" | sed 's/[)(]//g') FS=$(echo "$FS" | sed 's/[)(]//g')
@@ -349,11 +280,14 @@ TOTAL_MEM=$(formatBytes "$RAM_TOTAL" "up")
echo " CPU: ${CPU} | RAM: ${AVAIL_MEM/ GB/}/$TOTAL_MEM | DISK: $SPACE_GB (${FS}) | KERNEL: ${SYS}" echo " CPU: ${CPU} | RAM: ${AVAIL_MEM/ GB/}/$TOTAL_MEM | DISK: $SPACE_GB (${FS}) | KERNEL: ${SYS}"
echo echo
checkHost
checkKvm checkKvm
# Runtime state is intentionally discarded at each container start; persistent
# machine and disk identity lives under STORAGE instead.
# Cleanup files # Cleanup files
rm -f "$QEMU_DIR"/dsm.url rm -f "$QEMU_DIR"/dsm.url
rm -f "$QEMU_DIR"/qemu.* "$QEMU_DIR"/*.pid "$QEMU_DIR"/*.sock rm -f "$QEMU_DIR"/{qemu.*,*.{pid,sock,pipe}}
# Cleanup dirs # Cleanup dirs
rm -rf /tmp/dsm rm -rf /tmp/dsm
+39 -13
View File
@@ -3,6 +3,8 @@ set -Eeuo pipefail
: "${URL:=""}" # URL of the PAT file to be downloaded. : "${URL:=""}" # URL of the PAT file to be downloaded.
# Persist the exact PAT base name so future starts reopen the matching boot,
# system, and cached installation files.
if [ -f "$STORAGE/dsm.ver" ]; then if [ -f "$STORAGE/dsm.ver" ]; then
BASE=$(<"$STORAGE/dsm.ver") BASE=$(<"$STORAGE/dsm.ver")
BASE="${BASE//[![:print:]]/}" BASE="${BASE//[![:print:]]/}"
@@ -16,6 +18,8 @@ FN="boot.pat"
DIR=$(find / -maxdepth 1 -type d -iname "$FN" -print -quit) DIR=$(find / -maxdepth 1 -type d -iname "$FN" -print -quit)
[ ! -d "$DIR" ] && DIR=$(find "$STORAGE" -maxdepth 1 -type d -iname "$FN" -print -quit) [ ! -d "$DIR" ] && DIR=$(find "$STORAGE" -maxdepth 1 -type d -iname "$FN" -print -quit)
# A boot.pat directory bind represents already extracted boot and system
# images and therefore takes precedence over PAT file or URL discovery.
if [ -d "$DIR" ]; then if [ -d "$DIR" ]; then
BASE="DSM_VirtualDSM" && URL="file://$DIR" BASE="DSM_VirtualDSM" && URL="file://$DIR"
if [[ ! -s "$STORAGE/$BASE.boot.img" || ! -s "$STORAGE/$BASE.system.img" ]]; then if [[ ! -s "$STORAGE/$BASE.boot.img" || ! -s "$STORAGE/$BASE.system.img" ]]; then
@@ -29,6 +33,8 @@ FILE=$(find / -maxdepth 1 -type f -iname "$FN" -print -quit)
URL=$(strip "$URL") URL=$(strip "$URL")
# Derive a filesystem-safe identity from the URL only when no local boot.pat
# source was supplied; preserve an existing system image identity if present.
if [ -n "$URL" ] && [ ! -s "$FILE" ] && [ ! -d "$DIR" ]; then if [ -n "$URL" ] && [ ! -s "$FILE" ] && [ ! -d "$DIR" ]; then
BASE=$(basename "$URL" .pat) BASE=$(basename "$URL" .pat)
if [ ! -s "$STORAGE/$BASE.system.img" ]; then if [ ! -s "$STORAGE/$BASE.system.img" ]; then
@@ -42,6 +48,8 @@ if [ -n "$URL" ] && [ ! -s "$FILE" ] && [ ! -d "$DIR" ]; then
fi fi
fi fi
# A complete matching image pair is the installation marker; the cached PAT
# itself is optional after installation.
if [[ -s "$STORAGE/$BASE.boot.img" && -s "$STORAGE/$BASE.system.img" ]]; then if [[ -s "$STORAGE/$BASE.boot.img" && -s "$STORAGE/$BASE.system.img" ]]; then
return 0 # Previous installation found return 0 # Previous installation found
fi fi
@@ -55,6 +63,8 @@ DL_GLOBAL="https://global.synologydownload.com/download/DSM"
[[ "${URL,,}" == *"cndl.synology"* ]] && DL="$DL_CHINA" [[ "${URL,,}" == *"cndl.synology"* ]] && DL="$DL_CHINA"
[[ "${URL,,}" == *"global.synology"* ]] && DL="$DL_GLOBAL" [[ "${URL,,}" == *"global.synology"* ]] && DL="$DL_GLOBAL"
# Honor an explicitly selected Synology mirror first, otherwise choose the
# China or global endpoint from the detected country.
if [ -z "$DL" ]; then if [ -z "$DL" ]; then
[ -z "$COUNTRY" ] && setCountry [ -z "$COUNTRY" ] && setCountry
[ -z "$COUNTRY" ] && info "Warning: could not detect country to select mirror!" [ -z "$COUNTRY" ] && info "Warning: could not detect country to select mirror!"
@@ -98,6 +108,8 @@ if [[ "${FS,,}" == "fat"* || "${FS,,}" == "vfat"* || "${FS,,}" == "msdos"* ]]; t
error "Unable to install on $FS filesystems, please use a different filesystem for /storage." && exit 61 error "Unable to install on $FS filesystems, please use a different filesystem for /storage." && exit 61
fi fi
# Extract beside storage on Unix filesystems to avoid container-space limits;
# use /tmp for filesystems that cannot safely host the installer workspace.
if [[ "${FS,,}" != "exfat"* && "${FS,,}" != "ntfs"* && "${FS,,}" != "unknown"* ]]; then if [[ "${FS,,}" != "exfat"* && "${FS,,}" != "ntfs"* && "${FS,,}" != "unknown"* ]]; then
TMP="$STORAGE/tmp" TMP="$STORAGE/tmp"
rm -rf "$TMP" rm -rf "$TMP"
@@ -126,13 +138,6 @@ SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
SPACE_GB=$(formatBytes "$SPACE") 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 (( MIN_SPACE > SPACE )) && error "Not enough free space for installation in $STORAGE, have $SPACE_GB available but need at least 14 GB." && exit 94
# Check if output is to interactive TTY
if [ -t 1 ]; then
PROGRESS="--progress=bar:noscroll"
else
PROGRESS="--progress=dot:giga"
fi
if [[ "$URL" == "file://"* ]]; then if [[ "$URL" == "file://"* ]]; then
MSG="Copying DSM" MSG="Copying DSM"
ERR="Failed to copy ${URL:7}" ERR="Failed to copy ${URL:7}"
@@ -160,17 +165,26 @@ else
SIZE=0 SIZE=0
REASON="" REASON=""
PROGRESS=()
OUTPUT=""
LOG=$(mktemp) LOG=$(mktemp)
[[ "${URL,,}" == *"_72806.pat" ]] && SIZE=361010261 [[ "${URL,,}" == *"_72806.pat" ]] && SIZE=361010261
[[ "${URL,,}" == *"_69057.pat" ]] && SIZE=363837333 [[ "${URL,,}" == *"_69057.pat" ]] && SIZE=363837333
[[ "${URL,,}" == *"_42218.pat" ]] && SIZE=379637760 [[ "${URL,,}" == *"_42218.pat" ]] && SIZE=379637760
/run/progress.sh "$PAT" "$SIZE" "$MSG ([P])..." & # 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"
fi
/run/progress.sh "$PAT" "$SIZE" "$MSG ([P])..." "$OUTPUT" 52428800 &
{ {
LC_ALL=C wget "$URL" -O "$PAT" --no-verbose --no-check-certificate \ LC_ALL=C wget "$URL" -O "$PAT" --no-verbose --no-check-certificate \
--timeout=30 --no-http-keep-alive --show-progress "$PROGRESS" \ --timeout=30 --no-http-keep-alive "${PROGRESS[@]}" \
--output-file="$LOG" --output-file="$LOG"
rc=$? rc=$?
} || : } || :
@@ -204,6 +218,8 @@ fi
SIZE=$(stat -c%s "$PAT") SIZE=$(stat -c%s "$PAT")
# Full Virtual DSM PAT files are substantially larger than update packs;
# reject undersized inputs before attempting destructive image preparation.
if ((SIZE<250000000)); then if ((SIZE<250000000)); then
error "The specified PAT file is probably an update pack as it's too small." && exit 62 error "The specified PAT file is probably an update pack as it's too small." && exit 62
fi fi
@@ -211,6 +227,8 @@ fi
MSG="Extracting installation image..." MSG="Extracting installation image..."
info "Install: $MSG" && html "$MSG" info "Install: $MSG" && html "$MSG"
# Newer PAT files are normal tar archives; older encrypted/proprietary forms
# require the bundled extractor as a compatibility fallback.
if { tar tf "$PAT"; } >/dev/null 2>&1; then if { tar tf "$PAT"; } >/dev/null 2>&1; then
tar xpf "$PAT" -C "$TMP/." tar xpf "$PAT" -C "$TMP/."
@@ -229,6 +247,8 @@ fi
MSG="Preparing system partition..." MSG="Preparing system partition..."
info "Install: $MSG" && html "$MSG" info "Install: $MSG" && html "$MSG"
# The PAT boot archive becomes the persistent QEMU boot disk after its
# companion system partition has been assembled.
BOOT=$(find "$TMP" -name "*.bin.zip" -print -quit) BOOT=$(find "$TMP" -name "*.bin.zip" -print -quit)
[ -z "$BOOT" ] && error "The PAT file contains no boot image." && exit 67 [ -z "$BOOT" ] && error "The PAT file contains no boot image." && exit 67
[ ! -s "$BOOT" ] && error "The PAT boot image archive is empty." && exit 67 [ ! -s "$BOOT" ] && error "The PAT boot image archive is empty." && exit 67
@@ -252,7 +272,7 @@ if ! touch "$SYSTEM"; then
error "Could not create file $SYSTEM for the system disk." && exit 98 error "Could not create file $SYSTEM for the system disk." && exit 98
fi fi
! setOwner "$SYSTEM" && warn "failed to set the owner for \"$SYSTEM\" !" setOwner "$SYSTEM" || warn "failed to set the owner for \"$SYSTEM\" !"
if [[ "${FS,,}" == "btrfs" ]]; then if [[ "${FS,,}" == "btrfs" ]]; then
{ chattr +C "$SYSTEM"; } || : { chattr +C "$SYSTEM"; } || :
@@ -271,6 +291,8 @@ if ! fallocate -l "$SYSTEM_SIZE" "$SYSTEM" &>/dev/null; then
fi fi
fi fi
# Recreate Synology's expected DOS partition layout inside the fixed 10 GiB
# system image before populating the ext4 root partition.
PART="$TMP/partition.fdisk" PART="$TMP/partition.fdisk"
{ {
@@ -318,6 +340,8 @@ OFFSET="1048576" # 2048 * 512
NUMBLOCKS="2097152" # (16777216 * 512) / 4096 NUMBLOCKS="2097152" # (16777216 * 512) / 4096
MSG="Installing system partition..." MSG="Installing system partition..."
# Build the ext4 filesystem directly from the extracted tree under fakeroot,
# preserving archive ownership without mounting a loop device.
fakeroot -- bash -c "set -Eeu;\ fakeroot -- bash -c "set -Eeu;\
[ -s $HDP.txz ] && tar xpfJ $HDP.txz --absolute-names -C $MOUNT/;\ [ -s $HDP.txz ] && tar xpfJ $HDP.txz --absolute-names -C $MOUNT/;\
[ -s $IDB.txz ] && tar xpfJ $IDB.txz --absolute-names -C $INDEX_DB/;\ [ -s $IDB.txz ] && tar xpfJ $IDB.txz --absolute-names -C $INDEX_DB/;\
@@ -327,8 +351,10 @@ fakeroot -- bash -c "set -Eeu;\
rm -rf "$MOUNT" rm -rf "$MOUNT"
echo "$BASE" > "$STORAGE/dsm.ver" echo "$BASE" > "$STORAGE/dsm.ver"
! setOwner "$STORAGE/dsm.ver" && warn "failed to set the owner for \"$STORAGE/dsm.ver\" !" setOwner "$STORAGE/dsm.ver" || warn "failed to set the owner for \"$STORAGE/dsm.ver\" !"
# Do not keep a second copy when the source PAT already lives in storage;
# downloaded or externally mounted sources are cached for later reuse.
if [[ "$URL" == "file://$STORAGE/$BASE.pat" ]]; then if [[ "$URL" == "file://$STORAGE/$BASE.pat" ]]; then
rm -f "$PAT" rm -f "$PAT"
else else
@@ -336,11 +362,11 @@ else
fi fi
if [ -f "$STORAGE/$BASE.pat" ]; then if [ -f "$STORAGE/$BASE.pat" ]; then
! setOwner "$STORAGE/$BASE.pat" && warn "failed to set the owner for \"$STORAGE/$BASE.pat\" !" setOwner "$STORAGE/$BASE.pat" || warn "failed to set the owner for \"$STORAGE/$BASE.pat\" !"
fi fi
mv -f "$BOOT" "$STORAGE/$BASE.boot.img" mv -f "$BOOT" "$STORAGE/$BASE.boot.img"
! setOwner "$STORAGE/$BASE.boot.img" && warn "failed to set the owner for \"$STORAGE/$BASE.boot.img\" !" setOwner "$STORAGE/$BASE.boot.img" || warn "failed to set the owner for \"$STORAGE/$BASE.boot.img\" !"
rm -rf "$TMP" rm -rf "$TMP"
+153 -30
View File
@@ -1,77 +1,144 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
msg="Checking memory..." normalizeMemory() {
enabled "$DEBUG" && echo "$msg"
local wanted
RAM_SPARE=500000000
RAM_MINIMUM="${RAM_MINIMUM:-1073741824}"
RAM_MINIMUM=$(strip "$RAM_MINIMUM")
RAM_MINIMUM="${RAM_MINIMUM// /}"
RAM_MINIMUM=$(echo "${RAM_MINIMUM^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
numfmt --from=iec "$RAM_MINIMUM" &>/dev/null || {
error "Invalid RAM_MINIMUM: $RAM_MINIMUM"
exit 16
}
RAM_MINIMUM=$(numfmt --from=iec "$RAM_MINIMUM")
RAM_SIZE=$(strip "$RAM_SIZE")
RAM_SIZE="${RAM_SIZE// /}"
[ -z "$RAM_SIZE" ] && RAM_SIZE="2G"
if [[ "${RAM_SIZE,,}" != "max" && "${RAM_SIZE,,}" != "half" ]]; then
# Bare values below 130 are interpreted as GiB for convenience; larger bare
# values are treated as MiB to preserve historical configurations.
if [ -z "${RAM_SIZE//[0-9. ]}" ]; then
[ "${RAM_SIZE%%.*}" -lt "130" ] && RAM_SIZE="${RAM_SIZE}G" || RAM_SIZE="${RAM_SIZE}M"
fi
RAM_SIZE=$(echo "${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
numfmt --from=iec "$RAM_SIZE" &>/dev/null || {
error "Invalid RAM_SIZE: $RAM_SIZE"
exit 16
}
wanted=$(numfmt --from=iec "$RAM_SIZE")
if [ "$wanted" -lt "$RAM_MINIMUM" ]; then
error "$(app) requires at least $(formatBytes "$RAM_MINIMUM") of RAM, but RAM_SIZE is set to $(formatBytes "$wanted")."
exit 16
fi
# QEMU requires a whole-number memory value, so convert decimal sizes to MiB.
if [[ "$RAM_SIZE" == *.* ]]; then
RAM_SIZE="$(( wanted / 1048576 ))M"
fi
fi
app() {
echo "Virtual DSM"
return 0 return 0
} }
checkConfiguredMemory() { checkConfiguredMemory() {
local wanted msg
local final="$1"
if disabled "$RAM_CHECK" || [[ "${RAM_SIZE,,}" == "max" || "${RAM_SIZE,,}" == "half" ]]; then if disabled "$RAM_CHECK" || [[ "${RAM_SIZE,,}" == "max" || "${RAM_SIZE,,}" == "half" ]]; then
return 0 return 0
fi fi
local wanted avail_mem
wanted=$(numfmt --from=iec "$RAM_SIZE") wanted=$(numfmt --from=iec "$RAM_SIZE")
avail_mem=$(formatBytes "$RAM_AVAIL")
if (( (wanted + RAM_SPARE) > RAM_AVAIL )); then if (( (wanted + RAM_SPARE) > RAM_AVAIL )); then
msg="Your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is too high for the $AVAIL_MEM of free memory available,"
local msg="Your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is too high for the $avail_mem of free memory available,"
# ZFS ARC can release cached memory under pressure, so this free-memory
# heuristic remains informational instead of rewriting RAM_SIZE.
if [[ "${FS,,}" == "zfs" ]]; then if [[ "${FS,,}" == "zfs" ]]; then
info "$msg but since ZFS is active this will be ignored."
enabled "$final" && info "$msg but since ZFS is active this will be ignored."
else else
RAM_SIZE="max" RAM_SIZE="max"
warn "$msg it will automatically be adjusted to a lower amount." RAM_WARNING="$msg it will automatically be adjusted to a lower amount."
fi fi
else else
if (( (wanted + (RAM_SPARE * 3)) > RAM_AVAIL )); then if (( (wanted + (RAM_SPARE * 3)) > RAM_AVAIL )); then
msg="your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is very close to the $AVAIL_MEM of free memory available,"
local msg="your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is very close to the $avail_mem of free memory available,"
if [[ "${FS,,}" == "zfs" ]]; then if [[ "${FS,,}" == "zfs" ]]; then
info "$msg but since ZFS is active this will be ignored." enabled "$final" && info "$msg but since ZFS is active this will be ignored."
else else
warn "$msg please consider a lower amount." enabled "$final" && warn "$msg please consider a lower amount."
fi fi
fi fi
fi fi
return 0 return 0
} }
configureHalfMemory() { configureHalfMemory() {
local wanted
if [[ "${RAM_SIZE,,}" != "half" ]]; then if [[ "${RAM_SIZE,,}" != "half" ]]; then
return 0 return 0
fi fi
if (( (RAM_AVAIL / 2) > RAM_SPARE )); then if (( (RAM_AVAIL / 2) > RAM_SPARE )); then
wanted=$(( (RAM_AVAIL / 2) / 1048577 ))
RAM_SIZE="${wanted}M" local wanted=$(( RAM_AVAIL / 2 ))
info "Allocated $wanted MB of RAM for $(app)."
# Divide by one byte more than a MiB to round down
local target=$(( wanted / 1048577 ))
RAM_SIZE="${target}M"
RAM_ALLOCATION="$wanted"
else else
RAM_SIZE="max" RAM_SIZE="max"
fi fi
return 0 return 0
} }
configureMaxMemory() { configureMaxMemory() {
local wanted
if [[ "${RAM_SIZE,,}" != "max" ]]; then if [[ "${RAM_SIZE,,}" != "max" ]]; then
return 0 return 0
fi fi
# max keeps a host reserve when possible, but on very small systems falls back
# to half the available memory to avoid starving the container.
if (( RAM_AVAIL < (RAM_SPARE * 2) )); then if (( RAM_AVAIL < (RAM_SPARE * 2) )); then
wanted=$(( RAM_AVAIL / 2 )) local wanted=$(( RAM_AVAIL / 2 ))
else else
wanted=$(( RAM_AVAIL - (RAM_SPARE * 3) )) local wanted=$(( RAM_AVAIL - (RAM_SPARE * 3) ))
if (( wanted < (RAM_SPARE * 6) )); then if (( wanted < (RAM_SPARE * 6) )); then
wanted=$(( RAM_AVAIL - RAM_SPARE )) wanted=$(( RAM_AVAIL - RAM_SPARE ))
@@ -79,34 +146,90 @@ configureMaxMemory() {
fi fi
wanted=$(( wanted / 1048577 )) # Divide by one byte more than a MiB to round down
RAM_SIZE="${wanted}M" local target=$(( wanted / 1048577 ))
RAM_SIZE="${target}M"
RAM_ALLOCATION="$wanted"
info "Allocated $wanted MB of RAM for $(app)." return 0
}
showMemoryLimitHint() {
local kernel
kernel=$(uname -r)
if [[ "${kernel,,}" == *-wsl2* ]]; then
echo
info "Docker Desktop (WSL2) is detected, follow these instructions:"
info ""
info "Increase the memory limit in \"%UserProfile%\\.wslconfig\" by setting \"memory=<size>\" under \"[wsl2]\"."
info "Then run \"wsl --shutdown\" in PowerShell and restart Docker Desktop for the new limit to take effect."
echo
fi
return 0 return 0
} }
checkMinimumMemory() { checkMinimumMemory() {
local wanted
local wanted
wanted=$(numfmt --from=iec "$RAM_SIZE") wanted=$(numfmt --from=iec "$RAM_SIZE")
if [ "$wanted" -lt "$RAM_MINIMUM" ]; then if [ "$wanted" -lt "$RAM_MINIMUM" ]; then
wanted=$(( wanted / 1048577 ))
error "Not enough memory available, there is only $wanted MB left!" error "$(app) requires at least $(formatBytes "$RAM_MINIMUM") of RAM, but only $(formatBytes "$wanted") can be allocated."
showMemoryLimitHint
exit 16 exit 16
fi fi
return 0 return 0
} }
getMemoryInfo checkMemoryAllocation() {
AVAIL_MEM=$(formatBytes "$RAM_AVAIL")
checkConfiguredMemory local final="${1:-N}"
configureHalfMemory local configured
configureMaxMemory
checkMinimumMemory normalizeMemory
configured="$RAM_SIZE"
RAM_WARNING=""
RAM_ALLOCATION=""
getMemoryInfo
checkConfiguredMemory "$final"
configureHalfMemory
configureMaxMemory
checkMinimumMemory
if enabled "$final"; then
[ -n "$RAM_WARNING" ] && warn "$RAM_WARNING"
[ -n "$RAM_ALLOCATION" ] && info "Allocated $(formatBytes "$RAM_ALLOCATION") of RAM for $(app)."
else
RAM_SIZE="$configured"
fi
return 0
}
checkMemoryRequirement() {
checkMemoryAllocation "N"
return 0
}
finalizeMemory() {
checkMemoryAllocation "Y"
return 0
}
checkMemoryRequirement
return 0 return 0
+929 -359
View File
File diff suppressed because it is too large Load Diff
+194 -44
View File
@@ -2,25 +2,31 @@
set -Eeuo pipefail set -Eeuo pipefail
: "${SHUTDOWN:="Y"}" # Graceful ACPI shutdown : "${SHUTDOWN:="Y"}" # Graceful ACPI shutdown
: "${TIMEOUT:="115"}" # QEMU termination timeout : "${TIMEOUT:="105"}" # QEMU termination timeout
: "${API_TIMEOUT:="90"}" # External API call timeout : "${API_TIMEOUT:="90"}" # External API call timeout
# Configure QEMU for graceful shutdown # Configure QEMU for graceful shutdown
API_CMD=6 API_CMD=6
API_HOST="127.0.0.1:$COM_PORT"
# Configure QEMU for graceful shutdown SHUTDOWN_SKIP=0
SHUTDOWN_SIGNAL=0
QEMU_END="$QEMU_DIR/qemu.end" QEMU_END="$QEMU_DIR/qemu.end"
CONSOLE_PID="$QEMU_DIR/console.pid"
CONSOLE_SOCKET="$QEMU_DIR/console.sock"
QEMU_START_PID="$QEMU_DIR/qemu.start.pid"
_trap() { _trap() {
local func="$1"; shift local func="$1"; shift
local sig local sig
TRAP_PID=$BASHPID TRAP_PID=$BASHPID
for sig; do for sig; do
# Capture the local callback and signal while registering the trap.
# shellcheck disable=SC2064
trap "$func $sig" "$sig" trap "$func $sig" "$sig"
done done
@@ -61,11 +67,40 @@ displayReason() {
readQemuPid() { readQemuPid() {
local -n _pid="$1" # Interactive startup uses a wrapper-created PID file before QEMU writes its
# own pidfile, so accept either during startup and shutdown races.
readPidFile "$1" "$QEMU_START_PID" && return 0
readPidFile "$1" "$QEMU_PID"
}
if [ ! -s "$QEMU_PID" ] || ! read -r _pid <"$QEMU_PID"; then qemuPidFile() {
return 1
fi local -n _file="$1"
_file="$QEMU_PID"
[ -s "$QEMU_START_PID" ] && _file="$QEMU_START_PID"
return 0
}
waitQemuExit() {
local timeout="${1:-10}"
local file
qemuPidFile file
waitPidFile "$file" "$timeout"
}
waitQemuPid() {
local cnt=0
while ! readQemuPid "$1"; do
sleep 0.02
cnt=$((cnt + 1))
(( cnt >= 50 )) && return 1
done
return 0 return 0
} }
@@ -73,11 +108,10 @@ readQemuPid() {
forceKillQemu() { forceKillQemu() {
local reason="$1" local reason="$1"
local pid="" local pid display
local display
! readQemuPid pid && return 0 readQemuPid pid || return 0
! isAlive "$pid" && return 0 isAlive "$pid" || return 0
display=$(displayReason "$reason") display=$(displayReason "$reason")
error "Forcefully terminating $(app), reason: $display..." error "Forcefully terminating $(app), reason: $display..."
@@ -88,29 +122,117 @@ forceKillQemu() {
cleanupHelpers() { cleanupHelpers() {
local pids=( "${HOST_PID:-}" "${WSD_PID:-}" \ local pids=( "${HOST_PID:-}" "${WSD_PID:-}" "${CONSOLE_PID:-}" \
"${WEB_PID:-}" "${PASST_PID:-}" "${DNSMASQ_PID:-}" ) "${WEB_PID:-}" "${PASST_PID:-}" "${DNSMASQ_PID:-}" )
mKill "${pids[@]}" mKill "${pids[@]}"
fKill "print.sh" fKill "print.sh"
rm -f -- "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET"
closeNetwork closeNetwork
return 0 return 0
} }
startConsole() {
local output="${1:-/dev/tty}"
local cnt=0
rm -f -- "$CONSOLE_SOCKET" "$CONSOLE_PID"
if ! stty -icanon -echo isig -ixon min 1 time 0 </dev/tty; then
error "Failed to configure serial console terminal!"
return 1
fi
(
trap '' INT QUIT
exec nc -lU "$CONSOLE_SOCKET" </dev/tty >"$output"
) &
local pid="$!"
echo "$pid" > "$CONSOLE_PID"
while [ ! -S "$CONSOLE_SOCKET" ]; do
if ! isAlive "$pid"; then
rm -f -- "$CONSOLE_PID"
error "Serial console relay exited unexpectedly!"
return 1
fi
sleep 0.02
cnt=$((cnt + 1))
if (( cnt > 100 )); then
error "Failed to start serial console relay!"
return 1
fi
done
return 0
}
stopConsole() {
mKill "$CONSOLE_PID"
return 0
}
startQemu() {
rm -f -- "$QEMU_START_PID"
# Launch QEMU in a separate session while recording the real child PID;
# setsid's wrapper PID is not suitable for guest shutdown or forced cleanup.
(
trap '' INT QUIT
# shellcheck disable=SC2016
exec setsid -f -w sh -c '
file=$1
shift
"$@" &
pid=$!
printf "%s\n" "$pid" > "$file" || exit 1
rc=0
wait "$pid" 2>/dev/null || rc=$?
exit "$rc"
' sh "$QEMU_START_PID" "$@"
) </dev/null &
return 0
}
finish() { finish() {
local reason=$1 local reason=$1 failed=0
if [ ! -f "$QEMU_END" ] && (( reason != 0 )); then
failed=1
fi
touch "$QEMU_END" touch "$QEMU_END"
forceKillQemu "$reason" forceKillQemu "$reason"
cleanupHelpers cleanupHelpers
if ! waitPidFile "$QEMU_PID" 10; then if ! waitQemuExit 10; then
warn "Timed out while waiting for $(app) to exit!" warn "Timed out while waiting for $(app) to exit!"
fi fi
(( reason != 1 )) && echo && echo " Shutdown completed!" echo
if (( failed == 0 )); then
echo " Shutdown completed!"
else
error "QEMU exited unexpectedly!"
fi
exit "$reason" exit "$reason"
} }
@@ -119,15 +241,16 @@ sendGuestShutdown() {
local pid="$1" local pid="$1"
local response local response
local url
# Virtual DSM ignores ACPI powerdown, so graceful shutdown must go through
# the qemu-host guest API exposed on the Unix socket.
# Don't send the powerdown signal because vDSM ignores ACPI signals # Don't send the powerdown signal because vDSM ignores ACPI signals
# nc -q 1 -w 1 -U "$QEMU_DIR/monitor.sock" &> /dev/null <<<'system_powerdown' || : # nc -q 1 -w 1 -U "$QEMU_DIR/monitor.sock" &> /dev/null <<<'system_powerdown' || :
# Send shutdown command to guest agent via serial port # Send shutdown command to guest agent via serial port
API_TIMEOUT=$(strip "$API_TIMEOUT") API_TIMEOUT=$(strip "$API_TIMEOUT")
url="http://$API_HOST/read?command=$API_CMD&timeout=$API_TIMEOUT" local url="http://localhost/read?command=$API_CMD&timeout=$API_TIMEOUT"
response=$(curl -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1) response=$(curl --unix-socket "$HOST_API_SOCKET" -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1)
if [[ "$response" =~ "\"success\"" ]]; then if [[ "$response" =~ "\"success\"" ]]; then
@@ -148,15 +271,14 @@ sendGuestShutdown() {
normalizeTimeout() { normalizeTimeout() {
# Divide the remaining timeout into guest wait, SIGTERM grace, and final
# cleanup instead of allowing the API call to consume the entire budget.
local term_grace=3 # seconds before loop ends to send SIGTERM local term_grace=3 # seconds before loop ends to send SIGTERM
local cleanup_grace=3 # seconds reserved after the loop for cleanup local cleanup_grace=3 # seconds reserved after the loop for cleanup
local elapsed
local timeout_left
local min
TIMEOUT=$(strip "$TIMEOUT") TIMEOUT=$(strip "$TIMEOUT")
if [[ ! "$TIMEOUT" =~ ^[0-9]+$ ]]; then if [[ ! "$TIMEOUT" =~ ^[0-9]+$ ]]; then
TIMEOUT=115 TIMEOUT=105
fi fi
if (( TIMEOUT >= 30 )); then if (( TIMEOUT >= 30 )); then
@@ -167,10 +289,10 @@ normalizeTimeout() {
cleanup_grace=4 cleanup_grace=4
fi fi
elapsed=$((SECONDS - start)) local elapsed=$((SECONDS - start))
timeout_left=$((TIMEOUT - elapsed)) local timeout_left=$((TIMEOUT - elapsed))
min=$((term_grace + cleanup_grace + 1)) local min=$((term_grace + cleanup_grace + 1))
(( timeout_left < min )) && timeout_left=$min (( timeout_left < min )) && timeout_left=$min
wait_until=$((timeout_left - cleanup_grace)) wait_until=$((timeout_left - cleanup_grace))
@@ -184,18 +306,19 @@ waitForShutdown() {
local cnt=0 local cnt=0
local pid="$1" local pid="$1"
local name="$APP" local name="$APP"
local slp
while (( cnt <= wait_until )); do while (( cnt <= wait_until && SHUTDOWN_SKIP == 0 )); do
sleep 1 & sleep 1 &
slp=$! local slp=$!
# Stop waiting if the process has exited # Stop waiting if the process has exited
! isAlive "$pid" && break isAlive "$pid" || break
# The process state is authoritative, but disappearance of both pidfiles
# also ends the wait when a wrapper exits before process reaping completes.
# Workaround for stale/zombie QEMU pid file # Workaround for stale/zombie QEMU pid file
[ ! -s "$QEMU_PID" ] && break [ ! -s "$QEMU_START_PID" ] && [ ! -s "$QEMU_PID" ] && break
if (( cnt == sigterm_at )); then if (( cnt == sigterm_at )); then
info "${name^} is still running, sending SIGTERM... ($cnt/$wait_until)" info "${name^} is still running, sending SIGTERM... ($cnt/$wait_until)"
@@ -204,7 +327,7 @@ waitForShutdown() {
info "Waiting for $name to shut down... ($cnt/$wait_until)" info "Waiting for $name to shut down... ($cnt/$wait_until)"
fi fi
wait "$slp" wait "$slp" || :
(( cnt++ )) (( cnt++ ))
done done
@@ -212,32 +335,47 @@ waitForShutdown() {
return 0 return 0
} }
graceful_shutdown() { gracefulShutdown() {
local sig="$1" local sig="$1"
local pid="" local pid code
local code=0
[[ $BASHPID != "$TRAP_PID" ]] && return [[ $BASHPID != "$TRAP_PID" ]] && return
code=$(signalCode "$sig") code=$(signalCode "$sig")
if [ -f "$QEMU_END" ]; then if (( SHUTDOWN_SIGNAL != 0 )); then
echo && info "Received $1 signal while already shutting down..."
# 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..."
return
fi
echo && info "Received $sig signal while already shutting down..."
return return
fi fi
set +e
start=$SECONDS 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" touch "$QEMU_END"
echo && info "Received $1 signal, sending shutdown command..."
echo && info "Received $sig signal, sending shutdown command..."
if ! readQemuPid pid; then if ! readQemuPid pid; then
warn "QEMU PID file ($QEMU_PID) does not exist?" if ! interactive || ! waitQemuPid pid; then
finish "$code" warn "QEMU PID file does not exist?"
finish "$code"
fi
fi fi
if ! isAlive "$pid"; then if [ -z "$pid" ] || ! isAlive "$pid"; then
warn "QEMU process with PID $pid does not exist?" warn "QEMU process with PID $pid does not exist?"
finish "$code" finish "$code"
fi fi
@@ -249,9 +387,21 @@ graceful_shutdown() {
finish "$code" finish "$code"
} }
! enabled "$SHUTDOWN" && return 0 enableTrap() {
enabled "$SHUTDOWN" || return 0
# Keep Ctrl-C available to interactive users without installing an unnecessary
# SIGINT handler for background/container execution.
if interactive; then
_trap gracefulShutdown SIGINT
fi
_trap gracefulShutdown SIGTERM SIGHUP SIGABRT SIGQUIT
return 0
}
[ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT" [ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT"
_trap graceful_shutdown SIGTERM SIGHUP SIGABRT SIGQUIT
return 0 return 0
+35 -14
View File
@@ -9,6 +9,7 @@ cd /run
. utils.sh # Load functions . utils.sh # Load functions
info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "$1" "\E[0m\n" >&2; } info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "$1" "\E[0m\n" >&2; }
warn () { printf "%b%s%b" "\E[1;33m " "WARNING: $1" "\E[0m\n" >&2; }
error () { printf "%b%s%b" "\E[1;31m " "ERROR: $1" "\E[0m\n" >&2; } error () { printf "%b%s%b" "\E[1;31m " "ERROR: $1" "\E[0m\n" >&2; }
disabled "$NETWORK" && exit 0 disabled "$NETWORK" && exit 0
@@ -19,8 +20,9 @@ driver="/run/shm/qemu.nic"
page="/run/shm/index.html" page="/run/shm/index.html"
address="/run/shm/qemu.ip" address="/run/shm/qemu.ip"
shutdown="/run/shm/qemu.end" shutdown="/run/shm/qemu.end"
socket="/run/shm/qemu-host-api.sock"
template="/var/www/index.html" template="/var/www/index.html"
url="http://127.0.0.1:2210/read?command=10" url="http://localhost/read?command=10"
resp_err="Guest returned an invalid response:" resp_err="Guest returned an invalid response:"
curl_err="Failed to connect to guest: curl error" curl_err="Failed to connect to guest: curl error"
@@ -35,9 +37,9 @@ exitIfShuttingDown() {
queryGuest() { queryGuest() {
local rc # Query DSM through the qemu-host sidecar rather than the guest network,
# which may not be configured yet.
{ json=$(curl -m 20 -sk "$url"); rc=$?; } || : { json=$(curl --unix-socket "$socket" -m 20 -sk "$url"); local rc=$?; } || :
exitIfShuttingDown exitIfShuttingDown
@@ -53,9 +55,8 @@ readJsonField() {
local query="$1" local query="$1"
local result local result
local rc
{ result=$(jq -r "$query" <<< "$json"); rc=$?; } || : { result=$(jq -r "$query" <<< "$json"); local rc=$?; } || :
if (( rc != 0 )); then if (( rc != 0 )); then
error "$jq_err $rc ( $json )" error "$jq_err $rc ( $json )"
@@ -73,12 +74,12 @@ readJsonField() {
readGuestStatus() { readGuestStatus() {
local result msg rc local result msg
result=$(readJsonField '.status') || return 1 result=$(readJsonField '.status') || return 1
if [[ "$result" != "success" ]]; then if [[ "$result" != "success" ]]; then
{ msg=$(jq -r '.message // empty' <<< "$json"); rc=$?; } || : { msg=$(jq -r '.message // empty' <<< "$json"); local rc=$?; } || :
if (( rc != 0 )); then if (( rc != 0 )); then
error "$jq_err $rc ( $json )" error "$jq_err $rc ( $json )"
@@ -117,6 +118,8 @@ writeDsmLocation() {
pollGuestLocation() { pollGuestLocation() {
# Keep polling until the guest reports a usable address, but stop promptly
# when container shutdown begins.
while [ ! -s "$file" ]; do while [ ! -s "$file" ]; do
# Check if not shutting down # Check if not shutting down
@@ -139,14 +142,29 @@ pollGuestLocation() {
return 0 return 0
} }
checkAddressConflict() {
local guest_ip="${location%:*}"
local container_ip=""
[ -s "$address" ] && container_ip=$(<"$address")
[ -z "$container_ip" ] && return 0
[[ "$guest_ip" != "$container_ip" ]] && return 0
warn "DSM is using the same IP as the container, this will cause connectivity issues."
warn "change the container's macvlan IP or assign DSM a different address in your router."
return 0
}
writeDhcpPage() { writeDhcpPage() {
local title body script html local html
msg="http://$location" msg="http://$location"
title="<title>Virtual DSM</title>" local title="<title>Virtual DSM</title>"
body="The location of DSM is <a href='http://$location'>http://$location</a>" local body="The location of DSM is <a href='http://$location'>http://$location</a>"
script="<script>setTimeout(function(){ window.location.assign('http://$location'); }, 3000);</script>" local script="<script>setTimeout(function(){ window.location.assign('http://$location'); }, 3000);</script>"
html=$(<"$template") html=$(<"$template")
html="${html/\[1\]/$title}" html="${html/\[1\]/$title}"
@@ -163,12 +181,14 @@ writeDhcpPage() {
buildStaticMessage() { buildStaticMessage() {
local nic ip port local nic ip
nic=$(<"$driver") nic=$(<"$driver")
ip=$(<"$address") ip=$(<"$address")
port="${location##*:}" local port="${location##*:}"
# NAT and user-mode networking are reached through a forwarded host port;
# macvlan exposes DSM directly on the container-facing LAN address.
if [[ "${nic,,}" != "macvlan" ]]; then if [[ "${nic,,}" != "macvlan" ]]; then
msg="port $port" msg="port $port"
else else
@@ -195,6 +215,7 @@ exitIfShuttingDown
location=$(<"$file") location=$(<"$file")
if enabled "$DHCP"; then if enabled "$DHCP"; then
checkAddressConflict
writeDhcpPage writeDhcpPage
else else
buildStaticMessage buildStaticMessage
+14 -6
View File
@@ -45,9 +45,9 @@ checkClocksource() {
checkSse42() { checkSse42() {
if ! grep -qw "sse4_2" <<< "$flags"; then if ! hasFlag "sse4_2"; then
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!" error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
! enabled "$DEBUG" && exit 88 enabled "$DEBUG" || exit 88
fi fi
return 0 return 0
@@ -66,6 +66,8 @@ trimSpaces() {
removeCpuArgument() { removeCpuArgument() {
# CPU configuration has dedicated variables. Remove raw -cpu arguments so
# option ordering cannot silently override the validated model and flags.
local args=" ${ARGUMENTS:-} " local args=" ${ARGUMENTS:-} "
while [[ "$args" =~ [[:space:]]-cpu([[:space:]][^[:space:]]+|=[^[:space:]]+)? ]]; do while [[ "$args" =~ [[:space:]]-cpu([[:space:]][^[:space:]]+|=[^[:space:]]+)? ]]; do
@@ -94,10 +96,12 @@ configureKvmCpuModel() {
appendKvmInvtscFeature() { appendKvmInvtscFeature() {
if grep -qw "svm" <<< "$flags"; then # invtsc is safe only when the active accelerator can scale the host TSC;
# AMD and Intel expose that capability through different host flags.
if hasFlag "svm"; then
# AMD processor # AMD processor
if grep -qw "tsc_scale" <<< "$flags"; then if hasFlag "tsc_scale"; then
CPU_FEATURES+=",+invtsc" CPU_FEATURES+=",+invtsc"
fi fi
@@ -131,6 +135,8 @@ configureTcgCpuModel() {
return 0 return 0
fi fi
# TCG uses the broad max model on native x86, but qemu64 is the compatible
# cross-architecture fallback.
if [[ "$ARCH" == "amd64" ]]; then if [[ "$ARCH" == "amd64" ]]; then
CPU_MODEL="max" CPU_MODEL="max"
CPU_FEATURES+=",migratable=no" CPU_FEATURES+=",migratable=no"
@@ -158,6 +164,8 @@ configureTcg() {
composeCpuFlags() { composeCpuFlags() {
# Compose one -cpu value in precedence order: model, required features,
# then user-provided overrides.
CPU_FLAGS="${CPU_MODEL}${CPU_FEATURES:+,$CPU_FEATURES}${CPU_FLAGS:+,$CPU_FLAGS}" CPU_FLAGS="${CPU_MODEL}${CPU_FEATURES:+,$CPU_FEATURES}${CPU_FLAGS:+,$CPU_FLAGS}"
return 0 return 0
@@ -170,6 +178,8 @@ configureHostCpuName() {
fi fi
if [ -n "$HOST_CPU" ]; then if [ -n "$HOST_CPU" ]; then
# qemu-host expects a comma-separated CPU description with empty family
# and suffix fields, not QEMU's -cpu syntax.
HOST_CPU="${HOST_CPU%%,*},," HOST_CPU="${HOST_CPU%%,*},,"
else else
HOST_CPU="QEMU, Virtual CPU," HOST_CPU="QEMU, Virtual CPU,"
@@ -186,8 +196,6 @@ configureHostCpuName() {
selectClocksource selectClocksource
checkClocksource checkClocksource
flags=$(sed -ne '/^flags/s/^.*: //p' /proc/cpuinfo)
if ! disabled "$KVM"; then if ! disabled "$KVM"; then
configureKvm configureKvm
else else
+288 -23
View File
@@ -2,44 +2,309 @@
set -Eeuo pipefail set -Eeuo pipefail
info="/run/shm/msg.html" info="/run/shm/msg.html"
info_tmp="${info}.${BASHPID}.tmp"
escape () { escape() {
local s
s=${1//&/\&amp;} local s
s=${s//</\&lt;}
s=${s//>/\&gt;} s=${1//&/\&amp;}
s=${s//'"'/\&quot;} s=${s//</\&lt;}
printf -- %s "$s" s=${s//>/\&gt;}
s=${s//'"'/\&quot;}
s=${s//"'"/\&#39;}
printf '%s' "$s"
return 0
}
writeInfo() {
local content="$1"
# Replace the web status atomically so websocket readers never observe a
# partially written HTML fragment.
if ! printf '%s\n' "$content" > "$info_tmp"; then
rm -f -- "$info_tmp"
return 1
fi
if ! mv -f -- "$info_tmp" "$info"; then
rm -f -- "$info_tmp"
return 1
fi
return 0
}
getBytes() {
local path="$1"
local mode="$2"
local bytes="0"
if [[ "$mode" == "counter" ]]; then
if [ -r "$path" ]; then
read -r bytes < "$path" || bytes="0"
fi
[[ "$bytes" =~ ^[0-9]+$ ]] || bytes="0"
printf '%s\n' "$bytes"
return 0 return 0
fi
if [ ! -s "$path" ] && [ ! -d "$path" ]; then
printf '0\n'
return 0
fi
if [[ "$mode" == "allocated" ]]; then
bytes=$(du -sB1 -- "$path" 2>/dev/null | cut -f1) || bytes="0"
else
bytes=$(du -sb -- "$path" 2>/dev/null | cut -f1) || bytes="0"
fi
printf '%s\n' "$bytes"
return 0
}
getStatus() {
local file="$1"
local bytes total extra=""
[ -r "$file" ] || return 1
read -r bytes total extra < "$file" || return 1
if [[ ! "$bytes" =~ ^[0-9]+$ ||
! "$total" =~ ^[0-9]+$ ||
-n "$extra" ]]; then
return 1
fi
printf '%s %s\n' "$bytes" "$total"
return 0
}
formatSize() {
local bytes="$1"
local size
size=$(numfmt --to=iec --suffix=B "$bytes" |
sed -r 's/([A-Z])/ \1/') ||
size="${bytes} bytes"
printf '%s' "$size"
return 0
}
printPercentProgress() {
local percent="$1"
while (( next_percent <= percent && next_percent <= 100 )); do
if [[ "$printed" == "Y" ]]; then
printf ' → %s%%' "$next_percent"
else
printf '%s%%' "$next_percent"
fi
printed="Y"
next_percent=$((next_percent + 10))
done
return 0
}
printCurrentSize() {
local bytes="$1"
local size
size=$(formatSize "$bytes")
if [[ "$printed" == "Y" ]]; then
printf ' → %s' "$size"
else
printf '%s' "$size"
fi
printed="Y"
return 0
}
printSizeProgress() {
local bytes="$1"
local size
while (( bytes >= next_bytes )); do
size=$(formatSize "$next_bytes")
if [[ "$printed" == "Y" ]]; then
printf ' → %s' "$size"
else
printf '%s' "$size"
fi
printed="Y"
next_bytes=$((next_bytes + step_bytes))
done
return 0
}
stopProgress() {
if [ -z "$status_file" ]; then
exit 0
fi
stopping="Y"
return 0
}
finishProgress() {
rm -f -- "$info_tmp"
if [[ "$output" == "log" && "$printed" == "Y" ]]; then
printf '\n'
fi
return 0
} }
path="$1" path="$1"
total="$2" total="$2"
body=$(escape "$3") body=$(escape "$3")
output="${4:-}"
step_bytes="${5:-536870912}"
mode="${6:-apparent}"
status_file="${7:-}"
if [[ -n "$total" && ! "$total" =~ ^(0|[1-9][0-9]*)$ ]]; then
printf 'Invalid total size: %s\n' "$total" >&2
exit 2
fi
if [[ ! "$step_bytes" =~ ^[1-9][0-9]*$ ]]; then
printf 'Invalid progress interval: %s\n' "$step_bytes" >&2
exit 2
fi
case "$mode" in
apparent | allocated | counter ) ;;
* )
printf 'Invalid progress mode: %s\n' "$mode" >&2
exit 2
;;
esac
case "$output" in
"" | log ) ;;
* )
printf 'Invalid progress output: %s\n' "$output" >&2
exit 2
;;
esac
printed="N"
next_percent=10
next_bytes="$step_bytes"
log_mode="percent"
stopping="N"
if [ -z "$total" ] || [[ "$total" == "0" ]]; then
log_mode="size"
fi
trap finishProgress EXIT
trap 'exit 0' HUP INT QUIT
# SIGTERM requests one final measurement and web update rather than
# terminating between progress samples.
trap stopProgress TERM
if [[ "$body" == *"..." ]]; then if [[ "$body" == *"..." ]]; then
body="<p class=\"loading\">${body::-3}</p>" body="<p class=\"loading\">${body::-3}</p>"
fi fi
while true while true; do
do
if [ ! -s "$path" ] && [ ! -d "$path" ]; then final_pass="${stopping:-}"
bytes="0" bytes=$(getBytes "$path" "$mode")
else effective_total="$total"
bytes=$(du -sb "$path" 2>/dev/null | cut -f1) || bytes="0"
fi # An external downloader may provide authoritative completed and total byte
# counters; use them instead of filesystem size when available.
if (( bytes > 4096 )); then if [ -n "$status_file" ] && status=$(getStatus "$status_file"); then
if [ -z "$total" ] || [[ "$total" == "0" ]] || [ "$bytes" -gt "$total" ]; then read -r status_bytes status_total <<< "$status"
size=$(numfmt --to=iec --suffix=B "$bytes" | sed -r 's/([A-Z])/ \1/') || size="${bytes} bytes" bytes="$status_bytes"
else
size="$(echo "$bytes" "$total" | awk '{printf "%.1f", $1 * 100 / $2}')" if (( status_total > 0 )); then
size="$size%" effective_total="$status_total"
fi fi
[[ "$size" != "0.0%" ]] && echo "${body//(\[P\])/($size)}"> "$info"
fi fi
sleep 1 & wait $! # A real total may become available shortly after aria2 starts.
if [[ "$log_mode" == "size" &&
"$printed" == "N" &&
-n "$effective_total" &&
"$effective_total" != "0" ]]; then
log_mode="percent"
fi
if (( bytes > 4096 )); then
write_html="Y"
if [ -z "$effective_total" ] ||
[[ "$effective_total" == "0" ]] ||
(( bytes > effective_total )); then
size=$(formatSize "$bytes")
if [[ "$output" == "log" ]]; then
if [[ "$log_mode" == "percent" ]]; then
printCurrentSize "$bytes"
next_bytes=$(((bytes / step_bytes + 1) * step_bytes))
log_mode="size"
else
printSizeProgress "$bytes"
fi
fi
else
# Floor the percentage rather than rounding so displayed completion
# never gets ahead of bytes actually written.
# Truncate to one decimal so progress is never reported early.
progress=$((bytes * 1000 / effective_total))
(( progress > 1000 )) && progress=1000
percent=$((progress / 10))
printf -v size '%d.%d%%' \
"$((progress / 10))" \
"$((progress % 10))"
if [[ "$output" == "log" ]]; then
if [[ "$log_mode" == "size" ]]; then
printSizeProgress "$bytes"
else
printPercentProgress "$percent"
fi
fi
# Do not update the web viewer until at least 0.1% is reached.
(( progress == 0 )) && write_html="N"
fi
if [[ "$write_html" == "Y" ]]; then
writeInfo "${body//(\[P\])/($size)}"
fi
fi
[[ "$final_pass" == "Y" ]] && break
sleep 1 &
wait $! || :
done done
+62 -18
View File
@@ -15,6 +15,10 @@ HOST_MODEL=$(strip "$HOST_MODEL")
HOST_SERIAL=$(strip "$HOST_SERIAL") HOST_SERIAL=$(strip "$HOST_SERIAL")
GUEST_SERIAL=$(strip "$GUEST_SERIAL") GUEST_SERIAL=$(strip "$GUEST_SERIAL")
HOST_PID="$QEMU_DIR/host.pid"
HOST_API_SOCKET="$QEMU_DIR/qemu-host-api.sock"
HOST_AGENT_SOCKET="$QEMU_DIR/qemu-host-agent.sock"
validateHostMac() { validateHostMac() {
local m local m
@@ -38,9 +42,13 @@ validateHostMac() {
buildHostArguments() { buildHostArguments() {
# qemu-host is a sidecar that bridges DSM's proprietary serial agent to
# Unix sockets used by shutdown and post-boot discovery helpers.
HOST_ARGS=() HOST_ARGS=()
HOST_ARGS+=("-cpu=$CPU_CORES") HOST_ARGS+=("-cpu=$CPU_CORES")
HOST_ARGS+=("-cpu_arch=$HOST_CPU") HOST_ARGS+=("-cpu_arch=$HOST_CPU")
HOST_ARGS+=("-api=$HOST_API_SOCKET")
HOST_ARGS+=("-addr=$HOST_AGENT_SOCKET")
[ -n "$HOST_MAC" ] && HOST_ARGS+=("-mac=$HOST_MAC") [ -n "$HOST_MAC" ] && HOST_ARGS+=("-mac=$HOST_MAC")
[ -n "$HOST_MODEL" ] && HOST_ARGS+=("-model=$HOST_MODEL") [ -n "$HOST_MODEL" ] && HOST_ARGS+=("-model=$HOST_MODEL")
@@ -52,30 +60,50 @@ buildHostArguments() {
startHostBinary() { 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 if enabled "$HOST_DEBUG"; then
set -x set -x
./host.bin "${HOST_ARGS[@]}" & ./host.bin "${HOST_ARGS[@]}" &
{ set +x; } 2>/dev/null { set +x; } 2>/dev/null
echo "$!" > "$HOST_PID" pid=$!
echo echo
else else
./host.bin "${HOST_ARGS[@]}" >/dev/null & ./host.bin "${HOST_ARGS[@]}" >/dev/null &
echo "$!" > "$HOST_PID" pid=$!
fi fi
printf '%s\n' "$pid" > "$HOST_PID"
return 0 return 0
} }
waitForPort() { waitForSocket() {
local port="$1" local socket="$1"
local exit_code="$2" local exit_code="$2"
local 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 ! readPidFile pid "$HOST_PID" || ! isAlive "$pid"; then
error "qemu-host exited unexpectedly!"
exit "$exit_code"
fi
if (( SECONDS >= deadline )); then
error "Failed to create qemu-host socket: $socket"
exit "$exit_code"
fi
while ! nc -z -w2 127.0.0.1 "$port" > /dev/null 2>&1; do
sleep 0.1 sleep 0.1
cnt=$((cnt + 1))
(( cnt > 50 )) && error "Failed to connect to qemu-host.." && exit "$exit_code"
done done
return 0 return 0
@@ -83,10 +111,30 @@ waitForPort() {
configureSerialPorts() { configureSerialPorts() {
# Configure serial ports local bus
SERIAL_OPTS="-serial mon:stdio \ bus=$(getPciBus)
-device virtio-serial-pci,id=virtio-serial0,bus=pcie.0,addr=0x3 \
-chardev socket,id=charchannel0,host=127.0.0.1,port=$CHR_PORT,reconnect=10 \ # 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"
MONITOR_SOCKET="$QEMU_DIR/monitor.sock"
SERIAL_OPTS="-chardev socket,id=console0,path=$CONSOLE_SOCKET,reconnect-ms=1000 \
-serial chardev:console0 \
-chardev socket,id=monitor0,path=$MONITOR_SOCKET,server=on,wait=off \
-mon chardev=monitor0,mode=readline"
else
SERIAL_OPTS="-serial mon:stdio"
fi
SERIAL_OPTS+=" \
-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" -device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=vchannel"
return 0 return 0
@@ -94,15 +142,11 @@ configureSerialPorts() {
validateHostMac validateHostMac
HOST_PID="$QEMU_DIR/host.pid"
buildHostArguments buildHostArguments
startHostBinary startHostBinary
sleep 0.2 waitForSocket "$HOST_API_SOCKET" 58
waitForSocket "$HOST_AGENT_SOCKET" 59
waitForPort "$COM_PORT" 58
waitForPort "$CHR_PORT" 59
configureSerialPorts configureSerialPorts
+115 -30
View File
@@ -1,41 +1,72 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
: "${COM_PORT:="2210"}" # Comm port
: "${WEB_PORT:="5000"}" # Webserver port : "${WEB_PORT:="5000"}" # Webserver port
: "${CHR_PORT:="12345"}" # Character port
: "${WSD_PORT:="8004"}" # Websockets port
# Sanitize port variables # Sanitize port variables
COM_PORT=$(strip "$COM_PORT")
WEB_PORT=$(strip "$WEB_PORT") WEB_PORT=$(strip "$WEB_PORT")
CHR_PORT=$(strip "$CHR_PORT")
WSD_PORT=$(strip "$WSD_PORT")
WEB_PID="/run/nginx.pid" WEB_PID="/run/nginx.pid"
WSD_LOG="/var/log/websocketd.log"
WSD_PID="$QEMU_DIR/websocketd.pid" WSD_PID="$QEMU_DIR/websocketd.pid"
WSD_SOCKET="$QEMU_DIR/status-ws.sock"
prepareWebFiles() { prepareWebFiles() {
cp -r /var/www/* "$QEMU_DIR" || return 1 cp -r /var/www/* "$QEMU_DIR" || return 1
rm -f "$WSD_PID" "$WEB_PID" || return 1 rm -f -- "$WSD_PID" "$WSD_SOCKET" "$WEB_PID" "$WSD_LOG" || return 1
return 0 return 0
} }
configureWebPorts() { configureWebPorts() {
sed -i "s/listen 5000 default_server;/listen $WEB_PORT default_server;/g" /etc/nginx/sites-enabled/web.conf if ! sed -i \
sed -i "s/proxy_pass http:\/\/127.0.0.1:8004\/;/proxy_pass http:\/\/127.0.0.1:$WSD_PORT\/;/g" /etc/nginx/sites-enabled/web.conf -e "s|listen 5000 default_server;|listen $WEB_PORT default_server;|g" \
/etc/nginx/sites-enabled/web.conf; then
error "Failed to configure webserver port!"
return 1
fi
return 0 return 0
} }
configureIpv6Listen() { configureIpv6Listen() {
# shellcheck disable=SC2143 # Use one dual-stack listener when IPv6 is active, avoiding separate IPv4
# and IPv6 sockets that can conflict on the same port.
if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]]; then if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]]; then
sed -i "s/listen $WEB_PORT default_server;/listen [::]:$WEB_PORT default_server ipv6only=off;/g" /etc/nginx/sites-enabled/web.conf
if ! sed -i \
"s/listen $WEB_PORT default_server;/listen [::]:$WEB_PORT default_server ipv6only=off;/g" \
/etc/nginx/sites-enabled/web.conf; then
error "Failed to configure IPv6 webserver listener!"
return 1
fi
fi
return 0
}
configureNginx() {
mkdir -p /etc/nginx/sites-enabled || return 1
rm -f /etc/nginx/sites-enabled/default || return 1
# TODO: Use setfacl to grant www-data access to the Unix sockets
# and restore unprivileged nginx workers.
if ! sed -i \
-e 's/^user .*/user root;/' \
-e 's/^worker_processes.*/worker_processes 1;/' \
/etc/nginx/nginx.conf; then
error "Failed to configure nginx!"
return 1
fi
if ! cp /etc/nginx/default.conf /etc/nginx/sites-enabled/web.conf; then
error "Failed to copy nginx config!"
return 1
fi fi
return 0 return 0
@@ -43,15 +74,31 @@ configureIpv6Listen() {
configureWebServer() { configureWebServer() {
mkdir -p /etc/nginx/sites-enabled configureNginx || return 1
cp /etc/nginx/default.conf /etc/nginx/sites-enabled/web.conf
configureWebPorts || return 1 configureWebPorts || return 1
configureIpv6Listen || return 1 configureIpv6Listen || return 1
return 0 return 0
} }
stopWebServer() {
local pid
if readPidFile pid "$WEB_PID"; then
pKill "$pid" 2
# Escalate only after the normal termination grace period; stale nginx
# processes would otherwise keep the configured web port occupied.
if isAlive "$pid"; then
kill -9 -- "$pid" 2>/dev/null || :
fi
fi
rm -f -- "$WEB_PID"
return 0
}
startWebServer() { startWebServer() {
# Start webserver # Start webserver
@@ -60,41 +107,79 @@ startWebServer() {
return 0 return 0
} }
stopWebsocketServer() {
local pid
if readPidFile pid "$WSD_PID"; then
pKill "$pid" 2
if isAlive "$pid"; then
kill -9 -- "$pid" 2>/dev/null || :
fi
fi
rm -f -- "$WSD_PID" "$WSD_SOCKET"
return 0
}
startWebsocketServer() { startWebsocketServer() {
local log="/var/log/websocketd.log"
rm -f "$log"
# Start websocket server # Start websocket server
websocketd --address 127.0.0.1 --port="$WSD_PORT" /run/socket.sh > "$log" 2>&1 & websocketd \
--unixsocket="$WSD_SOCKET" \
/run/socket.sh \
>"$WSD_LOG" 2>&1 &
local pid=$! local pid=$!
if ! echo "$pid" > "$WSD_PID"; then if ! echo "$pid" > "$WSD_PID"; then
kill "$pid" 2>/dev/null || : kill "$pid" 2>/dev/null || :
rm -f -- "$WSD_PID"
return 1 return 1
fi fi
sleep 0.1 local i
for (( i = 1; i <= 50; i++ )); do
if ! isAlive "$pid"; then if ! isAlive "$pid"; then
rm -f "$WSD_PID" rm -f -- "$WSD_PID" "$WSD_SOCKET"
[ -s "$log" ] && cat "$log" >&2 [ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
error "Failed to start websocket server!" error "Failed to start websocket server!"
return 1 return 1
fi
[ -S "$WSD_SOCKET" ] && return 0
sleep 0.1
done
pKill "$pid" 2
if isAlive "$pid"; then
kill -9 -- "$pid" 2>/dev/null || :
fi fi
return 0 rm -f -- "$WSD_PID" "$WSD_SOCKET"
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
error "Websocket server did not create its socket!"
return 1
} }
prepareWebFiles || return 1 prepareWebFiles
html "Starting $APP for $ENGINE..." html "Starting $APP for $ENGINE..."
disabled "${WEB:-}" && return 0 disabled "${WEB:-}" && return 0
configureWebServer || return 1 configureWebServer
startWebServer || return 1 if startWebServer && startWebsocketServer; then
startWebsocketServer || return 1 return 0
fi
return 0 stopWebsocketServer || :
stopWebServer || :
return 1
+22 -5
View File
@@ -3,6 +3,8 @@ set -Eeuo pipefail
lastmsg="" lastmsg=""
path="/run/shm/msg.html" path="/run/shm/msg.html"
dir=$(dirname -- "$path")
name=$(basename -- "$path")
refresh() { refresh() {
@@ -16,16 +18,31 @@ refresh() {
[[ "$msg" == "$lastmsg" ]] && return 0 [[ "$msg" == "$lastmsg" ]] && return 0
lastmsg="$msg" lastmsg="$msg"
# websocketd clients interpret s: as a status update and c: as a command;
# suppress unchanged status to avoid redundant browser work.
echo "s: $msg" echo "s: $msg"
return 0 return 0
} }
refresh refresh
inotifywait -m "$path" | inotifywait \
while read -r fp event fn; do -m -q \
-e close_write,moved_to,delete \
--format '%e %f' \
"$dir" |
while read -r event file; do
[[ "$file" == "$name" ]] || continue
case "${event,,}" in case "${event,,}" in
"modify"* ) refresh ;; "delete"* )
"delete_self" ) echo "c: vnc" ;; echo "c: vnc" ;;
esac # moved_to covers the atomic replacement used by html()/writeAtomic(),
# while close_write handles direct writers.
"close_write"* | "moved_to"* )
refresh ;;
esac
done done
+194 -45
View File
@@ -7,6 +7,79 @@ info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "${1:-}" "\E[0m\n"; }
error () { printf "%b%s%b" "\E[1;31m " "ERROR: ${1:-}" "\E[0m\n" >&2; } error () { printf "%b%s%b" "\E[1;31m " "ERROR: ${1:-}" "\E[0m\n" >&2; }
warn () { printf "%b%s%b" "\E[1;31m " "Warning: ${1:-}" "\E[0m\n" >&2; } warn () { printf "%b%s%b" "\E[1;31m " "Warning: ${1:-}" "\E[0m\n" >&2; }
app() {
echo "Virtual DSM"
return 0
}
readPidFile() {
local -n _pid="$1"
_pid=""
if ! _pid=$(cat -- "$2" 2>/dev/null); then
_pid=""
return 1
fi
# Reject empty, zero, or nonnumeric pidfiles so cleanup can never signal an
# unintended process group.
if [[ ! "$_pid" =~ ^[1-9][0-9]*$ ]]; then
_pid=""
return 1
fi
return 0
}
hasFlag() {
# Match a whitespace-delimited token in /proc/cpuinfo
grep -m1 '^flags[[:space:]]*:' /proc/cpuinfo | grep -Fqw -- "$1"
}
hasFeature() {
# Match a whitespace-delimited token in /proc/cpuinfo
grep -m1 '^Features[[:space:]]*:' /proc/cpuinfo | grep -Fqw -- "$1"
}
isAmdCpu() {
local vendor
vendor=$(awk -F ': *' '/^vendor_id/{print $2; exit}' /proc/cpuinfo)
[[ "$vendor" == "AuthenticAMD" ]]
}
getPciBus() {
local machine="${1:-${MACHINE:-q35}}"
if [ -n "${PCI_BUS:-}" ]; then
echo "$PCI_BUS"
return 0
fi
case "${machine,,}" in
pc|pc-i440fx*) echo "pci.0" ;;
*) echo "pcie.0" ;;
esac
return 0
}
interactive() {
# A TTY on stdin is insufficient when /dev/tty is unavailable; require both
# before enabling interactive console handling.
[ -t 0 ] && : 2>/dev/null </dev/tty >/dev/tty
}
strip() { strip() {
local value="${1:-}" local value="${1:-}"
@@ -90,14 +163,13 @@ isAlive() {
waitPid() { waitPid() {
local i=0
local pid="$1" local pid="$1"
local timeout="${2:-10}" local timeout="${2:-10}"
local deadline=$((SECONDS + timeout))
while [ -n "$pid" ] && isAlive "$pid"; do while [ -n "$pid" ] && isAlive "$pid"; do
(( SECONDS >= deadline )) && return 1
sleep 0.2 sleep 0.2
i=$((i + 1))
(( i >= timeout * 5 )) && return 1
done done
return 0 return 0
@@ -105,19 +177,16 @@ waitPid() {
waitPidFile() { waitPidFile() {
local i=0 local pid
local pid=""
local file="$1" local file="$1"
local timeout="${2:-10}" local timeout="${2:-10}"
local deadline=$((SECONDS + timeout))
[ ! -s "$file" ] && return 0 readPidFile pid "$file" || return 0
! read -r pid <"$file" && return 0
[ -z "$pid" ] && return 0
while [ -s "$file" ] && isAlive "$pid"; do while [ -s "$file" ] && isAlive "$pid"; do
(( SECONDS >= deadline )) && return 1
sleep 0.2 sleep 0.2
i=$((i + 1))
(( i >= timeout * 5 )) && return 1
done done
rm -f -- "$file" rm -f -- "$file"
@@ -140,19 +209,19 @@ pKill() {
fWait() { fWait() {
local i=0
local name="$1" local name="$1"
local timeout="${2:-10}" local timeout="${2:-10}"
local deadline=$((SECONDS + timeout))
[ -z "$name" ] && return 0 [ -z "$name" ] && return 0
while pgrep -f -l "$name" >/dev/null; do while pgrep -f -l "$name" >/dev/null; do
sleep 0.2 if (( SECONDS >= deadline )); then
i=$((i + 1))
if (( i >= timeout * 5 )); then
warn "Timed out while waiting for process: $name" warn "Timed out while waiting for process: $name"
break break
fi fi
sleep 0.2
done done
return 0 return 0
@@ -173,12 +242,10 @@ fKill() {
sKill() { sKill() {
local pid="" local pid
local file="$1" local file="$1"
[ ! -s "$file" ] && return 0 readPidFile pid "$file" || return 0
! read -r pid <"$file" && return 0
[ -z "$pid" ] && return 0
if isAlive "$pid"; then if isAlive "$pid"; then
{ kill -15 -- "$pid" || :; } 2>/dev/null { kill -15 -- "$pid" || :; } 2>/dev/null
@@ -212,11 +279,13 @@ setOwner() {
[ ! -f "$file" ] && return 1 [ ! -f "$file" ] && return 1
# Match generated files to the owner of their bind-mounted parent directory
# instead of assuming a fixed container or host UID.
dir=$(dirname -- "$file") dir=$(dirname -- "$file")
uid=$(stat -c '%u' "$dir") || return 1 uid=$(stat -c '%u' "$dir") || return 1
gid=$(stat -c '%g' "$dir") || return 1 gid=$(stat -c '%g' "$dir") || return 1
! chown "$uid:$gid" "$file" && return 1 chown "$uid:$gid" "$file" || return 1
return 0 return 0
} }
@@ -227,7 +296,7 @@ makeDir() {
local dir uid gid local dir uid gid
[ -d "$path" ] && return 0 [ -d "$path" ] && return 0
! mkdir -p "$path" && return 1 mkdir -p "$path" || return 1
dir=$(dirname -- "$path") dir=$(dirname -- "$path")
@@ -244,6 +313,64 @@ makeDir() {
return 0 return 0
} }
finiteMemoryLimit() {
local limit="$1"
# cgroup v1 commonly reports this enormous sentinel for an unlimited memory
# limit; compare as decimal strings to avoid shell integer overflow.
local sentinel="4611686018427387904"
local i
[[ "$limit" =~ ^[0-9]+$ ]] || return 1
(( ${#limit} < ${#sentinel} )) && return 0
(( ${#limit} > ${#sentinel} )) && return 1
for (( i=0; i<${#sentinel}; i++ )); do
local left="${limit:i:1}"
local right="${sentinel:i:1}"
(( left < right )) && return 0
(( left > right )) && return 1
done
return 1
}
getMemoryInfo() {
local host_total
local host_avail
local limit=""
local current=""
host_total=$(free -b | awk '/^Mem:/ {print $2; exit}')
host_avail=$(free -b | awk '/^Mem:/ {print $7; exit}')
RAM_TOTAL="$host_total"
RAM_AVAIL="$host_avail"
if [ -r /sys/fs/cgroup/memory.max ] && [ -r /sys/fs/cgroup/memory.current ]; then
limit=$(< /sys/fs/cgroup/memory.max)
current=$(< /sys/fs/cgroup/memory.current)
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ] && [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then
limit=$(< /sys/fs/cgroup/memory/memory.limit_in_bytes)
current=$(< /sys/fs/cgroup/memory/memory.usage_in_bytes)
fi
# Use the tighter of host availability and the container's remaining cgroup
# allowance so RAM sizing cannot exceed either boundary.
if finiteMemoryLimit "$limit" && [[ "$current" =~ ^[0-9]+$ ]]; then
(( limit < RAM_TOTAL )) && RAM_TOTAL="$limit"
local available=$(( limit - current ))
(( available < 0 )) && available=0
(( available < RAM_AVAIL )) && RAM_AVAIL="$available"
fi
return 0
}
stateFile() { stateFile() {
local name="$1" local name="$1"
@@ -272,6 +399,27 @@ writeFile() {
return 0 return 0
} }
writeAtomic() {
local path="$1"
local content="$2"
# Use a per-process temporary file and rename so readers see either the old
# complete value or the new complete value.
local tmp="${path}.${BASHPID}.tmp"
if ! printf '%s\n' "$content" > "$tmp"; then
rm -f -- "$tmp"
return 1
fi
if ! mv -f -- "$tmp" "$path"; then
rm -f -- "$tmp"
return 1
fi
return 0
}
readFile() { readFile() {
local path="$1" local path="$1"
@@ -321,6 +469,8 @@ restoreState() {
local prefix="${4:-$PROCESS}" local prefix="${4:-$PROCESS}"
local value local value
# Persistent state fills only unset variables unless force is requested,
# preserving explicit environment overrides.
if ! enabled "$force"; then if ! enabled "$force"; then
[ -z "${!var:-}" ] || return 0 [ -z "${!var:-}" ] || return 0
fi fi
@@ -334,8 +484,7 @@ restoreState() {
escape () { escape () {
local s local s=${1//&/\&amp;}
s=${1//&/\&amp;}
s=${s//</\&lt;} s=${s//</\&lt;}
s=${s//>/\&gt;} s=${s//>/\&gt;}
s=${s//'"'/\&quot;} s=${s//'"'/\&quot;}
@@ -345,11 +494,23 @@ escape () {
return 0 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() { html() {
local title local title
local body local body
local script local script="${2:-}"
local footer local footer
title=$(escape "$APP") title=$(escape "$APP")
@@ -361,8 +522,6 @@ html() {
body="<p class=\"loading\">${body/.../}</p>" body="<p class=\"loading\">${body/.../}</p>"
fi fi
[ -n "${2:-}" ] && script="$2" || script=""
local HTML local HTML
HTML=$(<"$TEMPLATE") HTML=$(<"$TEMPLATE")
HTML="${HTML/\[1\]/$title}" HTML="${HTML/\[1\]/$title}"
@@ -371,8 +530,10 @@ html() {
HTML="${HTML/\[4\]/$footer}" HTML="${HTML/\[4\]/$footer}"
HTML="${HTML/\[5\]/$FOOTER2}" HTML="${HTML/\[5\]/$FOOTER2}"
echo "$HTML" > "$PAGE" || return 1 # Publish both the full page and websocket fragment atomically because nginx
echo "$body" > "$INFO" || return 1 # and websocketd may read them concurrently.
writeAtomic "$PAGE" "$HTML" || return 1
writeAtomic "$INFO" "$body" || return 1
return 0 return 0
} }
@@ -415,29 +576,13 @@ cpu() {
return 0 return 0
} }
hasDisk() {
enabled "${DISK_DISABLE:-}" && return 1
[ -b "/disk" ] && return 0
[ -b "/disk1" ] && return 0
[ -b "/dev/disk1" ] && return 0
[ -b "${DEVICE:-}" ] && return 0
[ -z "${DISK_NAME:-}" ] && DISK_NAME="data"
[ -s "$STORAGE/$DISK_NAME.img" ] && return 0
[ -s "$STORAGE/$DISK_NAME.qcow2" ] && return 0
return 1
}
getCountry() { getCountry() {
local url=$1 local url=$1
local query=$2 local query=$2
local rc json result local json result
{ json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); rc=$?; } || : { json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); local rc=$?; } || :
(( rc != 0 )) && return 0 (( rc != 0 )) && return 0
{ result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || : { result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || :
@@ -460,6 +605,8 @@ setCountry() {
[[ "${TZ,,}" == "asia/shanghai" ]] && COUNTRY="CN" [[ "${TZ,,}" == "asia/shanghai" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/chongqing" ]] && COUNTRY="CN" [[ "${TZ,,}" == "asia/chongqing" ]] && COUNTRY="CN"
# Country detection is best-effort and tries independent services in order;
# failure leaves mirror selection at its global default.
[ -z "$COUNTRY" ] && getCountry "https://api.ipapi.is" ".location.country_code" [ -z "$COUNTRY" ] && getCountry "https://api.ipapi.is" ".location.country_code"
[ -z "$COUNTRY" ] && getCountry "https://ifconfig.co/json" ".country_iso" [ -z "$COUNTRY" ] && getCountry "https://ifconfig.co/json" ".country_iso"
[ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code" [ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code"
@@ -484,6 +631,8 @@ addPackage() {
[ -z "$COUNTRY" ] && setCountry [ -z "$COUNTRY" ] && setCountry
# Use a mainland mirror only for on-demand package installation, avoiding
# slow or inaccessible Debian endpoints in that region.
if [[ "${COUNTRY^^}" == "CN" ]]; then if [[ "${COUNTRY^^}" == "CN" ]]; then
sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
fi fi
+2 -2
View File
@@ -39,6 +39,6 @@ server {
proxy_read_timeout 3600s; proxy_read_timeout 3600s;
proxy_send_timeout 3600s; proxy_send_timeout 3600s;
proxy_pass http://127.0.0.1:8004/; proxy_pass http://unix:/run/shm/status-ws.sock:/;
} }
} }
+48 -8
View File
@@ -1,15 +1,27 @@
var timer;
var request; var request;
var booting = false; var booting = false;
var interval = 1000; var interval = 1000;
function abortRequest() {
if (!request) {
return false;
}
request.onreadystatechange = null;
request.abort();
request = null;
return true;
}
function getInfo() { function getInfo() {
var url = "msg.html"; var url = "msg.html";
try { try {
if (request) { abortRequest();
request.abort();
}
if (window.XMLHttpRequest) { if (window.XMLHttpRequest) {
request = new XMLHttpRequest(); request = new XMLHttpRequest();
@@ -56,7 +68,17 @@ function processInfo() {
return true; return true;
} }
var msg = request.responseText; var response = request;
request = null;
var status = response.status;
if (status == 502 || status == 503 || status == 504) {
schedule();
return true;
}
var msg = response.responseText;
if (msg == null || msg.length == 0) { if (msg == null || msg.length == 0) {
if (booting) { if (booting) {
@@ -68,9 +90,9 @@ function processInfo() {
return false; return false;
} }
var notFound = (request.status == 404); var notFound = (status == 404);
if (request.status == 200) { if (status == 200) {
if (msg.toLowerCase().indexOf("<html>") !== -1) { if (msg.toLowerCase().indexOf("<html>") !== -1) {
notFound = true; notFound = true;
} else { } else {
@@ -88,7 +110,7 @@ function processInfo() {
return true; return true;
} }
setError("Error: Received statuscode " + request.status); setError("Error: Received statuscode " + status);
return false; return false;
} catch (e) { } catch (e) {
@@ -157,7 +179,9 @@ function setError(text) {
} }
function schedule() { function schedule() {
setTimeout(getInfo, interval);
clearTimeout(timer);
timer = setTimeout(getInfo, interval);
} }
function reload() { function reload() {
@@ -179,11 +203,27 @@ function connect() {
switch (cmd) { switch (cmd) {
case "s": case "s":
var aborted = abortRequest();
processMsg(msg); processMsg(msg);
if (aborted &&
msg.toLowerCase().indexOf("href=") == -1) {
schedule();
}
break; break;
case "e": case "e":
if (abortRequest()) {
schedule();
}
setError(msg); setError(msg);
break; break;
default: default:
console.warn("Unknown event: " + cmd); console.warn("Unknown event: " + cmd);
break; break;