mirror of
https://github.com/vdsm/virtual-dsm.git
synced 2026-08-29 04:07:02 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c70b93c2b6 | ||
|
|
4c700e87fe | ||
|
|
030255004f | ||
|
|
e64f9f2c39 | ||
|
|
537fc97d53 | ||
|
|
438e992653 | ||
|
|
615fad2a0d | ||
|
|
aac543e5c7 | ||
|
|
fec4f876e7 | ||
|
|
dced815c06 | ||
|
|
51d6f3aeca | ||
|
|
4544620d48 | ||
|
|
de8468161f | ||
|
|
18d6be8210 | ||
|
|
e5b8cf3bf8 | ||
|
|
ec12039f43 | ||
|
|
3c7c3ca1b1 | ||
|
|
2b27f32cd4 | ||
|
|
55d1d50284 | ||
|
|
784b73b5b5 | ||
|
|
85f00bb9cc | ||
|
|
efe8732e47 | ||
|
|
f6871dd61b | ||
|
|
a713b728ff | ||
|
|
0d74c6ac80 | ||
|
|
63e4529eff | ||
|
|
afd848cebb | ||
|
|
695b075130 | ||
|
|
7eff53e722 | ||
|
|
bcaf0e5980 | ||
|
|
faa820c2cc | ||
|
|
81e477fcc4 | ||
|
|
abd3a1c3df | ||
|
|
3b59bcd284 | ||
|
|
f1b56f394a | ||
|
|
1a983ebcd1 | ||
|
|
41c76198fa | ||
|
|
222b5649b0 | ||
|
|
e14fd4b711 | ||
|
|
6fe19a8af4 | ||
|
|
072b5c3070 | ||
|
|
89dbaeb2a4 | ||
|
|
02f76b44cd | ||
|
|
99d6b8f830 | ||
|
|
e6b68b2f08 | ||
|
|
7a8f0041e7 | ||
|
|
64809bb4d4 | ||
|
|
f08b2cbd99 | ||
|
|
18d9ecd434 | ||
|
|
6944293a32 | ||
|
|
d807a365ff | ||
|
|
066af83d07 | ||
|
|
81536818df | ||
|
|
949c288565 | ||
|
|
5d5bbbcf4d | ||
|
|
82a62c0240 | ||
|
|
5399e1d463 | ||
|
|
c7e7b60f01 | ||
|
|
f1defa6890 | ||
|
|
a7fb161974 | ||
|
|
dcdf5e5293 | ||
|
|
1eb0db66ba | ||
|
|
3eec8e03ed | ||
|
|
5483981ed4 | ||
|
|
62330a5bf4 | ||
|
|
2b0b59dfba | ||
|
|
b680ff9dd8 | ||
|
|
5c889b272d | ||
|
|
585ebb3f53 | ||
|
|
401307b981 | ||
|
|
46820fe6eb | ||
|
|
9452d48694 | ||
|
|
5c3fed1387 | ||
|
|
797411ae7f | ||
|
|
e81c509208 | ||
|
|
ace8614a50 | ||
|
|
8263b3a737 | ||
|
|
61c6142988 | ||
|
|
5d26338bb3 | ||
|
|
084d475cde | ||
|
|
c36b0dcb00 | ||
|
|
6e9993742b | ||
|
|
6946af0212 | ||
|
|
0a9361d62f | ||
|
|
7cbfcd44a6 | ||
|
|
6bcacf5e59 | ||
|
|
3bea87f087 | ||
|
|
f01e1564f7 | ||
|
|
cf8cdbf0c1 | ||
|
|
0813494ee0 | ||
|
|
0d71e30f50 | ||
|
|
c203deab9a | ||
|
|
9cf31df266 | ||
|
|
0cd08f2ee1 | ||
|
|
a32d215e90 | ||
|
|
c1f9a690f9 | ||
|
|
5de504deb3 | ||
|
|
250eb18238 | ||
|
|
ca80da59bd | ||
|
|
9cd86dfe82 | ||
|
|
5bb93651f7 | ||
|
|
0c268308a4 | ||
|
|
7b741d3e4b | ||
|
|
f1d29212ae | ||
|
|
fdee9f89a3 | ||
|
|
210a360797 | ||
|
|
991bb2627a | ||
|
|
764a186836 | ||
|
|
726786a98a | ||
|
|
2caf97d407 | ||
|
|
0e67286599 | ||
|
|
2b9fc80275 | ||
|
|
0d88774bd3 | ||
|
|
fa23052ed4 | ||
|
|
cc3267f258 | ||
|
|
fd002564a6 | ||
|
|
2703a97cfa | ||
|
|
29e2d034bf | ||
|
|
cbe3644335 | ||
|
|
c9a2a6966b | ||
|
|
498248f43e | ||
|
|
8963228b1c | ||
|
|
827d6d9e10 | ||
|
|
1c577abef4 | ||
|
|
20fd1a975c | ||
|
|
770774680c | ||
|
|
0c75aa81fe | ||
|
|
36ec57ac15 | ||
|
|
9c488c52b9 | ||
|
|
2ba756260f | ||
|
|
b306012743 | ||
|
|
61b3ee7e01 | ||
|
|
245036f844 | ||
|
|
68cadfe035 | ||
|
|
96102832b1 | ||
|
|
211a56bf73 | ||
|
|
506fb7b114 | ||
|
|
bcc3393b7f | ||
|
|
45c0f91d6e | ||
|
|
a1910546a6 | ||
|
|
637ed0e3a6 | ||
|
|
954a6076ad | ||
|
|
415aaa3038 | ||
|
|
c87e45719b | ||
|
|
102945ab48 | ||
|
|
1c3d490303 | ||
|
|
73fa70f52f | ||
|
|
7b095702fc | ||
|
|
f82b2769d3 | ||
|
|
cafe619028 | ||
|
|
e94a0c4a52 | ||
|
|
d6a353df0e | ||
|
|
050812e996 | ||
|
|
75280e9aa9 | ||
|
|
8dcc8ccee1 | ||
|
|
ba866e9e75 | ||
|
|
06e7a8edb9 | ||
|
|
495c67734a | ||
|
|
1557735331 | ||
|
|
f45b308c78 | ||
|
|
d5821cd782 | ||
|
|
312816b18e | ||
|
|
f42a3f1662 | ||
|
|
558e5022ca | ||
|
|
e03822f26e | ||
|
|
7b90ca30ba | ||
|
|
22c01bf2f1 | ||
|
|
1f601db8b0 | ||
|
|
e7e2c35cb7 | ||
|
|
50089112c0 | ||
|
|
f9538ce53b | ||
|
|
49d25a13d0 | ||
|
|
7c9d2b1fd5 | ||
|
|
b68d243b6a | ||
|
|
79e85f9fd1 | ||
|
|
476048a4ee | ||
|
|
f983dc3e46 | ||
|
|
38f9c49200 | ||
|
|
7157717a85 | ||
|
|
031674424d | ||
|
|
28e6f8da78 | ||
|
|
d5802607b5 | ||
|
|
7168f2f843 | ||
|
|
819dd29025 | ||
|
|
48ef47b85b | ||
|
|
10c902bc9b | ||
|
|
d6de10efe1 | ||
|
|
6d1d9d92b4 | ||
|
|
799649e78c | ||
|
|
12060c72b2 | ||
|
|
6eadd1b953 | ||
|
|
342ca4da03 | ||
|
|
b0b6190560 | ||
|
|
075410e4cf | ||
|
|
2a388b434b | ||
|
|
280c4037f3 | ||
|
|
007bdc735d | ||
|
|
41fc0a6a10 | ||
|
|
7753e4ac74 | ||
|
|
f74bf35202 | ||
|
|
aa472c96c6 | ||
|
|
0dcb880800 | ||
|
|
8daa6973b7 | ||
|
|
3884befa56 | ||
|
|
85b83f8eb1 | ||
|
|
f395dfaa66 | ||
|
|
b3124075ea | ||
|
|
aed909bdee | ||
|
|
d4cf5778e2 | ||
|
|
48de7dd00b | ||
|
|
686e64da3f | ||
|
|
7517dc8b3e | ||
|
|
660fe715e4 | ||
|
|
04ea29ec77 | ||
|
|
02c891cb16 | ||
|
|
0b5d21357e | ||
|
|
e0545b37d7 | ||
|
|
4161c21082 | ||
|
|
48d9a1771d | ||
|
|
471cdbb338 | ||
|
|
e77bca202b | ||
|
|
2e6c01e934 | ||
|
|
302c991c0c | ||
|
|
a89007ee03 | ||
|
|
8a89149d58 | ||
|
|
5e8bbc2868 | ||
|
|
4e48920309 | ||
|
|
8b145924b9 | ||
|
|
a0328e1e9c | ||
|
|
b7f5214a7b | ||
|
|
b0e4c4ac5f | ||
|
|
bbb67aac93 | ||
|
|
433c83b393 | ||
|
|
5577178eeb | ||
|
|
221b0242fa | ||
|
|
c05623f8af | ||
|
|
eb9884cc96 | ||
|
|
8e2490e6bc | ||
|
|
399243886e | ||
|
|
b0dbfcb805 | ||
|
|
dec2dc9230 | ||
|
|
2c44669d91 | ||
|
|
175d90aad7 | ||
|
|
e1e5f8f91d | ||
|
|
dff8abbe1e | ||
|
|
0f97091e61 | ||
|
|
ea0d7ee6cd | ||
|
|
75daa31d36 | ||
|
|
8c44319c45 | ||
|
|
dac574d933 | ||
|
|
fd4c5001e8 | ||
|
|
dfe0b23995 | ||
|
|
6158372eaa | ||
|
|
b1a778d7b2 | ||
|
|
bc0defd813 | ||
|
|
48e7a9fff0 | ||
|
|
c2fa58ef27 | ||
|
|
ea49cb144b | ||
|
|
b8e778a79d | ||
|
|
c70e12f0a2 | ||
|
|
6281205912 | ||
|
|
1ffc5c55b2 | ||
|
|
c3302e1720 | ||
|
|
25227944b5 | ||
|
|
06650e916a | ||
|
|
2e34dffed5 | ||
|
|
3db91f077f | ||
|
|
fae14f4dd9 | ||
|
|
d190b3ba87 | ||
|
|
554f3295cb | ||
|
|
cdc4689d6a | ||
|
|
21d18fd439 | ||
|
|
fd5ec52226 | ||
|
|
fc7ab22741 | ||
|
|
59cf59faa4 | ||
|
|
705c6ce7f1 | ||
|
|
f2937ab507 | ||
|
|
399829cf3c | ||
|
|
b694d6faf8 | ||
|
|
7acd1f6cdb | ||
|
|
09ca3bf118 | ||
|
|
6cac45c397 | ||
|
|
f18663d840 | ||
|
|
fefe1af9e6 | ||
|
|
87a8cf7513 | ||
|
|
7f31cb6023 | ||
|
|
138742c953 | ||
|
|
2c6efc45f2 | ||
|
|
24d795fbe3 | ||
|
|
7fae62d286 | ||
|
|
2135df07ea | ||
|
|
521beedf1c | ||
|
|
e362c9a8a9 | ||
|
|
78817ecfc1 | ||
|
|
8de7f56ff8 | ||
|
|
2e2017470e | ||
|
|
0a0cd98de3 | ||
|
|
17187dd23a | ||
|
|
10237b31cf | ||
|
|
6883efb857 | ||
|
|
1ca55cad8f | ||
|
|
f841eb1ef1 | ||
|
|
1ee49332f3 | ||
|
|
095e618785 | ||
|
|
57a902ad9f | ||
|
|
a7e229aaae | ||
|
|
10466d7851 | ||
|
|
238ebd273b | ||
|
|
0b08f4212e | ||
|
|
9aec5d20e3 | ||
|
|
a19bf2f066 | ||
|
|
bee68ee548 | ||
|
|
520c70ab22 | ||
|
|
67a7fbc94b | ||
|
|
1b8054e847 | ||
|
|
631a558a9f | ||
|
|
d5805b4e08 | ||
|
|
1dc15ce2e0 | ||
|
|
4145e811df | ||
|
|
fe8615d7d3 | ||
|
|
27ea5506bc | ||
|
|
394131a0d7 | ||
|
|
4d0d16023f | ||
|
|
60dd794b29 | ||
|
|
f13d104968 | ||
|
|
914099fd12 | ||
|
|
639725957d | ||
|
|
64bbe82c4a | ||
|
|
73a13e5697 | ||
|
|
e8ec5fb8ed | ||
|
|
0b500ca377 | ||
|
|
df58745ed8 | ||
|
|
55e8ab4930 | ||
|
|
4882fd3bf6 | ||
|
|
3e69f4bcc9 | ||
|
|
39428909d6 | ||
|
|
71d2ed40db | ||
|
|
9ad21a28a6 | ||
|
|
0c7acd6e71 | ||
|
|
ad26129375 | ||
|
|
4e7822fedd | ||
|
|
4dfcbe4ab1 | ||
|
|
8fb6f1f9ad | ||
|
|
11d4fafa6d | ||
|
|
be15557798 | ||
|
|
1e83757494 | ||
|
|
fad463a439 | ||
|
|
608563d029 | ||
|
|
5fcd22b09f | ||
|
|
65548da2ef | ||
|
|
111e513dac | ||
|
|
7fce3a98fe | ||
|
|
4018eeadb4 | ||
|
|
a8e4647fa6 | ||
|
|
36d3fca4fc | ||
|
|
89b28f36d3 | ||
|
|
b4f7d70a7a | ||
|
|
902bafbd0c | ||
|
|
7ad5c7fa70 | ||
|
|
4d29f056b7 | ||
|
|
36af9a3483 | ||
|
|
5e6f931433 | ||
|
|
beadfe720c | ||
|
|
92b4bfc383 | ||
|
|
092ed23085 | ||
|
|
c70fa3d00a | ||
|
|
fea0ba09f6 | ||
|
|
7d2af63eac | ||
|
|
2e73bf560e | ||
|
|
bfc8d7a9c6 | ||
|
|
a1e9936572 | ||
|
|
3675a50129 | ||
|
|
91229152bd | ||
|
|
a1993e590a | ||
|
|
52fe712b6f | ||
|
|
7f400b6b59 | ||
|
|
9cb88a99e6 | ||
|
|
b967a471b5 | ||
|
|
f40127df01 | ||
|
|
8c5e0ee274 | ||
|
|
2adf0b292b | ||
|
|
1c9b793c75 | ||
|
|
00c4ef7795 | ||
|
|
619657adf2 |
@@ -0,0 +1,19 @@
|
|||||||
|
services:
|
||||||
|
dsm:
|
||||||
|
container_name: dsm
|
||||||
|
image: ghcr.io/vdsm/virtual-dsm
|
||||||
|
environment:
|
||||||
|
RAM_SIZE: "half"
|
||||||
|
DISK_SIZE: "max"
|
||||||
|
CPU_CORES: "max"
|
||||||
|
devices:
|
||||||
|
- /dev/kvm
|
||||||
|
- /dev/net/tun
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
ports:
|
||||||
|
- 5000:5000
|
||||||
|
volumes:
|
||||||
|
- ./dsm:/storage
|
||||||
|
restart: on-failure
|
||||||
|
stop_grace_period: 2m
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "Virtual DSM",
|
||||||
|
"service": "dsm",
|
||||||
|
"forwardPorts": [5000],
|
||||||
|
"portsAttributes": {
|
||||||
|
"5000": {
|
||||||
|
"label": "Web",
|
||||||
|
"onAutoForward": "notify"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"otherPortsAttributes": {
|
||||||
|
"onAutoForward": "ignore"
|
||||||
|
},
|
||||||
|
"dockerComposeFile": "codespaces.yml"
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
.dockerignore
|
.dockerignore
|
||||||
|
.devcontainer
|
||||||
.git
|
.git
|
||||||
.github
|
.github
|
||||||
.gitignore
|
.gitignore
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Docker compose
|
label: Docker compose
|
||||||
description: The compose file (or otherwise the `docker run` command used).
|
description: The compose file (or otherwise the `docker run` command used).
|
||||||
|
render: yaml
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -28,6 +29,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Docker log
|
label: Docker log
|
||||||
description: The logfile of the container (as shown by `docker logs dsm`).
|
description: The logfile of the container (as shown by `docker logs dsm`).
|
||||||
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Docker compose
|
label: Docker compose
|
||||||
description: The compose file (or otherwise the `docker run` command used).
|
description: The compose file (or otherwise the `docker run` command used).
|
||||||
|
render: yaml
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -30,6 +31,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Docker log
|
label: Docker log
|
||||||
description: The logfile of the container (as shown by `docker logs dsm`).
|
description: The logfile of the container (as shown by `docker logs dsm`).
|
||||||
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -2,20 +2,6 @@ name: Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
paths-ignore:
|
|
||||||
- '**/*.md'
|
|
||||||
- '**/*.yml'
|
|
||||||
- '**/*.js'
|
|
||||||
- '**/*.css'
|
|
||||||
- '**/*.html'
|
|
||||||
- 'web/**'
|
|
||||||
- '.gitignore'
|
|
||||||
- '.dockerignore'
|
|
||||||
- '.github/**'
|
|
||||||
- '.github/workflows/**'
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: build
|
group: build
|
||||||
@@ -36,13 +22,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
-
|
-
|
||||||
name: Docker metadata
|
name: Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
context: git
|
context: git
|
||||||
images: |
|
images: |
|
||||||
@@ -57,23 +43,23 @@ jobs:
|
|||||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
-
|
-
|
||||||
name: Login into Docker Hub
|
name: Login into Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
-
|
-
|
||||||
name: Login to GitHub Container Registry
|
name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
-
|
-
|
||||||
name: Build Docker image
|
name: Build Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
|||||||
+27
-14
@@ -1,5 +1,7 @@
|
|||||||
on: [workflow_call]
|
on: [workflow_call]
|
||||||
|
|
||||||
name: "Check"
|
name: "Check"
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -7,18 +9,29 @@ jobs:
|
|||||||
name: shellcheck
|
name: shellcheck
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
|
||||||
name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
-
|
-
|
||||||
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.1.0
|
env:
|
||||||
with:
|
SHELLCHECK_OPTS: >-
|
||||||
dockerfile: Dockerfile
|
-x
|
||||||
ignore: DL3008,DL3003,DL3006
|
--source-path=src
|
||||||
failure-threshold: warning
|
-e SC1091
|
||||||
|
-e SC2001
|
||||||
|
-e SC2034
|
||||||
|
-e SC2317
|
||||||
|
-e SC2153
|
||||||
|
-
|
||||||
|
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
|
||||||
|
|||||||
+12
-10
@@ -12,13 +12,15 @@ jobs:
|
|||||||
dockerHubDescription:
|
dockerHubDescription:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
-
|
||||||
-
|
name: Checkout repo
|
||||||
name: Docker Hub Description
|
uses: actions/checkout@v7
|
||||||
uses: peter-evans/dockerhub-description@v4
|
-
|
||||||
with:
|
name: Docker Hub Description
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
uses: peter-evans/dockerhub-description@v5
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
with:
|
||||||
repository: ${{ secrets.DOCKERHUB_REPO }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
short-description: ${{ github.event.repository.description }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
readme-filepath: ./readme.md
|
repository: ${{ secrets.DOCKERHUB_REPO }}
|
||||||
|
short-description: ${{ github.event.repository.description }}
|
||||||
|
readme-filepath: ./readme.md
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
name: "Review"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
checks: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
review:
|
||||||
|
name: review
|
||||||
|
uses: action-pack/.github/.github/workflows/review.yml@master
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
|
||||||
- '**/*.sh'
|
|
||||||
- 'Dockerfile'
|
|
||||||
- '.github/workflows/test.yml'
|
|
||||||
- '.github/workflows/check.yml'
|
|
||||||
|
|
||||||
name: "Test"
|
name: "Test"
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|||||||
+268
-1
@@ -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
|
||||||
|
|||||||
+67
-43
@@ -1,63 +1,87 @@
|
|||||||
FROM qemux/qemu-host:2.05 AS builder
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
# FROM golang as builder
|
|
||||||
# WORKDIR /
|
|
||||||
# RUN git clone https://github.com/qemus/qemu-host.git
|
|
||||||
# WORKDIR /qemu-host/src
|
|
||||||
# RUN go mod download
|
|
||||||
# RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /qemu-host.bin .
|
|
||||||
|
|
||||||
|
FROM qemux/qemu-host:2.06 AS builder
|
||||||
FROM debian:trixie-slim
|
FROM debian:trixie-slim
|
||||||
|
|
||||||
|
ARG TARGETARCH
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
ARG VERSION_ARG="0.0"
|
ARG VERSION_ARG="0.0"
|
||||||
|
ARG VERSION_WSD="0.4.2"
|
||||||
|
ARG VERSION_CSTRUCT="4.7"
|
||||||
|
ARG VERSION_PASST="2026_07_28"
|
||||||
|
|
||||||
ARG DEBCONF_NOWARNINGS="yes"
|
ARG DEBCONF_NOWARNINGS="yes"
|
||||||
ARG DEBIAN_FRONTEND="noninteractive"
|
ARG DEBIAN_FRONTEND="noninteractive"
|
||||||
ARG DEBCONF_NONINTERACTIVE_SEEN="true"
|
ARG DEBCONF_NONINTERACTIVE_SEEN="true"
|
||||||
|
|
||||||
RUN set -eu && extra="" && \
|
RUN <<EOF
|
||||||
if [ "$TARGETPLATFORM" != "linux/amd64" ]; then extra="qemu-user"; fi && \
|
set -eu
|
||||||
apt-get update && \
|
|
||||||
apt-get --no-install-recommends -y install \
|
apt-get update
|
||||||
jq \
|
apt-get --no-install-recommends -y install \
|
||||||
tini \
|
jq \
|
||||||
curl \
|
tini \
|
||||||
cpio \
|
curl \
|
||||||
wget \
|
wget \
|
||||||
fdisk \
|
fdisk \
|
||||||
unzip \
|
unzip \
|
||||||
nginx \
|
nginx \
|
||||||
procps \
|
procps \
|
||||||
xz-utils \
|
ipcalc \
|
||||||
iptables \
|
ethtool \
|
||||||
iproute2 \
|
xz-utils \
|
||||||
apt-utils \
|
iptables \
|
||||||
dnsmasq \
|
iproute2 \
|
||||||
fakeroot \
|
dnsmasq \
|
||||||
net-tools \
|
fakeroot \
|
||||||
qemu-utils \
|
apt-utils \
|
||||||
ca-certificates \
|
net-tools \
|
||||||
netcat-openbsd \
|
e2fsprogs \
|
||||||
qemu-system-x86 \
|
diffutils \
|
||||||
"$extra" && \
|
qemu-utils \
|
||||||
apt-get clean && \
|
iputils-ping \
|
||||||
unlink /etc/nginx/sites-enabled/default && \
|
inotify-tools \
|
||||||
sed -i 's/^worker_processes.*/worker_processes 1;/' /etc/nginx/nginx.conf && \
|
ca-certificates \
|
||||||
echo "$VERSION_ARG" > /run/version && \
|
netcat-openbsd \
|
||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
qemu-system-x86 \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
python3-msgpack \
|
||||||
|
python3-pysodium
|
||||||
|
|
||||||
|
# Install Passt package
|
||||||
|
wget "https://github.com/qemus/passt/releases/download/v${VERSION_PASST}/passt_${VERSION_PASST}_${TARGETARCH}.deb" -O /tmp/passt.deb -q --timeout=10
|
||||||
|
dpkg -i /tmp/passt.deb
|
||||||
|
|
||||||
|
# Install Websocketd package
|
||||||
|
wget "https://github.com/qemus/websocketd/releases/download/v${VERSION_WSD}/websocketd-${VERSION_WSD}_${TARGETARCH}.deb" -O /tmp/wsd.deb -q --timeout=10
|
||||||
|
dpkg -i /tmp/wsd.deb
|
||||||
|
|
||||||
|
apt-get clean
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
pip3 install --no-cache-dir --break-system-packages --root-user-action=ignore "dissect.cstruct==$VERSION_CSTRUCT"
|
||||||
|
|
||||||
|
# Set version file
|
||||||
|
echo "$VERSION_ARG" > /etc/version
|
||||||
|
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
EOF
|
||||||
|
|
||||||
COPY --chmod=755 ./src /run/
|
COPY --chmod=755 ./src /run/
|
||||||
COPY --chmod=755 ./web /var/www/
|
COPY --chmod=755 ./web /var/www/
|
||||||
COPY --chmod=755 --from=builder /qemu-host.bin /run/host.bin
|
COPY --chmod=755 --from=builder /qemu-host.bin /run/host.bin
|
||||||
COPY --chmod=744 ./web/nginx.conf /etc/nginx/sites-enabled/web.conf
|
COPY --chmod=744 ./web/conf/nginx.conf /etc/nginx/default.conf
|
||||||
|
ADD --chmod=775 https://raw.githubusercontent.com/sud0woodo/patology/refs/heads/main/patology.py /run/extract.py
|
||||||
|
|
||||||
VOLUME /storage
|
VOLUME /storage
|
||||||
EXPOSE 22 139 445 5000
|
EXPOSE 22 139 445 5000
|
||||||
|
|
||||||
ENV RAM_SIZE="1G"
|
ENV RAM_SIZE="2G"
|
||||||
ENV DISK_SIZE="16G"
|
ENV CPU_CORES="2"
|
||||||
ENV CPU_CORES="1"
|
ENV DISK_SIZE="256G"
|
||||||
|
|
||||||
HEALTHCHECK --interval=60s --start-period=45s --retries=2 CMD /run/check.sh
|
HEALTHCHECK --interval=60s --start-period=45s --retries=2 CMD ["/run/check.sh"]
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/tini", "-s", "/run/entry.sh"]
|
ENTRYPOINT ["/usr/bin/tini", "-s", "/run/entry.sh"]
|
||||||
|
|||||||
+16
-15
@@ -1,16 +1,17 @@
|
|||||||
services:
|
services:
|
||||||
dsm:
|
dsm:
|
||||||
container_name: dsm
|
container_name: dsm
|
||||||
image: vdsm/virtual-dsm:latest
|
image: vdsm/virtual-dsm
|
||||||
environment:
|
environment:
|
||||||
DISK_SIZE: "16G"
|
DISK_SIZE: "256G"
|
||||||
devices:
|
devices:
|
||||||
- /dev/kvm
|
- /dev/kvm
|
||||||
- /dev/net/tun
|
- /dev/net/tun
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
ports:
|
ports:
|
||||||
- 5000:5000
|
- 5000:5000
|
||||||
volumes:
|
volumes:
|
||||||
- /var/dsm:/storage
|
- ./dsm:/storage
|
||||||
stop_grace_period: 2m
|
restart: always
|
||||||
|
stop_grace_period: 2m
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# Environment Variables
|
||||||
|
|
||||||
|
This page lists all the environment variables that can be used to configure the container.
|
||||||
|
|
||||||
|
An empty default means the variable is unset and its value is determined automatically when applicable.
|
||||||
|
|
||||||
|
## 💽 Virtual DSM
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `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_MODEL` | | Synology host model reported to DSM. |
|
||||||
|
| `HOST_SERIAL` | | Synology host serial number reported to DSM. |
|
||||||
|
| `GUEST_SERIAL` | | Synology guest serial number reported to DSM. |
|
||||||
|
|
||||||
|
## 🧠 CPU and Memory
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `CPU_CORES` | `2` | Number of virtual CPU cores, such as `4`, `half`, or `max`. |
|
||||||
|
| `CPU_MODEL` | `host` | QEMU CPU model. |
|
||||||
|
| `CPU_FLAGS` | | Additional QEMU CPU flags. |
|
||||||
|
| `HOST_CPU` | | CPU name reported to DSM. Selected automatically when unset. |
|
||||||
|
| `KVM` | `Y` | Enables KVM hardware acceleration. |
|
||||||
|
| `RAM_SIZE` | `2G` | Amount of RAM assigned to DSM, such as `2G`, `4G`, `half`, or `max`. |
|
||||||
|
| `RAM_CHECK` | `Y` | Checks whether enough host memory is available before starting DSM. |
|
||||||
|
|
||||||
|
## 💾 Storage
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `DISK_SIZE` | `256G` | Size of the main data disk. |
|
||||||
|
| `DISK_FMT` | `raw` | Disk image format: `raw` or `qcow2`. |
|
||||||
|
| `DISK_TYPE` | `scsi` | Disk device type, such as `sata`, `scsi`, `nvme`, or `blk`. |
|
||||||
|
| `DISK_CACHE` | `none` | Disk cache mode, such as `none` or `writeback`. |
|
||||||
|
| `DISK_IO` | `native` | Disk I/O mode, such as `native`, `threads`, or `io_uring`. |
|
||||||
|
| `DISK_DISCARD` | `unmap` | Discard/TRIM mode for the primary disk. |
|
||||||
|
| `DISK_ROTATION` | `1` | Rotation rate reported to the guest. Use `1` to identify the disk as an SSD. |
|
||||||
|
| `DISK_FLAGS` | | Additional options used when creating `qcow2` disks. |
|
||||||
|
| `DISK_OPTIONS` | | Additional options appended to QEMU disk devices. |
|
||||||
|
| `ALLOCATE` | `N` | Preallocates space for the data disks. |
|
||||||
|
| `STORAGE` | `/storage` | Storage directory used for disks, settings, and downloads. |
|
||||||
|
|
||||||
|
## 🌐 Networking
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `NETWORK` | | Network mode, such as `nat`, `user`, or `N` to disable networking. |
|
||||||
|
| `DHCP` | `N` | Enables macvtap networking so DSM receives a DHCP address. |
|
||||||
|
| `HOST` | | Hostname assigned to the machine on the network. |
|
||||||
|
| `IP` | | Overrides the automatically selected guest IPv4 address. |
|
||||||
|
| `MAC` | | Guest network adapter MAC address. |
|
||||||
|
| `ADAPTER` | `virtio-net-pci` | QEMU network adapter model. |
|
||||||
|
| `DEV` | `eth0` | Container network interface used as the uplink. |
|
||||||
|
| `MTU` | | MTU assigned to the guest network interface. |
|
||||||
|
| `MASK` | `255.255.255.0` | IPv4 netmask for guest network. |
|
||||||
|
| `TAP` | `dsm` | TAP or macvtap interface name. |
|
||||||
|
| `BRIDGE` | `docker` | Bridge name used for NAT networking. |
|
||||||
|
| `HOST_PORTS` | | Ports excluded from guest forwarding. |
|
||||||
|
| `USER_PORTS` | | Additional ports to forward to DSM when using user-mode networking. |
|
||||||
|
| `DNSMASQ_OPTS` | | Additional options passed to dnsmasq. |
|
||||||
|
| `DNSMASQ_DEBUG` | `N` | Enables dnsmasq debug output. |
|
||||||
|
| `DNSMASQ_DISABLE` | `N` | Disables the internal dnsmasq resolver. |
|
||||||
|
| `PASST_OPTS` | | Additional options passed to passt. |
|
||||||
|
| `PASST_DEBUG` | `N` | Enables passt debug output. |
|
||||||
|
|
||||||
|
## 🖥️ Display
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `DISPLAY` | `none` | Display backend, such as `vnc`, `disabled`, or `none`. |
|
||||||
|
| `LOSSY` | `N` | Enables lossy VNC compression to reduce bandwidth usage. |
|
||||||
|
| `VGA` | `none` | QEMU video adapter model. |
|
||||||
|
| `GPU` | `N` | Enables Intel iGPU acceleration. |
|
||||||
|
| `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
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `SHUTDOWN` | `Y` | Enables graceful shutdown. |
|
||||||
|
| `TIMEOUT` | `115` | Maximum time, in seconds, to wait before forcing DSM to stop. |
|
||||||
|
| `API_TIMEOUT` | `90` | Maximum time, in seconds, to wait for the shutdown API call. |
|
||||||
|
|
||||||
|
## 🐞 Debugging
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `DEBUG` | `N` | Enables verbose debug output. |
|
||||||
|
| `TRACE` | `N` | Enables shell command tracing. |
|
||||||
|
| `HOST_DEBUG` | `N` | Enables debug output for the DSM host helper. |
|
||||||
+55
-43
@@ -8,60 +8,72 @@ spec:
|
|||||||
- ReadWriteOnce
|
- ReadWriteOnce
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: 16Gi
|
storage: 256Gi
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: apps/v1
|
||||||
kind: Pod
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
name: dsm
|
name: dsm
|
||||||
labels:
|
labels:
|
||||||
name: dsm
|
name: dsm
|
||||||
spec:
|
spec:
|
||||||
containers:
|
replicas: 1
|
||||||
- name: dsm
|
strategy:
|
||||||
image: vdsm/virtual-dsm
|
type: Recreate
|
||||||
env:
|
selector:
|
||||||
- name: RAM_SIZE
|
matchLabels:
|
||||||
value: "1G"
|
app: dsm
|
||||||
- name: CPU_CORES
|
template:
|
||||||
value: "1"
|
metadata:
|
||||||
- name: DISK_SIZE
|
labels:
|
||||||
value: "16G"
|
app: dsm
|
||||||
ports:
|
spec:
|
||||||
- containerPort: 5000
|
containers:
|
||||||
securityContext:
|
- name: dsm
|
||||||
capabilities:
|
image: vdsm/virtual-dsm
|
||||||
add:
|
env:
|
||||||
- NET_ADMIN
|
- name: DISK_SIZE
|
||||||
privileged: true
|
value: "256G"
|
||||||
volumeMounts:
|
ports:
|
||||||
- mountPath: /storage
|
- containerPort: 5000
|
||||||
name: storage
|
name: http
|
||||||
- mountPath: /dev/kvm
|
protocol: TCP
|
||||||
name: dev-kvm
|
securityContext:
|
||||||
- mountPath: /dev/net/tun
|
capabilities:
|
||||||
name: dev-tun
|
add:
|
||||||
terminationGracePeriodSeconds: 120
|
- NET_ADMIN
|
||||||
volumes:
|
privileged: true
|
||||||
- name: storage
|
volumeMounts:
|
||||||
persistentVolumeClaim:
|
- mountPath: /storage
|
||||||
claimName: dsm-pvc
|
name: storage
|
||||||
- hostPath:
|
- mountPath: /dev/kvm
|
||||||
path: /dev/kvm
|
name: dev-kvm
|
||||||
name: dev-kvm
|
- mountPath: /dev/net/tun
|
||||||
- hostPath:
|
name: dev-tun
|
||||||
path: /dev/net/tun
|
terminationGracePeriodSeconds: 120
|
||||||
type: CharDevice
|
volumes:
|
||||||
name: dev-tun
|
- name: storage
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: dsm-pvc
|
||||||
|
- hostPath:
|
||||||
|
path: /dev/kvm
|
||||||
|
name: dev-kvm
|
||||||
|
- hostPath:
|
||||||
|
path: /dev/net/tun
|
||||||
|
type: CharDevice
|
||||||
|
name: dev-tun
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: dsm
|
name: dsm
|
||||||
spec:
|
spec:
|
||||||
|
internalTrafficPolicy: Cluster
|
||||||
ports:
|
ports:
|
||||||
- name: tcp-5000
|
- name: http
|
||||||
port: 5000
|
port: 5000
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: 5000
|
||||||
selector:
|
selector:
|
||||||
name: dsm
|
app: dsm
|
||||||
type: NodePort
|
type: ClusterIP
|
||||||
|
|||||||
@@ -16,13 +16,17 @@ Virtual DSM in a Docker container.
|
|||||||
|
|
||||||
## Features ✨
|
## Features ✨
|
||||||
|
|
||||||
- Multiple disks
|
- Runs Virtual DSM inside a Docker container
|
||||||
- KVM acceleration
|
- Automatic download of the installation files
|
||||||
- Upgrades supported
|
- Web-based access to the DSM desktop
|
||||||
|
- Near-native performance with KVM acceleration
|
||||||
|
- Customizable CPU, memory, and storage allocation
|
||||||
|
- Supports multiple disks and physical disk passthrough
|
||||||
|
- Supports NAT, user-mode, macvlan, and macvtap networking
|
||||||
|
|
||||||
## Usage 🐳
|
## Usage 🐳
|
||||||
|
|
||||||
Via Docker Compose:
|
##### Docker Compose:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
@@ -30,7 +34,7 @@ services:
|
|||||||
container_name: dsm
|
container_name: dsm
|
||||||
image: vdsm/virtual-dsm
|
image: vdsm/virtual-dsm
|
||||||
environment:
|
environment:
|
||||||
DISK_SIZE: "16G"
|
DISK_SIZE: "256G"
|
||||||
devices:
|
devices:
|
||||||
- /dev/kvm
|
- /dev/kvm
|
||||||
- /dev/net/tun
|
- /dev/net/tun
|
||||||
@@ -39,31 +43,36 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5000:5000
|
- 5000:5000
|
||||||
volumes:
|
volumes:
|
||||||
- /var/dsm:/storage
|
- ./dsm:/storage
|
||||||
|
restart: always
|
||||||
stop_grace_period: 2m
|
stop_grace_period: 2m
|
||||||
```
|
```
|
||||||
|
|
||||||
Via Docker CLI:
|
##### Docker CLI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -it --rm -p 5000:5000 --device=/dev/kvm --device=/dev/net/tun --cap-add NET_ADMIN --stop-timeout 120 vdsm/virtual-dsm
|
docker run -it --rm --name dsm -e "DISK_SIZE=256G" -p 5000:5000 --device=/dev/kvm --device=/dev/net/tun --cap-add NET_ADMIN -v "${PWD:-.}/dsm:/storage" --stop-timeout 120 docker.io/vdsm/virtual-dsm
|
||||||
```
|
```
|
||||||
|
|
||||||
Via Kubernetes:
|
##### Kubernetes:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/master/kubernetes.yml
|
kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/master/kubernetes.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Compatibility ⚙️
|
##### GitHub Codespaces:
|
||||||
|
|
||||||
| **Product** | **Platform** | |
|
[](https://codespaces.new/vdsm/virtual-dsm)
|
||||||
|---|---|---|
|
|
||||||
| Docker Engine | Linux| ✅ |
|
## Requirements ⚙️
|
||||||
| Docker Desktop | Linux | ❌ |
|
|
||||||
| Docker Desktop | macOS | ❌ |
|
- Docker or Podman on a Linux host with KVM support.
|
||||||
| Docker Desktop | Windows 11 | ✅ |
|
- Docker Desktop or Podman (Desktop) on Windows 11 with nested virtualization enabled.
|
||||||
| Docker Desktop | Windows 10 | ❌ |
|
- At least 1 GB of available RAM.
|
||||||
|
- At least 16 GB of free disk space.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Docker Desktop on Linux, macOS, and Windows 10 does not currently provide KVM access to containers and is therefore not supported.
|
||||||
|
|
||||||
## FAQ 💬
|
## FAQ 💬
|
||||||
|
|
||||||
@@ -71,9 +80,11 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
|
|||||||
|
|
||||||
Very simple! These are the steps:
|
Very simple! These are the steps:
|
||||||
|
|
||||||
- Start the container and connect to [port 5000](http://localhost:5000) using your web browser.
|
- Start the container and connect to [port 5000](http://127.0.0.1:5000/) using your web browser.
|
||||||
|
|
||||||
- Wait until DSM is ready, choose an username and password, and you will be taken to the desktop.
|
- Wait until DSM finishes its installation
|
||||||
|
|
||||||
|
- Choose a username and password, and you will be taken to the desktop.
|
||||||
|
|
||||||
Enjoy your brand new NAS, and don't forget to star this repo!
|
Enjoy your brand new NAS, and don't forget to star this repo!
|
||||||
|
|
||||||
@@ -83,36 +94,22 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
volumes:
|
volumes:
|
||||||
- /var/dsm:/storage
|
- ./dsm:/storage
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace the example path `/var/dsm` with the desired storage folder.
|
Replace the example path `./dsm` with the desired storage folder or named volume.
|
||||||
|
|
||||||
### How do I change the size of the disk?
|
### How do I change the size of the disk?
|
||||||
|
|
||||||
To expand the default size of 16 GB, locate the `DISK_SIZE` setting in your compose file and modify it to your preferred capacity:
|
To expand the default size of 256 GB, locate the `DISK_SIZE` setting in your compose file and modify it to your preferred capacity:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
DISK_SIZE: "128G"
|
DISK_SIZE: "512G"
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> This can also be used to resize the existing disk to a larger capacity without any data loss.
|
> This can also be used to resize an existing disk to a larger capacity without any data loss.
|
||||||
|
|
||||||
### How do I create a growable disk?
|
|
||||||
|
|
||||||
By default, the entire capacity of the disk is reserved in advance.
|
|
||||||
|
|
||||||
To create a growable disk that only allocates space that is actually used, add the following environment variable:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
environment:
|
|
||||||
DISK_FMT: "qcow2"
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> This may reduce the write performance of the disk.
|
|
||||||
|
|
||||||
### How do I add multiple disks?
|
### How do I add multiple disks?
|
||||||
|
|
||||||
@@ -120,33 +117,28 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
DISK2_SIZE: "32G"
|
DISK2_SIZE: "500G"
|
||||||
DISK3_SIZE: "64G"
|
DISK3_SIZE: "750G"
|
||||||
volumes:
|
volumes:
|
||||||
- /home/example:/storage2
|
- ./example2:/storage2
|
||||||
- /mnt/data/example:/storage3
|
- ./example3:/storage3
|
||||||
```
|
```
|
||||||
|
|
||||||
### How do I pass-through a disk?
|
### How do I pass through a disk?
|
||||||
|
|
||||||
It is possible to pass-through a disk device directly, by adding it to your compose file in this way:
|
You can pass through disk devices or partitions directly by adding them to your compose file in this way:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
devices:
|
devices:
|
||||||
- /dev/disk/by-uuid/12345-12345-12345-12345-12345:/disk2
|
- /dev/sdb:/disk1
|
||||||
|
- /dev/sdc1:/disk2
|
||||||
```
|
```
|
||||||
|
|
||||||
Make sure to bind the disk via its UUID (obtainable via `lsblk -o name,uuid`) instead of its name (`/dev/sdc`), to prevent ever binding the wrong disk when the drive letters happen to change.
|
Make sure it is totally empty (without any filesystem), otherwise DSM may not format it as a volume.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> The device needs to be totally empty (without any partition table) otherwise DSM does not always format it into a volume.
|
|
||||||
|
|
||||||
> [!CAUTION]
|
|
||||||
> Do NOT use this feature with the goal of sharing files from the host, they will all be lost without warning when DSM creates the volume.
|
|
||||||
|
|
||||||
### How do I change the amount of CPU or RAM?
|
### How do I change the amount of CPU or RAM?
|
||||||
|
|
||||||
By default, the container will be allowed to use a maximum of 1 CPU core and 1 GB of RAM.
|
By default, Virtual DSM will be allowed to use 2 CPU cores and 2 GB of RAM.
|
||||||
|
|
||||||
If you want to adjust this, you can specify the desired amount using the following environment variables:
|
If you want to adjust this, you can specify the desired amount using the following environment variables:
|
||||||
|
|
||||||
@@ -156,31 +148,6 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
|
|||||||
CPU_CORES: "4"
|
CPU_CORES: "4"
|
||||||
```
|
```
|
||||||
|
|
||||||
### How do I verify if my system supports KVM?
|
|
||||||
|
|
||||||
Only Linux and Windows 11 support KVM virtualization, macOS and Windows 10 do not unfortunately.
|
|
||||||
|
|
||||||
You can run the following commands in Linux to check your system:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install cpu-checker
|
|
||||||
sudo kvm-ok
|
|
||||||
```
|
|
||||||
|
|
||||||
If you receive an error from `kvm-ok` indicating that KVM cannot be used, please check whether:
|
|
||||||
|
|
||||||
- the virtualization extensions (`Intel VT-x` or `AMD SVM`) are enabled in your BIOS.
|
|
||||||
|
|
||||||
- you enabled "nested virtualization" if you are running the container inside a virtual machine.
|
|
||||||
|
|
||||||
- you are not using a cloud provider, as most of them do not allow nested virtualization for their VPS's.
|
|
||||||
|
|
||||||
If you do not receive any error from `kvm-ok` but the container still complains about KVM, please check whether:
|
|
||||||
|
|
||||||
- you are not using "Docker Desktop for Linux" as it does not support KVM, instead make use of Docker Engine directly.
|
|
||||||
|
|
||||||
- it could help to add `privileged: true` to your compose file (or `sudo` to your `docker run` command), to rule out any permission issue.
|
|
||||||
|
|
||||||
### How do I assign an individual IP address to the container?
|
### How do I assign an individual IP address to the container?
|
||||||
|
|
||||||
By default, the container uses bridge networking, which shares the IP address with the host.
|
By default, the container uses bridge networking, which shares the IP address with the host.
|
||||||
@@ -222,7 +189,7 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
|
|||||||
|
|
||||||
After configuring the container for [macvlan](#how-do-i-assign-an-individual-ip-address-to-the-container), it is possible for DSM to become part of your home network by requesting an IP from your router, just like your other devices.
|
After configuring the container for [macvlan](#how-do-i-assign-an-individual-ip-address-to-the-container), it is possible for DSM to become part of your home network by requesting an IP from your router, just like your other devices.
|
||||||
|
|
||||||
To enable this mode, add the following lines to your compose file:
|
To enable this mode, in which the container and DSM will have separate IP addresses, add the following lines to your compose file:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
@@ -233,12 +200,9 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
|
|||||||
- 'c *:* rwm'
|
- 'c *:* rwm'
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
### How do I pass through the GPU?
|
||||||
> In this mode, the container and DSM will each have their own separate IPs.
|
|
||||||
|
|
||||||
### How do I pass-through the GPU?
|
To pass through your Intel GPU, add the following lines to your compose file:
|
||||||
|
|
||||||
To pass-through your Intel GPU, add the following lines to your compose file:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
@@ -247,41 +211,76 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
|
|||||||
- /dev/dri
|
- /dev/dri
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!TIP]
|
> [!NOTE]
|
||||||
> This can be used to enable the facial recognition function in Synology Photos for example.
|
> This can be used to enable the facial recognition function in Synology Photos, but does not provide hardware transcoding for video.
|
||||||
|
|
||||||
### How do I install a specific version of vDSM?
|
### How do I install a specific version of vDSM?
|
||||||
|
|
||||||
By default, version 7.2 will be installed, but if you prefer an older version, you can add its download URL to your compose file as follows:
|
By default, version 7.2 will be installed, but if you prefer an older version, you can add the download URL of the `.pat` file to your compose file as follows:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
URL: "https://global.synologydownload.com/download/DSM/release/7.0.1/42218/DSM_VirtualDSM_42218.pat"
|
URL: "https://global.synologydownload.com/download/DSM/release/7.0.1/42218/DSM_VirtualDSM_42218.pat"
|
||||||
```
|
```
|
||||||
|
|
||||||
With this method, it is even possible to switch between different versions while keeping all your file data intact.
|
With this method, it is even possible to switch back and forth between versions while keeping your file data intact.
|
||||||
|
|
||||||
If you don't have internet access, it's also possible to skip the download by setting URL to:
|
|
||||||
|
|
||||||
|
Alternatively, you can also skip the download and use a local file instead, by binding it in your compose file in this way:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
volumes:
|
||||||
URL: "DSM_VirtualDSM_42218.pat"
|
- ./DSM_VirtualDSM_42218.pat:/boot.pat
|
||||||
```
|
```
|
||||||
|
|
||||||
after placing a file called `DSM_VirtualDSM_42218.pat` in your `/storage` folder.
|
Replace the example path `./DSM_VirtualDSM_42218.pat` with the filename of your desired `.pat` file. The value of `URL` will be ignored in this case.
|
||||||
|
|
||||||
|
### Are these all available options?
|
||||||
|
|
||||||
|
No. For a complete overview of all supported settings, see the [environment variables](docs/environment.md) page.
|
||||||
|
|
||||||
|
### How do I verify that KVM is available?
|
||||||
|
|
||||||
|
First, make sure your platform and container runtime meet the [requirements](#requirements-️) listed above.
|
||||||
|
|
||||||
|
On a Linux host, install `cpu-checker` and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install cpu-checker
|
||||||
|
sudo kvm-ok
|
||||||
|
```
|
||||||
|
|
||||||
|
A working configuration should report:
|
||||||
|
|
||||||
|
```text
|
||||||
|
KVM acceleration can be used
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also verify that the KVM device exists:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -l /dev/kvm
|
||||||
|
```
|
||||||
|
|
||||||
|
If KVM is unavailable, check whether:
|
||||||
|
|
||||||
|
- Hardware virtualization (`Intel VT-x` or `AMD-V`) is enabled in your BIOS or UEFI.
|
||||||
|
- Nested virtualization is enabled when the host itself is a virtual machine.
|
||||||
|
- Your VPS or cloud provider supports nested virtualization.
|
||||||
|
|
||||||
|
If `kvm-ok` succeeds but the container still reports that KVM is unavailable, you can temporarily add `privileged: true` to your Compose file to rule out a permission or device-access issue.
|
||||||
|
|
||||||
### What are the differences compared to the standard DSM?
|
### What are the differences compared to the standard DSM?
|
||||||
|
|
||||||
There are only two minor differences: the Virtual Machine Manager package is not available, and Surveillance Station will not include any free licenses.
|
There are only two minor differences: the Virtual Machine Manager package is not available, and Surveillance Station will not include any free licenses.
|
||||||
|
|
||||||
### Is this project legal?
|
### Is this project legal?
|
||||||
|
|
||||||
Yes, this project contains only open-source code and does not distribute any copyrighted material. Neither does it try to circumvent any copyright protection measures. So under all applicable laws, this project will be considered legal.
|
Yes, this project contains only open-source code and does not distribute any material owned by Synology. Neither does it try to circumvent any copyright protection measures.
|
||||||
|
|
||||||
However, by installing Synology's Virtual DSM, you must accept their end-user license agreement, which does not permit installation on non-Synology hardware. So only run this container on an official Synology NAS, as any other use will be a violation of their terms and conditions.
|
However, by installing Synology's Virtual DSM, you must accept their end-user license agreement, which does not permit installation on non-Synology hardware. So only run this container on an official Synology NAS, as any other use will be a violation of their terms and conditions.
|
||||||
|
|
||||||
## Stars 🌟
|
## Stars 🌟
|
||||||
[](https://starchart.cc/vdsm/virtual-dsm)
|
[](https://github.com/vdsm/virtual-dsm/stargazers)
|
||||||
|
|
||||||
## Disclaimer ⚖️
|
## Disclaimer ⚖️
|
||||||
|
|
||||||
|
|||||||
+18
-9
@@ -1,28 +1,37 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
cd /run
|
||||||
|
. utils.sh # Load functions
|
||||||
|
|
||||||
|
: "${DHCP:="N"}"
|
||||||
: "${NETWORK:="Y"}"
|
: "${NETWORK:="Y"}"
|
||||||
|
|
||||||
[ -f "/run/shm/qemu.end" ] && echo "QEMU is shutting down.." && exit 1
|
[ -f "/run/shm/qemu.end" ] && echo "QEMU is shutting down..." && exit 1
|
||||||
[ ! -s "/run/shm/qemu.pid" ] && echo "QEMU is not running yet.." && exit 0
|
[ ! -s "/run/shm/qemu.pid" ] && echo "QEMU is not running yet..." && exit 0
|
||||||
[[ "$NETWORK" == [Nn]* ]] && echo "Networking is disabled.." && exit 0
|
disabled "$NETWORK" && echo "Networking is disabled." && exit 0
|
||||||
|
|
||||||
file="/run/shm/dsm.url"
|
file="/run/shm/dsm.url"
|
||||||
address="/run/shm/qemu.ip"
|
address="/run/shm/qemu.ip"
|
||||||
|
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
|
||||||
|
|
||||||
if [[ "$location" == "20.20"* ]]; then
|
# In DHCP mode the firewall must allow the container address; with port
|
||||||
ip="20.20.20.1"
|
# forwarding it must allow the internal gateway used to reach the guest.
|
||||||
|
if enabled "$DHCP"; then
|
||||||
|
ip=$(<"$address")
|
||||||
|
echo "Failed to reach DSM at http://$location"
|
||||||
|
else
|
||||||
|
ip=$(<"$gateway")
|
||||||
port="${location##*:}"
|
port="${location##*:}"
|
||||||
echo "Failed to reach DSM at port $port"
|
echo "Failed to reach DSM at port $port"
|
||||||
else
|
|
||||||
echo "Failed to reach DSM at http://$location"
|
|
||||||
ip=$(<"$address")
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "You might need to whitelist IP $ip in the DSM firewall." && exit 1
|
echo "You might need to whitelist IP $ip in the DSM firewall." && exit 1
|
||||||
|
|||||||
+60
-23
@@ -2,36 +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=""
|
||||||
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 ' ')
|
|
||||||
|
|
||||||
# Check available memory as the very last step
|
# 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"
|
||||||
|
|
||||||
if [[ "$RAM_CHECK" != [Nn]* ]]; then
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
RAM_AVAIL=$(free -b | grep -m 1 Mem: | awk '{print $7}')
|
configureMemory() {
|
||||||
AVAIL_GB=$(( RAM_AVAIL/1073741824 ))
|
|
||||||
|
|
||||||
if (( (RAM_WANTED + RAM_SPARE) > RAM_AVAIL )); then
|
RAM_OPTS=$(echo "-m ${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
|
||||||
error "Your configured RAM_SIZE of $WANTED_GB GB is too high for the $AVAIL_GB GB of memory available, please set a lower value."
|
|
||||||
exit 17
|
|
||||||
fi
|
|
||||||
|
|
||||||
if (( (RAM_WANTED + (RAM_SPARE * 3)) > RAM_AVAIL )); then
|
return 0
|
||||||
warn "your configured RAM_SIZE of $WANTED_GB GB is very close to the $AVAIL_GB GB of memory available, please consider a lower value."
|
}
|
||||||
fi
|
|
||||||
|
|
||||||
fi
|
configureMonitor() {
|
||||||
|
|
||||||
if [[ "$DEBUG" == [Yy1]* ]]; then
|
MON_OPTS="-name $PROCESS,process=$PROCESS,debug-threads=on"
|
||||||
printf "Arguments:\n\n%s" "${ARGS// -/$'\n-'}" && echo
|
MON_OPTS+=" -pidfile $QEMU_PID"
|
||||||
fi
|
|
||||||
|
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
|
||||||
|
|||||||
+588
-325
File diff suppressed because it is too large
Load Diff
+33
-3
@@ -6,22 +6,46 @@ 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
|
||||||
|
|
||||||
if [[ "$GPU" != [Yy1]* ]] || [[ "$ARCH" != "amd64" ]]; then
|
# Sanitize variables
|
||||||
|
VGA=$(strip "$VGA")
|
||||||
|
LOSSY=$(strip "$LOSSY")
|
||||||
|
DISPLAY=$(strip "$DISPLAY")
|
||||||
|
RENDERNODE=$(strip "$RENDERNODE")
|
||||||
|
|
||||||
|
CPU_VENDOR=$(lscpu | awk '/Vendor ID/{print $3}')
|
||||||
|
|
||||||
|
# The accelerated Intel render-node path is restricted to x86 Intel hosts;
|
||||||
|
# other platforms retain the normal QEMU display backend.
|
||||||
|
if ! enabled "$GPU" || isAmdCpu || [[ "$ARCH" != "amd64" ]]; then
|
||||||
|
|
||||||
|
# A disabled frontend also removes the emulated VGA device to keep the guest
|
||||||
|
# hardware layout headless.
|
||||||
[[ "${DISPLAY,,}" == "none" ]] && VGA="none"
|
[[ "${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
|
||||||
|
|
||||||
DISPLAY_OPTS="-display egl-headless,rendernode=$RENDERNODE"
|
msg="Configuring display drivers..."
|
||||||
|
html "$msg"
|
||||||
|
enabled "$DEBUG" && echo "$msg"
|
||||||
|
|
||||||
|
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))"
|
||||||
|
|
||||||
@@ -37,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"
|
||||||
|
|
||||||
|
|||||||
+26
-13
@@ -1,12 +1,17 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
APP="Virtual DSM"
|
: "${PLATFORM:="x64"}"
|
||||||
SUPPORT="https://github.com/vdsm/virtual-dsm"
|
: "${APP:="Virtual DSM"}"
|
||||||
|
: "${SUPPORT:="https://github.com/vdsm/virtual-dsm"}"
|
||||||
|
|
||||||
cd /run
|
cd /run
|
||||||
|
|
||||||
. reset.sh # Initialize system
|
. start.sh # Startup hook
|
||||||
|
. utils.sh # Load functions
|
||||||
|
. init.sh # Initialize system
|
||||||
|
. memory.sh # Check memory
|
||||||
|
. server.sh # Start webserver
|
||||||
. install.sh # Run installation
|
. install.sh # Run installation
|
||||||
. disk.sh # Initialize disks
|
. disk.sh # Initialize disks
|
||||||
. display.sh # Initialize graphics
|
. display.sh # Initialize graphics
|
||||||
@@ -15,22 +20,30 @@ cd /run
|
|||||||
. serial.sh # Initialize serialport
|
. serial.sh # Initialize serialport
|
||||||
. power.sh # Configure shutdown
|
. power.sh # Configure shutdown
|
||||||
. config.sh # Configure arguments
|
. config.sh # Configure arguments
|
||||||
|
. finish.sh # Finish initialization
|
||||||
|
|
||||||
trap - ERR
|
trap - ERR
|
||||||
|
|
||||||
version=$(qemu-system-x86_64 --version | head -n 1 | cut -d '(' -f 1 | awk '{ print $NF }')
|
cmd=(qemu-system-x86_64)
|
||||||
info "Booting $APP using QEMU v$version..."
|
version=$("${cmd[@]}" --version | awk 'NR==1 { print $4 }')
|
||||||
|
info "Booting $APP using QEMU v$version..." && echo
|
||||||
|
|
||||||
if [[ "$CONSOLE" == [Yy]* ]]; then
|
if ! enabled "$SHUTDOWN"; then
|
||||||
exec qemu-system-x86_64 ${ARGS:+ $ARGS}
|
exec "${cmd[@]}" ${ARGS:+ $ARGS}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
{ qemu-system-x86_64 ${ARGS:+ $ARGS} >"$QEMU_OUT" 2>"$QEMU_LOG"; rc=$?; } || :
|
if ! interactive; then
|
||||||
(( rc != 0 )) && error "$(<"$QEMU_LOG")" && exit 15
|
"${cmd[@]}" ${ARGS:+ $ARGS} &
|
||||||
|
else
|
||||||
|
startConsole
|
||||||
|
startQemu "${cmd[@]}" ${ARGS:+ $ARGS}
|
||||||
|
fi
|
||||||
|
|
||||||
terminal
|
pid=$!
|
||||||
tail -fn +0 "$QEMU_LOG" 2>/dev/null &
|
rc=0
|
||||||
cat "$QEMU_TERM" 2>/dev/null & wait $! || :
|
|
||||||
|
wait "$pid" || rc=$?
|
||||||
|
[ -f "$QEMU_END" ] && exit "$rc"
|
||||||
|
|
||||||
sleep 1 & wait $!
|
sleep 1 & wait $!
|
||||||
[ ! -f "$QEMU_END" ] && finish 0
|
finish "$rc"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
if enabled "$DEBUG"; then
|
||||||
|
printf "QEMU arguments:\n\n %s\n\n" "${ARGS// -/$'\n -'}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Must always remain the very last command
|
||||||
|
enableTrap
|
||||||
|
|
||||||
|
return 0
|
||||||
+296
@@ -0,0 +1,296 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
trap 'error "Status $? while: $BASH_COMMAND (line $LINENO/$BASH_LINENO)"' ERR
|
||||||
|
enabled "${TRACE:-}" && set -o functrace && trap 'echo "# $BASH_COMMAND" >&2' DEBUG
|
||||||
|
|
||||||
|
[ ! -f "/run/entry.sh" ] && error "Script must be run inside the container!" && exit 11
|
||||||
|
[ "$(id -u)" -ne "0" ] && error "Script must be executed with root privileges." && exit 12
|
||||||
|
|
||||||
|
# Docker environment variables
|
||||||
|
|
||||||
|
: "${TZ:=""}" # System timezone
|
||||||
|
: "${KVM:="Y"}" # KVM acceleration
|
||||||
|
: "${DEBUG:="N"}" # Disable debugging mode
|
||||||
|
: "${COUNTRY:=""}" # Country code for mirror
|
||||||
|
: "${MACHINE:="q35"}" # Machine type selection
|
||||||
|
: "${ALLOCATE:=""}" # Preallocate diskspace
|
||||||
|
: "${ARGUMENTS:=""}" # Extra QEMU parameters
|
||||||
|
: "${CPU_CORES:="2"}" # Amount of CPU cores
|
||||||
|
: "${RAM_SIZE:="2G"}" # Maximum RAM amount
|
||||||
|
: "${RAM_CHECK:="Y"}" # Check available RAM
|
||||||
|
: "${DISK_SIZE:="16G"}" # Initial data disk size
|
||||||
|
: "${STORAGE:="/storage"}" # Storage folder location
|
||||||
|
|
||||||
|
detectEngine() {
|
||||||
|
|
||||||
|
if [ -f "/run/.containerenv" ]; then
|
||||||
|
ENGINE="${container:-}"
|
||||||
|
|
||||||
|
if [[ "${ENGINE,,}" == *"podman"* ]]; then
|
||||||
|
ENGINE="Podman"
|
||||||
|
else
|
||||||
|
[ -z "$ENGINE" ] && ENGINE="Kubernetes"
|
||||||
|
fi
|
||||||
|
elif [ -f "/.dockerenv" ]; then
|
||||||
|
ENGINE="Docker"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
detectRootless() {
|
||||||
|
|
||||||
|
local uid_map
|
||||||
|
|
||||||
|
# A full identity UID map indicates a rootful container; any remapping is
|
||||||
|
# treated as rootless even though the process itself runs as UID 0.
|
||||||
|
uid_map=$(awk '{$1=$1; print}' /proc/self/uid_map 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ "$uid_map" == "0 0 4294967295" ]]; then
|
||||||
|
ROOTLESS="N"
|
||||||
|
else
|
||||||
|
ROOTLESS="Y"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPrivileged() {
|
||||||
|
|
||||||
|
local cap_bnd
|
||||||
|
local last_cap
|
||||||
|
# Get the capability bounding set
|
||||||
|
cap_bnd=$(grep '^CapBnd:' /proc/$$/status | awk '{print $2}')
|
||||||
|
cap_bnd=$(printf "%d" "0x${cap_bnd}")
|
||||||
|
|
||||||
|
# Get the last capability number
|
||||||
|
last_cap=$(cat /proc/sys/kernel/cap_last_cap)
|
||||||
|
|
||||||
|
# Calculate the maximum capability value
|
||||||
|
# Compare the bounding set with every capability supported by this kernel;
|
||||||
|
# checking only a few known capabilities would misclassify newer kernels.
|
||||||
|
local max_cap=$(((1 << (last_cap + 1)) - 1))
|
||||||
|
|
||||||
|
if [ "$cap_bnd" -eq "$max_cap" ]; then
|
||||||
|
PRIVILEGED="Y"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCores() {
|
||||||
|
|
||||||
|
CPU_CORES=$(strip "$CPU_CORES")
|
||||||
|
[ -z "$CPU_CORES" ] && CPU_CORES=2
|
||||||
|
[[ "${CPU_CORES,,}" == "max" ]] && CPU_CORES="$CORES"
|
||||||
|
[[ "${CPU_CORES,,}" == "half" ]] && CPU_CORES=$(( CORES / 2 ))
|
||||||
|
[ -z "${CPU_CORES##*[!0-9]*}" ] && error "Invalid amount of CPU_CORES: $CPU_CORES" && exit 15
|
||||||
|
[ "$CPU_CORES" -lt "1" ] && CPU_CORES=1
|
||||||
|
|
||||||
|
if [ "$CPU_CORES" -gt "$CORES" ]; then
|
||||||
|
warn "The amount for CPU_CORES (${CPU_CORES}) exceeds the amount of logical cores available (${CORES}) and will be limited."
|
||||||
|
CPU_CORES="$CORES"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
checkSockets() {
|
||||||
|
|
||||||
|
local lscpu_out
|
||||||
|
lscpu_out=$(lscpu 2>/dev/null || true)
|
||||||
|
|
||||||
|
if grep -qi "socket(s)" <<< "$lscpu_out"; then
|
||||||
|
SOCKETS=$(grep -m 1 -i 'socket(s)' <<< "$lscpu_out" | awk '{print $2}')
|
||||||
|
[ -z "${SOCKETS##*[!0-9]*}" ] && SOCKETS=1
|
||||||
|
[ "$SOCKETS" -lt "1" ] && SOCKETS=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
checkStorage() {
|
||||||
|
|
||||||
|
# Check system
|
||||||
|
|
||||||
|
QEMU_DIR="/run/shm"
|
||||||
|
|
||||||
|
if [ ! -d "/dev/shm" ]; then
|
||||||
|
error "Directory /dev/shm not found!" && exit 14
|
||||||
|
else
|
||||||
|
# Keep runtime sockets and PID files on shared memory even on images where
|
||||||
|
# /run/shm is absent but /dev/shm is available.
|
||||||
|
[ ! -d "$QEMU_DIR" ] && ln -s /dev/shm "$QEMU_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
QEMU_PID="$QEMU_DIR/qemu.pid"
|
||||||
|
|
||||||
|
# Check folder
|
||||||
|
|
||||||
|
if [[ "${STORAGE,,}" != "/storage" ]]; then
|
||||||
|
if ! mkdir -p -- "$STORAGE"; then
|
||||||
|
error "Cannot create storage folder ($STORAGE)!" && exit 13
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$STORAGE" ]; then
|
||||||
|
error "Storage folder ($STORAGE) not found!" && exit 13
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -w "$STORAGE" ]; then
|
||||||
|
msg="Storage folder ($STORAGE) is not writeable!"
|
||||||
|
msg+=" If SELinux is active, you need to add the \":Z\" flag to the bind mount."
|
||||||
|
error "$msg" && exit 13
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
checkHost() {
|
||||||
|
|
||||||
|
# Check filesystem
|
||||||
|
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then
|
||||||
|
DISK_IO="threads"
|
||||||
|
DISK_CACHE="writeback"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
checkKvm() {
|
||||||
|
|
||||||
|
# Check KVM support
|
||||||
|
|
||||||
|
if [[ "${PLATFORM,,}" == "x64" ]]; then
|
||||||
|
TARGET="amd64"
|
||||||
|
else
|
||||||
|
TARGET="arm64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if disabled "$KVM"; then
|
||||||
|
warn "KVM acceleration is disabled, this will cause the machine to run about 10 times slower!"
|
||||||
|
else
|
||||||
|
# KVM accelerates only matching host and guest instruction sets; cross-
|
||||||
|
# architecture execution must fall back to software emulation.
|
||||||
|
if [[ "${ARCH,,}" != "$TARGET" ]]; then
|
||||||
|
KVM="N"
|
||||||
|
warn "your CPU architecture is ${ARCH^^} and cannot provide KVM acceleration for ${PLATFORM^^} instructions, so the machine will run about 10 times slower."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! disabled "$KVM"; then
|
||||||
|
|
||||||
|
KVM_ERR=""
|
||||||
|
|
||||||
|
if [ ! -e /dev/kvm ]; then
|
||||||
|
KVM_ERR="(/dev/kvm is missing)"
|
||||||
|
else
|
||||||
|
if ! sh -c 'echo -n > /dev/kvm' &> /dev/null; then
|
||||||
|
KVM_ERR="(/dev/kvm is unwriteable)"
|
||||||
|
else
|
||||||
|
if [[ "${PLATFORM,,}" == "x64" ]]; then
|
||||||
|
flags=$(sed -ne '/^flags/s/^.*: //p' /proc/cpuinfo)
|
||||||
|
if ! grep -qw "vmx\|svm" <<< "$flags"; then
|
||||||
|
KVM_ERR="(not enabled in BIOS)"
|
||||||
|
fi
|
||||||
|
if ! grep -qw "sse4_2" <<< "$flags"; then
|
||||||
|
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
|
||||||
|
enabled "$DEBUG" || exit 88
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$KVM_ERR" ]; then
|
||||||
|
KVM="N"
|
||||||
|
if [[ "$OSTYPE" =~ ^darwin ]]; then
|
||||||
|
warn "you are using macOS which has no KVM support, so the machine will run about 10 times slower."
|
||||||
|
else
|
||||||
|
kernel=$(uname -a)
|
||||||
|
case "${kernel,,}" in
|
||||||
|
*"microsoft"* )
|
||||||
|
error "Please bind '/dev/kvm' as a volume in the optional container settings when using Docker Desktop." ;;
|
||||||
|
*"synology"* )
|
||||||
|
error "Please make sure that Synology VMM (Virtual Machine Manager) is installed and that '/dev/kvm' is binded to this container." ;;
|
||||||
|
*)
|
||||||
|
error "KVM acceleration is not available $KVM_ERR, this will cause the machine to run about 10 times slower."
|
||||||
|
error "See the FAQ for possible causes, or disable acceleration by adding the \"KVM=N\" variable (not recommended)." ;;
|
||||||
|
esac
|
||||||
|
enabled "$DEBUG" || exit 88
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sanitize variables
|
||||||
|
TZ=$(strip "$TZ")
|
||||||
|
STORAGE=$(strip "$STORAGE")
|
||||||
|
COUNTRY=$(strip "$COUNTRY")
|
||||||
|
MACHINE=$(strip "${MACHINE,,}")
|
||||||
|
DISK_SIZE=$(strip "$DISK_SIZE")
|
||||||
|
|
||||||
|
# Helper variables
|
||||||
|
ROOTLESS="N"
|
||||||
|
PRIVILEGED="N"
|
||||||
|
ENGINE="Docker"
|
||||||
|
PROCESS="${APP,,}"
|
||||||
|
PROCESS="${PROCESS// /-}"
|
||||||
|
|
||||||
|
detectEngine
|
||||||
|
detectRootless
|
||||||
|
|
||||||
|
echo "❯ Starting $APP for $ENGINE v$(</etc/version)..."
|
||||||
|
echo "❯ For support visit $SUPPORT"
|
||||||
|
|
||||||
|
checkPrivileged
|
||||||
|
|
||||||
|
INFO="/run/shm/msg.html"
|
||||||
|
PAGE="/run/shm/index.html"
|
||||||
|
TEMPLATE="/var/www/index.html"
|
||||||
|
FOOTER1="$APP for $ENGINE v$(</etc/version)"
|
||||||
|
FOOTER2="<a href='$SUPPORT'>$SUPPORT</a>"
|
||||||
|
|
||||||
|
SOCKETS=1
|
||||||
|
CPU=$(cpu)
|
||||||
|
SYS=$(uname -r)
|
||||||
|
ARCH=$(dpkg --print-architecture)
|
||||||
|
CORES=$(grep -c '^processor' /proc/cpuinfo)
|
||||||
|
IFS=. read -r KERNEL MINOR _ <<< "$SYS"
|
||||||
|
|
||||||
|
checkSockets
|
||||||
|
checkCores
|
||||||
|
checkStorage
|
||||||
|
getMemoryInfo
|
||||||
|
|
||||||
|
# Print system info
|
||||||
|
SYS="${SYS/-generic/}"
|
||||||
|
FS=$(stat -f -c %T "$STORAGE")
|
||||||
|
FS="${FS/UNKNOWN //}"
|
||||||
|
FS="${FS/ext2\/ext3/ext4}"
|
||||||
|
FS=$(echo "$FS" | sed 's/[)(]//g')
|
||||||
|
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
|
||||||
|
SPACE_GB=$(formatBytes "$SPACE" "down")
|
||||||
|
AVAIL_MEM=$(formatBytes "$RAM_AVAIL" "down")
|
||||||
|
TOTAL_MEM=$(formatBytes "$RAM_TOTAL" "up")
|
||||||
|
|
||||||
|
echo "❯ CPU: ${CPU} | RAM: ${AVAIL_MEM/ GB/}/$TOTAL_MEM | DISK: $SPACE_GB (${FS}) | KERNEL: ${SYS}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
checkHost
|
||||||
|
checkKvm
|
||||||
|
|
||||||
|
# Runtime state is intentionally discarded at each container start; persistent
|
||||||
|
# machine and disk identity lives under STORAGE instead.
|
||||||
|
# Cleanup files
|
||||||
|
rm -f "$QEMU_DIR"/dsm.url
|
||||||
|
rm -f "$QEMU_DIR"/{qemu.*,*.{pid,sock,pipe}}
|
||||||
|
|
||||||
|
# Cleanup dirs
|
||||||
|
rm -rf /tmp/dsm
|
||||||
|
rm -rf "$STORAGE/tmp"
|
||||||
|
|
||||||
|
return 0
|
||||||
+175
-216
@@ -3,31 +3,54 @@ 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:]]/}"
|
||||||
[ -z "$BASE" ] && BASE="DSM_VirtualDSM_69057"
|
[ -z "$BASE" ] && BASE="DSM_VirtualDSM_69057"
|
||||||
else
|
else
|
||||||
# Fallback for old installs
|
# Fallback for old installs
|
||||||
BASE="DSM_VirtualDSM_42962"
|
BASE="DSM_VirtualDSM_42962"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$URL" ]; then
|
FN="boot.pat"
|
||||||
BASE=$(basename "$URL" .pat)
|
DIR=$(find / -maxdepth 1 -type d -iname "$FN" -print -quit)
|
||||||
if [ ! -s "$STORAGE/$BASE.system.img" ]; then
|
[ ! -d "$DIR" ] && DIR=$(find "$STORAGE" -maxdepth 1 -type d -iname "$FN" -print -quit)
|
||||||
BASE=$(basename "${URL%%\?*}" .pat)
|
|
||||||
: "${BASE//+/ }"; printf -v BASE '%b' "${_//%/\\x}"
|
# A boot.pat directory bind represents already extracted boot and system
|
||||||
BASE=$(echo "$BASE" | sed -e 's/[^A-Za-z0-9._-]/_/g')
|
# images and therefore takes precedence over PAT file or URL discovery.
|
||||||
fi
|
if [ -d "$DIR" ]; then
|
||||||
if [[ "${URL,,}" != "http"* ]]; then
|
BASE="DSM_VirtualDSM" && URL="file://$DIR"
|
||||||
if [ -s "$STORAGE/$BASE.pat" ]; then
|
if [[ ! -s "$STORAGE/$BASE.boot.img" || ! -s "$STORAGE/$BASE.system.img" ]]; then
|
||||||
URL="file://$STORAGE/$BASE.pat"
|
error "The bind $DIR maps to a file that does not exist!" && exit 65
|
||||||
else
|
|
||||||
error "File $STORAGE/$BASE.pat does not exist!" && exit 65
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -s "$STORAGE/$BASE.boot.img" ]] && [[ -s "$STORAGE/$BASE.system.img" ]]; then
|
FILE=$(find / -maxdepth 1 -type f -iname "$FN" -print -quit)
|
||||||
|
[ ! -s "$FILE" ] && FILE=$(find "$STORAGE" -maxdepth 1 -type f -iname "$FN" -print -quit)
|
||||||
|
[ -s "$FILE" ] && BASE="DSM_VirtualDSM" && URL="file://$FILE"
|
||||||
|
|
||||||
|
URL=$(strip "$URL")
|
||||||
|
|
||||||
|
# Derive a filesystem-safe identity from the URL only when no local boot.pat
|
||||||
|
# source was supplied; preserve an existing system image identity if present.
|
||||||
|
if [ -n "$URL" ] && [ ! -s "$FILE" ] && [ ! -d "$DIR" ]; then
|
||||||
|
BASE=$(basename "$URL" .pat)
|
||||||
|
if [ ! -s "$STORAGE/$BASE.system.img" ]; then
|
||||||
|
BASE=$(basename "${URL%%\?*}" .pat)
|
||||||
|
printf -v BASE '%b' "${BASE//%/\\x}"
|
||||||
|
BASE="${BASE//[!A-Za-z0-9._-]/_}"
|
||||||
|
fi
|
||||||
|
if [[ "${URL,,}" != "http"* && "${URL,,}" != "file:"* ]]; then
|
||||||
|
[ ! -s "$STORAGE/$BASE.pat" ] && error "Invalid URL: $URL" && exit 65
|
||||||
|
URL="file://$STORAGE/$BASE.pat"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A complete matching image pair is the installation marker; the cached PAT
|
||||||
|
# itself is optional after installation.
|
||||||
|
if [[ -s "$STORAGE/$BASE.boot.img" && -s "$STORAGE/$BASE.system.img" ]]; then
|
||||||
return 0 # Previous installation found
|
return 0 # Previous installation found
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -40,17 +63,23 @@ 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!"
|
||||||
[[ "${COUNTRY^^}" == "CN" ]] && DL="$DL_CHINA" || DL="$DL_GLOBAL"
|
[[ "${COUNTRY^^}" == "CN" ]] && DL="$DL_CHINA" || DL="$DL_GLOBAL"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[ -z "$URL" ] && URL="$DL/release/7.2.2/72806/DSM_VirtualDSM_72806.pat"
|
if [ -z "$URL" ]; then
|
||||||
|
URL="$DL/release/7.2.2/72806/DSM_VirtualDSM_72806.pat"
|
||||||
|
fi
|
||||||
|
|
||||||
BASE=$(basename "${URL%%\?*}" .pat)
|
if [ ! -s "$FILE" ]; then
|
||||||
: "${BASE//+/ }"; printf -v BASE '%b' "${_//%/\\x}"
|
BASE=$(basename "${URL%%\?*}" .pat)
|
||||||
BASE=$(echo "$BASE" | sed -e 's/[^A-Za-z0-9._-]/_/g')
|
printf -v BASE '%b' "${BASE//%/\\x}"
|
||||||
|
BASE="${BASE//[!A-Za-z0-9._-]/_}"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$URL" != "file://$STORAGE/$BASE.pat" ]]; then
|
if [[ "$URL" != "file://$STORAGE/$BASE.pat" ]]; then
|
||||||
rm -f "$STORAGE/$BASE.pat"
|
rm -f "$STORAGE/$BASE.pat"
|
||||||
@@ -63,176 +92,62 @@ rm -f "$STORAGE/$BASE.system.img"
|
|||||||
# Check filesystem
|
# Check filesystem
|
||||||
FS=$(stat -f -c %T "$STORAGE")
|
FS=$(stat -f -c %T "$STORAGE")
|
||||||
|
|
||||||
if [[ "${FS,,}" == "overlay"* ]]; then
|
if [[ "${FS,,}" == "overlay"* && "${ENGINE,,}" == "docker" ]]; then
|
||||||
info "Warning: the filesystem of $STORAGE is OverlayFS, this usually means it was binded to an invalid path!"
|
warn "the filesystem of $STORAGE is OverlayFS, this usually means it was binded to an invalid path!"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${FS,,}" == "fuse"* ]]; then
|
if [[ "${FS,,}" == "fuse"* ]]; then
|
||||||
info "Warning: the filesystem of $STORAGE is FUSE, this extra layer will negatively affect performance!"
|
warn "the filesystem of $STORAGE is FUSE, this extra layer will negatively affect performance!"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${FS,,}" == "ecryptfs" ]] || [[ "${FS,,}" == "tmpfs" ]]; then
|
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then
|
||||||
info "Warning: the filesystem of $STORAGE is $FS, which does not support O_DIRECT mode, adjusting settings..."
|
warn "the filesystem of $STORAGE is $FS, which does not support O_DIRECT mode, adjusting settings..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${FS,,}" == "fat"* || "${FS,,}" == "vfat"* || "${FS,,}" == "msdos"* ]]; then
|
if [[ "${FS,,}" == "fat"* || "${FS,,}" == "vfat"* || "${FS,,}" == "msdos"* ]]; then
|
||||||
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"
|
||||||
|
if ! makeDir "$TMP"; then
|
||||||
|
error "Failed to create directory \"$TMP\" !" && exit 93
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
TMP="/tmp/dsm"
|
TMP="/tmp/dsm"
|
||||||
TMP_SPACE=2147483648
|
TMP_SPACE=2147483648
|
||||||
SPACE=$(df --output=avail -B 1 /tmp | tail -n 1)
|
SPACE=$(df --output=avail -B 1 /tmp | tail -n 1)
|
||||||
SPACE_MB=$(( (SPACE + 1048575)/1048576 ))
|
SPACE_MB=$(formatBytes "$SPACE")
|
||||||
if (( TMP_SPACE > SPACE )); then
|
if (( TMP_SPACE > SPACE )); then
|
||||||
error "Not enough free space inside the container, have $SPACE_MB MB available but need at least 2 GB." && exit 93
|
error "Not enough free space inside the container, have $SPACE_MB available but need at least 2 GB." && exit 93
|
||||||
fi
|
fi
|
||||||
|
rm -rf "$TMP" && mkdir -p "$TMP"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -rf "$TMP" && mkdir -p "$TMP"
|
|
||||||
|
|
||||||
# Check free diskspace
|
# Check free diskspace
|
||||||
ROOT_SPACE=536870912
|
ROOT_SPACE=536870912
|
||||||
SPACE=$(df --output=avail -B 1 / | tail -n 1)
|
SPACE=$(df --output=avail -B 1 / | tail -n 1)
|
||||||
SPACE_MB=$(( (SPACE + 1048575)/1048576 ))
|
SPACE_MB=$(formatBytes "$SPACE" "down")
|
||||||
(( ROOT_SPACE > SPACE )) && error "Not enough free space inside the container, have $SPACE_MB MB available but need at least 500 MB." && exit 96
|
(( ROOT_SPACE > SPACE )) && error "Not enough free space inside the container, have $SPACE_MB available but need at least 500 MB." && exit 96
|
||||||
|
|
||||||
MIN_SPACE=15032385536
|
MIN_SPACE=15032385536
|
||||||
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
|
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
|
||||||
SPACE_GB=$(( (SPACE + 1073741823)/1073741824 ))
|
SPACE_GB=$(formatBytes "$SPACE")
|
||||||
(( MIN_SPACE > SPACE )) && error "Not enough free space for installation in $STORAGE, have $SPACE_GB 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 [[ "$URL" == "file://"* ]]; then
|
||||||
if [ -t 1 ]; then
|
MSG="Copying DSM"
|
||||||
PROGRESS="--progress=bar:noscroll"
|
ERR="Failed to copy ${URL:7}"
|
||||||
|
info "Install: Copying installation image..."
|
||||||
else
|
else
|
||||||
PROGRESS="--progress=dot:giga"
|
MSG="Downloading DSM"
|
||||||
|
ERR="Failed to download $URL"
|
||||||
|
info "Install: Downloading $BASE.pat..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Download the required files from the Synology website
|
|
||||||
|
|
||||||
ROOT="Y"
|
|
||||||
RD="$TMP/rd.gz"
|
|
||||||
RDC="$STORAGE/dsm.rd"
|
|
||||||
|
|
||||||
if [ ! -s "$RDC" ] && [[ "$URL" == "file://"* ]] && [[ "${URL,,}" == *"_42218.pat" ]]; then
|
|
||||||
|
|
||||||
rm -f "$RD"
|
|
||||||
rm -f "$RDC"
|
|
||||||
|
|
||||||
tar --extract --file="${URL:7}" --directory="$(dirname "$RD")"/. "$(basename "$RD")"
|
|
||||||
cp "$RD" "$RDC"
|
|
||||||
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -s "$RDC" ]; then
|
|
||||||
|
|
||||||
rm -f "$RD"
|
|
||||||
rm -f "$RDC"
|
|
||||||
|
|
||||||
MSG="Downloading installer"
|
|
||||||
info "Install: $MSG..." && html "$MSG..."
|
|
||||||
|
|
||||||
SIZE=5394188
|
|
||||||
POS="65627648-71021835"
|
|
||||||
VERIFY="b4215a4b213ff5154db0488f92c87864"
|
|
||||||
LOC="$DL/release/7.0.1/42218/DSM_VirtualDSM_42218.pat"
|
|
||||||
[[ "${URL,,}" == *"_42218.pat" ]] && LOC="$URL"
|
|
||||||
|
|
||||||
/run/progress.sh "$RD" "$SIZE" "$MSG ([P])..." &
|
|
||||||
{ curl -r "$POS" -sfk --connect-timeout 10 -S -o "$RD" "$LOC"; rc=$?; } || :
|
|
||||||
|
|
||||||
fKill "progress.sh"
|
|
||||||
|
|
||||||
ERR="Failed to download $LOC"
|
|
||||||
(( rc == 3 )) && error "$ERR , cannot write file (disk full?)" && exit 60
|
|
||||||
(( rc == 4 )) && error "$ERR , network failure!" && exit 60
|
|
||||||
(( rc == 8 )) && error "$ERR , server issued an error response!" && exit 60
|
|
||||||
|
|
||||||
if (( rc != 0 )); then
|
|
||||||
if (( rc != 22 )) && (( rc != 56 )); then
|
|
||||||
error "$ERR , reason: $rc" && exit 60
|
|
||||||
fi
|
|
||||||
SUM="skip"
|
|
||||||
else
|
|
||||||
SUM=$(md5sum "$RD" | cut -f 1 -d " ")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$SUM" != "$VERIFY" ]; then
|
|
||||||
|
|
||||||
PAT="/install.pat"
|
|
||||||
SIZE=379637760
|
|
||||||
|
|
||||||
rm -f "$RD"
|
|
||||||
rm -f "$PAT"
|
|
||||||
|
|
||||||
html "$MSG..."
|
|
||||||
/run/progress.sh "$PAT" "$SIZE" "$MSG ([P])..." &
|
|
||||||
{ wget "$LOC" -O "$PAT" -q --no-check-certificate --timeout=10 --no-http-keep-alive --show-progress "$PROGRESS"; rc=$?; } || :
|
|
||||||
|
|
||||||
fKill "progress.sh"
|
|
||||||
|
|
||||||
ERR="Failed to download $LOC"
|
|
||||||
(( rc == 3 )) && error "$ERR , cannot write file (disk full?)" && exit 60
|
|
||||||
(( rc == 4 )) && error "$ERR , network failure!" && exit 60
|
|
||||||
(( rc == 8 )) && error "$ERR , server issued an error response!" && exit 60
|
|
||||||
(( rc != 0 )) && error "$ERR , reason: $rc" && exit 60
|
|
||||||
|
|
||||||
tar --extract --file="$PAT" --directory="$(dirname "$RD")"/. "$(basename "$RD")"
|
|
||||||
rm "$PAT"
|
|
||||||
|
|
||||||
fi
|
|
||||||
|
|
||||||
cp "$RD" "$RDC"
|
|
||||||
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "$RDC" ]; then
|
|
||||||
|
|
||||||
{ xz -dc <"$RDC" >"$TMP/rd" 2>/dev/null; rc=$?; } || :
|
|
||||||
(( rc != 1 )) && error "Failed to unxz $RDC on $FS, reason $rc" && exit 91
|
|
||||||
|
|
||||||
{ (cd "$TMP" && cpio -idm <"$TMP/rd" 2>/dev/null); rc=$?; } || :
|
|
||||||
|
|
||||||
if (( rc != 0 )); then
|
|
||||||
ROOT="N"
|
|
||||||
{ (cd "$TMP" && fakeroot cpio -idmu <"$TMP/rd" 2>/dev/null); rc=$?; } || :
|
|
||||||
(( rc != 0 )) && error "Failed to extract $RDC on $FS, reason $rc" && exit 92
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -rf /run/extract && mkdir -p /run/extract
|
|
||||||
for file in $TMP/usr/lib/libcurl.so.4 \
|
|
||||||
$TMP/usr/lib/libmbedcrypto.so.5 \
|
|
||||||
$TMP/usr/lib/libmbedtls.so.13 \
|
|
||||||
$TMP/usr/lib/libmbedx509.so.1 \
|
|
||||||
$TMP/usr/lib/libmsgpackc.so.2 \
|
|
||||||
$TMP/usr/lib/libsodium.so \
|
|
||||||
$TMP/usr/lib/libsynocodesign-ng-virtual-junior-wins.so.7 \
|
|
||||||
$TMP/usr/syno/bin/scemd; do
|
|
||||||
cp "$file" /run/extract/
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$ARCH" != "amd64" ]; then
|
|
||||||
mkdir -p /lib64/
|
|
||||||
cp "$TMP/usr/lib/libc.so.6" /lib64/
|
|
||||||
cp "$TMP/usr/lib/libpthread.so.0" /lib64/
|
|
||||||
cp "$TMP/usr/lib/ld-linux-x86-64.so.2" /lib64/
|
|
||||||
fi
|
|
||||||
|
|
||||||
mv /run/extract/scemd /run/extract/syno_extract_system_patch
|
|
||||||
chmod +x /run/extract/syno_extract_system_patch
|
|
||||||
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -rf "$TMP" && mkdir -p "$TMP"
|
|
||||||
|
|
||||||
info "Install: Downloading $BASE.pat..."
|
|
||||||
|
|
||||||
MSG="Downloading DSM"
|
|
||||||
ERR="Failed to download $URL"
|
|
||||||
|
|
||||||
html "$MSG..."
|
html "$MSG..."
|
||||||
|
|
||||||
PAT="/$BASE.pat"
|
PAT="/$BASE.pat"
|
||||||
@@ -240,25 +155,62 @@ rm -f "$PAT"
|
|||||||
|
|
||||||
if [[ "$URL" == "file://"* ]]; then
|
if [[ "$URL" == "file://"* ]]; then
|
||||||
|
|
||||||
|
if [ ! -f "${URL:7}" ]; then
|
||||||
|
error "File '${URL:7}' does not exist!" && exit 65
|
||||||
|
fi
|
||||||
|
|
||||||
cp "${URL:7}" "$PAT"
|
cp "${URL:7}" "$PAT"
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|
||||||
SIZE=0
|
SIZE=0
|
||||||
|
REASON=""
|
||||||
|
PROGRESS=()
|
||||||
|
OUTPUT=""
|
||||||
|
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
|
||||||
|
|
||||||
{ wget "$URL" -O "$PAT" -q --no-check-certificate --timeout=10 --no-http-keep-alive --show-progress "$PROGRESS"; rc=$?; } || :
|
/run/progress.sh "$PAT" "$SIZE" "$MSG ([P])..." "$OUTPUT" 52428800 &
|
||||||
|
|
||||||
|
{
|
||||||
|
LC_ALL=C wget "$URL" -O "$PAT" --no-verbose --no-check-certificate \
|
||||||
|
--timeout=30 --no-http-keep-alive "${PROGRESS[@]}" \
|
||||||
|
--output-file="$LOG"
|
||||||
|
rc=$?
|
||||||
|
} || :
|
||||||
|
|
||||||
fKill "progress.sh"
|
fKill "progress.sh"
|
||||||
|
|
||||||
(( rc == 3 )) && error "$ERR , cannot write file (disk full?)" && exit 69
|
if (( rc != 0 )); then
|
||||||
(( rc == 4 )) && error "$ERR , network failure!" && exit 69
|
REASON=$(sed -n \
|
||||||
(( rc == 8 )) && error "$ERR , server issued an error response!" && exit 69
|
-e 's/^wget: //p' \
|
||||||
(( rc != 0 )) && error "$ERR , reason: $rc" && exit 69
|
-e 's/^[0-9-]\{10\} [0-9:]\{8\} ERROR //p' \
|
||||||
|
"$LOG" | tail -n 1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$LOG"
|
||||||
|
|
||||||
|
if (( rc == 3 )); then
|
||||||
|
error "$ERR because the file could not be written (disk full?)."
|
||||||
|
exit 69
|
||||||
|
elif (( rc != 0 )); then
|
||||||
|
if [ -n "$REASON" ]; then
|
||||||
|
error "$ERR: ${REASON%.}."
|
||||||
|
else
|
||||||
|
error "$ERR with exit status $rc."
|
||||||
|
fi
|
||||||
|
exit 69
|
||||||
|
fi
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -266,43 +218,43 @@ 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
|
||||||
|
|
||||||
MSG="Extracting downloaded 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/."
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|
||||||
export LD_LIBRARY_PATH="/run/extract"
|
{ (cd "$TMP" && python3 /run/extract.py -i "$PAT" -d 2>/run/extract.log); rc=$?; } || :
|
||||||
|
|
||||||
if [ "$ARCH" == "amd64" ]; then
|
if (( rc != 0 )); then
|
||||||
{ /run/extract/syno_extract_system_patch "$PAT" "$TMP/."; rc=$?; } || :
|
cat /run/extract.log
|
||||||
else
|
error "Failed to extract PAT file, reason $rc" && exit 63
|
||||||
{ qemu-x86_64 /run/extract/syno_extract_system_patch "$PAT" "$TMP/."; rc=$?; } || :
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export LD_LIBRARY_PATH=""
|
|
||||||
|
|
||||||
(( rc != 0 )) && error "Failed to extract PAT file, reason $rc" && exit 63
|
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -rf /run/extract
|
|
||||||
|
|
||||||
MSG="Preparing system partition..."
|
MSG="Preparing system partition..."
|
||||||
info "Install: $MSG" && html "$MSG"
|
info "Install: $MSG" && html "$MSG"
|
||||||
|
|
||||||
BOOT=$(find "$TMP" -name "*.bin.zip")
|
# The PAT boot archive becomes the persistent QEMU boot disk after its
|
||||||
[ ! -s "$BOOT" ] && error "The PAT file contains no boot image." && exit 67
|
# companion system partition has been assembled.
|
||||||
|
BOOT=$(find "$TMP" -name "*.bin.zip" -print -quit)
|
||||||
|
[ -z "$BOOT" ] && error "The PAT file contains no boot image." && exit 67
|
||||||
|
[ ! -s "$BOOT" ] && error "The PAT boot image archive is empty." && exit 67
|
||||||
|
|
||||||
BOOT=$(echo "$BOOT" | head -c -5)
|
unzip -q -o "$BOOT" -d "$TMP"
|
||||||
unzip -q -o "$BOOT".zip -d "$TMP"
|
BOOT="${BOOT%.zip}"
|
||||||
|
|
||||||
SYSTEM="$STORAGE/$BASE.system.img"
|
SYSTEM="$STORAGE/$BASE.system.img"
|
||||||
rm -f "$SYSTEM"
|
rm -f "$SYSTEM"
|
||||||
@@ -310,16 +262,18 @@ rm -f "$SYSTEM"
|
|||||||
# Check free diskspace
|
# Check free diskspace
|
||||||
SYSTEM_SIZE=10738466816
|
SYSTEM_SIZE=10738466816
|
||||||
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
|
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
|
||||||
SPACE_MB=$(( (SPACE + 1048575)/1048576 ))
|
SPACE_MB=$(formatBytes "$SPACE")
|
||||||
|
|
||||||
if (( SYSTEM_SIZE > SPACE )); then
|
if (( SYSTEM_SIZE > SPACE )); then
|
||||||
error "Not enough free space in $STORAGE to create a 10 GB system disk, have only $SPACE_MB MB available." && exit 97
|
error "Not enough free space in $STORAGE to create a 10 GB system disk, have only $SPACE_MB available." && exit 97
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! touch "$SYSTEM"; then
|
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\" !"
|
||||||
|
|
||||||
if [[ "${FS,,}" == "btrfs" ]]; then
|
if [[ "${FS,,}" == "btrfs" ]]; then
|
||||||
{ chattr +C "$SYSTEM"; } || :
|
{ chattr +C "$SYSTEM"; } || :
|
||||||
FA=$(lsattr "$SYSTEM")
|
FA=$(lsattr "$SYSTEM")
|
||||||
@@ -328,29 +282,38 @@ if [[ "${FS,,}" == "btrfs" ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! fallocate -l "$SYSTEM_SIZE" "$SYSTEM"; then
|
if ! fallocate -l "$SYSTEM_SIZE" "$SYSTEM" &>/dev/null; then
|
||||||
if ! truncate -s "$SYSTEM_SIZE" "$SYSTEM"; then
|
if ! fallocate -l -x "$SYSTEM_SIZE" "$SYSTEM"; then
|
||||||
rm -f "$SYSTEM"
|
if ! truncate -s "$SYSTEM_SIZE" "$SYSTEM"; then
|
||||||
error "Could not allocate file $SYSTEM for the system disk." && exit 98
|
rm -f "$SYSTEM"
|
||||||
|
error "Could not allocate file $SYSTEM for the system disk." && exit 98
|
||||||
|
fi
|
||||||
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"
|
||||||
|
|
||||||
{ echo "label: dos"
|
{
|
||||||
echo "label-id: 0x6f9ee2e9"
|
echo "label: dos"
|
||||||
echo "device: $SYSTEM"
|
echo "label-id: 0x6f9ee2e9"
|
||||||
echo "unit: sectors"
|
echo "device: $SYSTEM"
|
||||||
echo "sector-size: 512"
|
echo "unit: sectors"
|
||||||
echo ""
|
echo "sector-size: 512"
|
||||||
echo "${SYSTEM}1 : start= 2048, size= 16777216, type=83"
|
echo ""
|
||||||
echo "${SYSTEM}2 : start= 16779264, size= 4194304, type=82"
|
echo "${SYSTEM}1 : start= 2048, size= 16777216, type=83"
|
||||||
|
echo "${SYSTEM}2 : start= 16779264, size= 4194304, type=82"
|
||||||
} > "$PART"
|
} > "$PART"
|
||||||
|
|
||||||
sfdisk -q "$SYSTEM" < "$PART"
|
sfdisk -q "$SYSTEM" < "$PART"
|
||||||
|
|
||||||
MOUNT="$TMP/system"
|
MOUNT="$TMP/system"
|
||||||
rm -rf "$MOUNT" && mkdir -p "$MOUNT"
|
rm -rf "$MOUNT"
|
||||||
|
|
||||||
|
if ! makeDir "$MOUNT"; then
|
||||||
|
error "Failed to create directory \"$MOUNT\" !" && exit 93
|
||||||
|
fi
|
||||||
|
|
||||||
MSG="Extracting system partition..."
|
MSG="Extracting system partition..."
|
||||||
info "Install: $MSG" && html "$MSG"
|
info "Install: $MSG" && html "$MSG"
|
||||||
@@ -366,12 +329,10 @@ mv "$HDA.tgz" "$HDA.txz"
|
|||||||
[ -d "$PKG" ] && mv "$PKG/" "$MOUNT/.SynoUpgradePackages/"
|
[ -d "$PKG" ] && mv "$PKG/" "$MOUNT/.SynoUpgradePackages/"
|
||||||
rm -f "$MOUNT/.SynoUpgradePackages/ActiveInsight-"*
|
rm -f "$MOUNT/.SynoUpgradePackages/ActiveInsight-"*
|
||||||
|
|
||||||
[ -s "$HDP.txz" ] && tar xpfJ "$HDP.txz" --absolute-names -C "$MOUNT/"
|
INDEX_DB="$MOUNT/usr/syno/synoman/indexdb"
|
||||||
|
|
||||||
if [ -s "$IDB.txz" ]; then
|
if [ -s "$IDB.txz" ]; then
|
||||||
INDEX_DB="$MOUNT/usr/syno/synoman/indexdb/"
|
|
||||||
mkdir -p "$INDEX_DB"
|
mkdir -p "$INDEX_DB"
|
||||||
tar xpfJ "$IDB.txz" --absolute-names -C "$INDEX_DB"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
LABEL="1.44.1-42218"
|
LABEL="1.44.1-42218"
|
||||||
@@ -379,36 +340,34 @@ OFFSET="1048576" # 2048 * 512
|
|||||||
NUMBLOCKS="2097152" # (16777216 * 512) / 4096
|
NUMBLOCKS="2097152" # (16777216 * 512) / 4096
|
||||||
MSG="Installing system partition..."
|
MSG="Installing system partition..."
|
||||||
|
|
||||||
if [[ "$ROOT" != [Nn]* ]]; then
|
# Build the ext4 filesystem directly from the extracted tree under fakeroot,
|
||||||
|
# preserving archive ownership without mounting a loop device.
|
||||||
tar xpfJ "$HDA.txz" --absolute-names --skip-old-files -C "$MOUNT/"
|
fakeroot -- bash -c "set -Eeu;\
|
||||||
|
[ -s $HDP.txz ] && tar xpfJ $HDP.txz --absolute-names -C $MOUNT/;\
|
||||||
info "Install: $MSG" && html "$MSG"
|
[ -s $IDB.txz ] && tar xpfJ $IDB.txz --absolute-names -C $INDEX_DB/;\
|
||||||
|
tar xpfJ $HDA.txz --absolute-names --skip-old-files -C $MOUNT/;\
|
||||||
mke2fs -q -t ext4 -b 4096 -d "$MOUNT/" -L "$LABEL" -F -E "offset=$OFFSET" "$SYSTEM" "$NUMBLOCKS"
|
printf '%b%s%b' '\E[1;34m❯ \E[1;36m' 'Install: $MSG' '\E[0m\n';\
|
||||||
|
mke2fs -q -t ext4 -b 4096 -d $MOUNT/ -L $LABEL -F -E offset=$OFFSET $SYSTEM $NUMBLOCKS"
|
||||||
else
|
|
||||||
|
|
||||||
fakeroot -- bash -c "set -Eeu;\
|
|
||||||
tar xpfJ $HDA.txz --absolute-names --skip-old-files -C $MOUNT/;\
|
|
||||||
printf '%b%s%b' '\E[1;34m❯ \E[1;36m' 'Install: $MSG' '\E[0m\n';\
|
|
||||||
mke2fs -q -t ext4 -b 4096 -d $MOUNT/ -L $LABEL -F -E offset=$OFFSET $SYSTEM $NUMBLOCKS"
|
|
||||||
|
|
||||||
fi
|
|
||||||
|
|
||||||
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\" !"
|
||||||
|
|
||||||
|
# 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
|
||||||
mv -f "$PAT" "$STORAGE/$BASE.pat"
|
mv -f "$PAT" "$STORAGE/$BASE.pat"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -f "$STORAGE/$BASE.pat" ]; then
|
||||||
|
setOwner "$STORAGE/$BASE.pat" || warn "failed to set the owner for \"$STORAGE/$BASE.pat\" !"
|
||||||
|
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\" !"
|
||||||
|
|
||||||
rm -rf "$TMP"
|
rm -rf "$TMP"
|
||||||
|
|
||||||
html "Booting DSM instance..."
|
|
||||||
sleep 1.2
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
+235
@@ -0,0 +1,235 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
normalizeMemory() {
|
||||||
|
|
||||||
|
local wanted
|
||||||
|
|
||||||
|
RAM_SPARE=500000000
|
||||||
|
RAM_MINIMUM="${RAM_MINIMUM:-1073741824}"
|
||||||
|
|
||||||
|
RAM_MINIMUM=$(strip "$RAM_MINIMUM")
|
||||||
|
RAM_MINIMUM="${RAM_MINIMUM// /}"
|
||||||
|
RAM_MINIMUM=$(echo "${RAM_MINIMUM^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
|
||||||
|
numfmt --from=iec "$RAM_MINIMUM" &>/dev/null || {
|
||||||
|
error "Invalid RAM_MINIMUM: $RAM_MINIMUM"
|
||||||
|
exit 16
|
||||||
|
}
|
||||||
|
RAM_MINIMUM=$(numfmt --from=iec "$RAM_MINIMUM")
|
||||||
|
|
||||||
|
RAM_SIZE=$(strip "$RAM_SIZE")
|
||||||
|
RAM_SIZE="${RAM_SIZE// /}"
|
||||||
|
[ -z "$RAM_SIZE" ] && RAM_SIZE="2G"
|
||||||
|
|
||||||
|
if [[ "${RAM_SIZE,,}" != "max" && "${RAM_SIZE,,}" != "half" ]]; then
|
||||||
|
|
||||||
|
# Bare values below 130 are interpreted as GiB for convenience; larger bare
|
||||||
|
# values are treated as MiB to preserve historical configurations.
|
||||||
|
if [ -z "${RAM_SIZE//[0-9. ]}" ]; then
|
||||||
|
[ "${RAM_SIZE%%.*}" -lt "130" ] && RAM_SIZE="${RAM_SIZE}G" || RAM_SIZE="${RAM_SIZE}M"
|
||||||
|
fi
|
||||||
|
|
||||||
|
RAM_SIZE=$(echo "${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
|
||||||
|
numfmt --from=iec "$RAM_SIZE" &>/dev/null || {
|
||||||
|
error "Invalid RAM_SIZE: $RAM_SIZE"
|
||||||
|
exit 16
|
||||||
|
}
|
||||||
|
|
||||||
|
wanted=$(numfmt --from=iec "$RAM_SIZE")
|
||||||
|
|
||||||
|
if [ "$wanted" -lt "$RAM_MINIMUM" ]; then
|
||||||
|
error "$(app) requires at least $(formatBytes "$RAM_MINIMUM") of RAM, but RAM_SIZE is set to $(formatBytes "$wanted")."
|
||||||
|
exit 16
|
||||||
|
fi
|
||||||
|
|
||||||
|
# QEMU requires a whole-number memory value, so convert decimal sizes to MiB.
|
||||||
|
if [[ "$RAM_SIZE" == *.* ]]; then
|
||||||
|
RAM_SIZE="$(( wanted / 1048576 ))M"
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
checkConfiguredMemory() {
|
||||||
|
|
||||||
|
local final="$1"
|
||||||
|
|
||||||
|
if disabled "$RAM_CHECK" || [[ "${RAM_SIZE,,}" == "max" || "${RAM_SIZE,,}" == "half" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local wanted avail_mem
|
||||||
|
wanted=$(numfmt --from=iec "$RAM_SIZE")
|
||||||
|
avail_mem=$(formatBytes "$RAM_AVAIL")
|
||||||
|
|
||||||
|
if (( (wanted + RAM_SPARE) > RAM_AVAIL )); then
|
||||||
|
|
||||||
|
local msg="Your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is too high for the $avail_mem of free memory available,"
|
||||||
|
|
||||||
|
# ZFS ARC can release cached memory under pressure, so this free-memory
|
||||||
|
# heuristic remains informational instead of rewriting RAM_SIZE.
|
||||||
|
if [[ "${FS,,}" == "zfs" ]]; then
|
||||||
|
|
||||||
|
enabled "$final" && info "$msg but since ZFS is active this will be ignored."
|
||||||
|
|
||||||
|
else
|
||||||
|
|
||||||
|
RAM_SIZE="max"
|
||||||
|
RAM_WARNING="$msg it will automatically be adjusted to a lower amount."
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
else
|
||||||
|
|
||||||
|
if (( (wanted + (RAM_SPARE * 3)) > RAM_AVAIL )); then
|
||||||
|
|
||||||
|
local msg="your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is very close to the $avail_mem of free memory available,"
|
||||||
|
|
||||||
|
if [[ "${FS,,}" == "zfs" ]]; then
|
||||||
|
enabled "$final" && info "$msg but since ZFS is active this will be ignored."
|
||||||
|
else
|
||||||
|
enabled "$final" && warn "$msg please consider a lower amount."
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
configureHalfMemory() {
|
||||||
|
|
||||||
|
if [[ "${RAM_SIZE,,}" != "half" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( (RAM_AVAIL / 2) > RAM_SPARE )); then
|
||||||
|
|
||||||
|
local wanted=$(( RAM_AVAIL / 2 ))
|
||||||
|
|
||||||
|
# Divide by one byte more than a MiB to round down
|
||||||
|
local target=$(( wanted / 1048577 ))
|
||||||
|
RAM_SIZE="${target}M"
|
||||||
|
RAM_ALLOCATION="$wanted"
|
||||||
|
|
||||||
|
else
|
||||||
|
|
||||||
|
RAM_SIZE="max"
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
configureMaxMemory() {
|
||||||
|
|
||||||
|
if [[ "${RAM_SIZE,,}" != "max" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# max keeps a host reserve when possible, but on very small systems falls back
|
||||||
|
# to half the available memory to avoid starving the container.
|
||||||
|
if (( RAM_AVAIL < (RAM_SPARE * 2) )); then
|
||||||
|
|
||||||
|
local wanted=$(( RAM_AVAIL / 2 ))
|
||||||
|
|
||||||
|
else
|
||||||
|
|
||||||
|
local wanted=$(( RAM_AVAIL - (RAM_SPARE * 3) ))
|
||||||
|
|
||||||
|
if (( wanted < (RAM_SPARE * 6) )); then
|
||||||
|
wanted=$(( RAM_AVAIL - RAM_SPARE ))
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Divide by one byte more than a MiB to round down
|
||||||
|
local target=$(( wanted / 1048577 ))
|
||||||
|
RAM_SIZE="${target}M"
|
||||||
|
RAM_ALLOCATION="$wanted"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
showMemoryLimitHint() {
|
||||||
|
|
||||||
|
local kernel
|
||||||
|
kernel=$(uname -r)
|
||||||
|
|
||||||
|
if [[ "${kernel,,}" == *-wsl2* ]]; then
|
||||||
|
echo
|
||||||
|
info "Docker Desktop (WSL2) is detected, follow these instructions:"
|
||||||
|
info ""
|
||||||
|
info "Increase the memory limit in \"%UserProfile%\\.wslconfig\" by setting \"memory=<size>\" under \"[wsl2]\"."
|
||||||
|
info "Then run \"wsl --shutdown\" in PowerShell and restart Docker Desktop for the new limit to take effect."
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
checkMinimumMemory() {
|
||||||
|
|
||||||
|
local wanted
|
||||||
|
wanted=$(numfmt --from=iec "$RAM_SIZE")
|
||||||
|
|
||||||
|
if [ "$wanted" -lt "$RAM_MINIMUM" ]; then
|
||||||
|
|
||||||
|
error "$(app) requires at least $(formatBytes "$RAM_MINIMUM") of RAM, but only $(formatBytes "$wanted") can be allocated."
|
||||||
|
|
||||||
|
showMemoryLimitHint
|
||||||
|
|
||||||
|
exit 16
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
checkMemoryAllocation() {
|
||||||
|
|
||||||
|
local final="${1:-N}"
|
||||||
|
local configured
|
||||||
|
|
||||||
|
normalizeMemory
|
||||||
|
configured="$RAM_SIZE"
|
||||||
|
|
||||||
|
RAM_WARNING=""
|
||||||
|
RAM_ALLOCATION=""
|
||||||
|
|
||||||
|
getMemoryInfo
|
||||||
|
checkConfiguredMemory "$final"
|
||||||
|
|
||||||
|
configureHalfMemory
|
||||||
|
configureMaxMemory
|
||||||
|
checkMinimumMemory
|
||||||
|
|
||||||
|
if enabled "$final"; then
|
||||||
|
[ -n "$RAM_WARNING" ] && warn "$RAM_WARNING"
|
||||||
|
[ -n "$RAM_ALLOCATION" ] && info "Allocated $(formatBytes "$RAM_ALLOCATION") of RAM for $(app)."
|
||||||
|
else
|
||||||
|
RAM_SIZE="$configured"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
checkMemoryRequirement() {
|
||||||
|
|
||||||
|
checkMemoryAllocation "N"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
finalizeMemory() {
|
||||||
|
|
||||||
|
checkMemoryAllocation "Y"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
checkMemoryRequirement
|
||||||
|
|
||||||
|
return 0
|
||||||
+2156
-244
File diff suppressed because it is too large
Load Diff
+345
-123
@@ -1,137 +1,256 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
: "${SHUTDOWN:="Y"}" # Graceful ACPI shutdown
|
||||||
|
: "${TIMEOUT:="105"}" # QEMU termination 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_TIMEOUT=50
|
|
||||||
API_HOST="127.0.0.1:2210"
|
|
||||||
|
|
||||||
QEMU_TERM=""
|
SHUTDOWN_SKIP=0
|
||||||
QEMU_PORT=7100
|
SHUTDOWN_SIGNAL=0
|
||||||
QEMU_TIMEOUT=50
|
|
||||||
QEMU_DIR="/run/shm"
|
|
||||||
QEMU_PID="$QEMU_DIR/qemu.pid"
|
|
||||||
QEMU_LOG="$QEMU_DIR/qemu.log"
|
|
||||||
QEMU_OUT="$QEMU_DIR/qemu.out"
|
|
||||||
QEMU_END="$QEMU_DIR/qemu.end"
|
QEMU_END="$QEMU_DIR/qemu.end"
|
||||||
|
CONSOLE_PID="$QEMU_DIR/console.pid"
|
||||||
if [[ "$KVM" == [Nn]* ]]; then
|
CONSOLE_SOCKET="$QEMU_DIR/console.sock"
|
||||||
API_TIMEOUT=$(( API_TIMEOUT*2 ))
|
QEMU_START_PID="$QEMU_DIR/qemu.start.pid"
|
||||||
QEMU_TIMEOUT=$(( QEMU_TIMEOUT*2 ))
|
|
||||||
fi
|
|
||||||
|
|
||||||
touch "$QEMU_LOG"
|
|
||||||
|
|
||||||
_trap() {
|
_trap() {
|
||||||
func="$1" ; shift
|
|
||||||
for sig ; do
|
local func="$1"; shift
|
||||||
|
local sig
|
||||||
|
|
||||||
|
TRAP_PID=$BASHPID
|
||||||
|
|
||||||
|
for sig; do
|
||||||
|
# Capture the local callback and signal while registering the trap.
|
||||||
|
# shellcheck disable=SC2064
|
||||||
trap "$func $sig" "$sig"
|
trap "$func $sig" "$sig"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
signalCode() {
|
||||||
|
|
||||||
|
local sig="$1"
|
||||||
|
|
||||||
|
case "$sig" in
|
||||||
|
SIGHUP) echo 129 ;;
|
||||||
|
SIGINT) echo 130 ;;
|
||||||
|
SIGQUIT) echo 131 ;;
|
||||||
|
SIGABRT) echo 134 ;;
|
||||||
|
SIGTERM) echo 143 ;;
|
||||||
|
*) echo 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
displayReason() {
|
||||||
|
|
||||||
|
local reason="$1"
|
||||||
|
|
||||||
|
case "$reason" in
|
||||||
|
129 ) echo "SIGHUP" ;;
|
||||||
|
130 ) echo "SIGINT" ;;
|
||||||
|
131 ) echo "SIGQUIT" ;;
|
||||||
|
134 ) echo "SIGABRT" ;;
|
||||||
|
143 ) echo "SIGTERM" ;;
|
||||||
|
* ) echo "$reason" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
readQemuPid() {
|
||||||
|
|
||||||
|
# Interactive startup uses a wrapper-created PID file before QEMU writes its
|
||||||
|
# own pidfile, so accept either during startup and shutdown races.
|
||||||
|
readPidFile "$1" "$QEMU_START_PID" && return 0
|
||||||
|
readPidFile "$1" "$QEMU_PID"
|
||||||
|
}
|
||||||
|
|
||||||
|
qemuPidFile() {
|
||||||
|
|
||||||
|
local -n _file="$1"
|
||||||
|
|
||||||
|
_file="$QEMU_PID"
|
||||||
|
[ -s "$QEMU_START_PID" ] && _file="$QEMU_START_PID"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
waitQemuExit() {
|
||||||
|
|
||||||
|
local timeout="${1:-10}"
|
||||||
|
local file
|
||||||
|
|
||||||
|
qemuPidFile file
|
||||||
|
waitPidFile "$file" "$timeout"
|
||||||
|
}
|
||||||
|
|
||||||
|
waitQemuPid() {
|
||||||
|
|
||||||
|
local cnt=0
|
||||||
|
|
||||||
|
while ! readQemuPid "$1"; do
|
||||||
|
sleep 0.02
|
||||||
|
cnt=$((cnt + 1))
|
||||||
|
(( cnt >= 50 )) && return 1
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
forceKillQemu() {
|
||||||
|
|
||||||
|
local reason="$1"
|
||||||
|
local pid display
|
||||||
|
|
||||||
|
readQemuPid pid || return 0
|
||||||
|
isAlive "$pid" || return 0
|
||||||
|
|
||||||
|
display=$(displayReason "$reason")
|
||||||
|
error "Forcefully terminating $(app), reason: $display..."
|
||||||
|
{ disown "$pid" || :; kill -9 -- "$pid" || :; } 2>/dev/null
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupHelpers() {
|
||||||
|
|
||||||
|
local pids=( "${HOST_PID:-}" "${WSD_PID:-}" "${CONSOLE_PID:-}" \
|
||||||
|
"${WEB_PID:-}" "${PASST_PID:-}" "${DNSMASQ_PID:-}" )
|
||||||
|
|
||||||
|
mKill "${pids[@]}"
|
||||||
|
fKill "print.sh"
|
||||||
|
|
||||||
|
rm -f -- "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET"
|
||||||
|
|
||||||
|
closeNetwork
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
startConsole() {
|
||||||
|
|
||||||
|
local output="${1:-/dev/tty}"
|
||||||
|
local cnt=0
|
||||||
|
|
||||||
|
rm -f -- "$CONSOLE_SOCKET" "$CONSOLE_PID"
|
||||||
|
|
||||||
|
if ! stty -icanon -echo isig -ixon min 1 time 0 </dev/tty; then
|
||||||
|
error "Failed to configure serial console terminal!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
(
|
||||||
|
trap '' INT QUIT
|
||||||
|
exec nc -lU "$CONSOLE_SOCKET" </dev/tty >"$output"
|
||||||
|
) &
|
||||||
|
|
||||||
|
local pid="$!"
|
||||||
|
echo "$pid" > "$CONSOLE_PID"
|
||||||
|
|
||||||
|
while [ ! -S "$CONSOLE_SOCKET" ]; do
|
||||||
|
|
||||||
|
if ! isAlive "$pid"; then
|
||||||
|
rm -f -- "$CONSOLE_PID"
|
||||||
|
error "Serial console relay exited unexpectedly!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 0.02
|
||||||
|
cnt=$((cnt + 1))
|
||||||
|
|
||||||
|
if (( cnt > 100 )); then
|
||||||
|
error "Failed to start serial console relay!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
stopConsole() {
|
||||||
|
|
||||||
|
mKill "$CONSOLE_PID"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
startQemu() {
|
||||||
|
|
||||||
|
rm -f -- "$QEMU_START_PID"
|
||||||
|
|
||||||
|
# Launch QEMU in a separate session while recording the real child PID;
|
||||||
|
# setsid's wrapper PID is not suitable for guest shutdown or forced cleanup.
|
||||||
|
(
|
||||||
|
trap '' INT QUIT
|
||||||
|
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
exec setsid -f -w sh -c '
|
||||||
|
file=$1
|
||||||
|
shift
|
||||||
|
|
||||||
|
"$@" &
|
||||||
|
pid=$!
|
||||||
|
printf "%s\n" "$pid" > "$file" || exit 1
|
||||||
|
|
||||||
|
rc=0
|
||||||
|
wait "$pid" 2>/dev/null || rc=$?
|
||||||
|
exit "$rc"
|
||||||
|
' sh "$QEMU_START_PID" "$@"
|
||||||
|
) </dev/null &
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
finish() {
|
finish() {
|
||||||
|
|
||||||
local pid
|
local reason=$1 failed=0
|
||||||
local reason=$1
|
|
||||||
|
if [ ! -f "$QEMU_END" ] && (( reason != 0 )); then
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
|
||||||
touch "$QEMU_END"
|
touch "$QEMU_END"
|
||||||
|
|
||||||
if [ -s "$QEMU_PID" ]; then
|
forceKillQemu "$reason"
|
||||||
|
cleanupHelpers
|
||||||
|
|
||||||
pid=$(<"$QEMU_PID")
|
if ! waitQemuExit 10; then
|
||||||
echo && error "Forcefully terminating QEMU process, reason: $reason..."
|
warn "Timed out while waiting for $(app) to exit!"
|
||||||
{ kill -15 "$pid" || true; } 2>/dev/null
|
|
||||||
|
|
||||||
while isAlive "$pid"; do
|
|
||||||
sleep 1
|
|
||||||
# Workaround for zombie pid
|
|
||||||
[ ! -s "$QEMU_PID" ] && break
|
|
||||||
done
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
fKill "print.sh"
|
echo
|
||||||
fKill "host.bin"
|
|
||||||
|
|
||||||
closeNetwork
|
if (( failed == 0 )); then
|
||||||
|
echo "❯ Shutdown completed!"
|
||||||
sleep 1
|
else
|
||||||
echo && echo "❯ Shutdown completed!"
|
error "QEMU exited unexpectedly!"
|
||||||
|
fi
|
||||||
|
|
||||||
exit "$reason"
|
exit "$reason"
|
||||||
}
|
}
|
||||||
|
|
||||||
terminal() {
|
sendGuestShutdown() {
|
||||||
|
|
||||||
local dev=""
|
local pid="$1"
|
||||||
|
local response
|
||||||
if [ -s "$QEMU_OUT" ]; then
|
|
||||||
|
|
||||||
local msg
|
|
||||||
msg=$(<"$QEMU_OUT")
|
|
||||||
|
|
||||||
if [ -n "$msg" ]; then
|
|
||||||
|
|
||||||
if [[ "${msg,,}" != "char"* || "$msg" != *"serial0)" ]]; then
|
|
||||||
echo "$msg"
|
|
||||||
fi
|
|
||||||
|
|
||||||
dev="${msg#*/dev/p}"
|
|
||||||
dev="/dev/p${dev%% *}"
|
|
||||||
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -c "$dev" ]; then
|
|
||||||
dev=$(echo 'info chardev' | nc -q 1 -w 1 localhost "$QEMU_PORT" | tr -d '\000')
|
|
||||||
dev="${dev#*serial0}"
|
|
||||||
dev="${dev#*pty:}"
|
|
||||||
dev="${dev%%$'\n'*}"
|
|
||||||
dev="${dev%%$'\r'*}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -c "$dev" ]; then
|
|
||||||
error "Device '$dev' not found!"
|
|
||||||
finish 34 && return 34
|
|
||||||
fi
|
|
||||||
|
|
||||||
QEMU_TERM="$dev"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
_graceful_shutdown() {
|
|
||||||
|
|
||||||
local code=$?
|
|
||||||
local pid url response
|
|
||||||
|
|
||||||
set +e
|
|
||||||
|
|
||||||
if [ -f "$QEMU_END" ]; then
|
|
||||||
echo && info "Received $1 signal while already shutting down..."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
touch "$QEMU_END"
|
|
||||||
echo && info "Received $1 signal, sending shutdown command..."
|
|
||||||
|
|
||||||
if [ ! -s "$QEMU_PID" ]; then
|
|
||||||
echo && error "QEMU PID file does not exist?"
|
|
||||||
finish "$code" && return "$code"
|
|
||||||
fi
|
|
||||||
|
|
||||||
pid=$(<"$QEMU_PID")
|
|
||||||
|
|
||||||
if ! isAlive "$pid"; then
|
|
||||||
echo && error "QEMU process does not exist?"
|
|
||||||
finish "$code" && return "$code"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
# 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
|
||||||
# echo 'system_powerdown' | nc -q 1 -w 1 localhost "${QEMU_PORT}" > /dev/null
|
# 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
|
||||||
url="http://$API_HOST/read?command=$API_CMD&timeout=$API_TIMEOUT"
|
API_TIMEOUT=$(strip "$API_TIMEOUT")
|
||||||
response=$(curl -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1)
|
local url="http://localhost/read?command=$API_CMD&timeout=$API_TIMEOUT"
|
||||||
|
response=$(curl --unix-socket "$HOST_API_SOCKET" -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1)
|
||||||
|
|
||||||
if [[ "$response" =~ "\"success\"" ]]; then
|
if [[ "$response" =~ "\"success\"" ]]; then
|
||||||
|
|
||||||
@@ -141,45 +260,148 @@ _graceful_shutdown() {
|
|||||||
|
|
||||||
response="${response#*message\"\: \"}"
|
response="${response#*message\"\: \"}"
|
||||||
[ -z "$response" ] && response="second signal"
|
[ -z "$response" ] && response="second signal"
|
||||||
|
|
||||||
echo && error "Forcefully terminating because of: ${response%%\"*}"
|
echo && error "Forcefully terminating because of: ${response%%\"*}"
|
||||||
{ kill -15 "$pid" || true; } 2>/dev/null
|
kill -15 -- "$pid" 2>/dev/null || :
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeTimeout() {
|
||||||
|
|
||||||
|
# Divide the remaining timeout into guest wait, SIGTERM grace, and final
|
||||||
|
# cleanup instead of allowing the API call to consume the entire budget.
|
||||||
|
local term_grace=3 # seconds before loop ends to send SIGTERM
|
||||||
|
local cleanup_grace=3 # seconds reserved after the loop for cleanup
|
||||||
|
|
||||||
|
TIMEOUT=$(strip "$TIMEOUT")
|
||||||
|
if [[ ! "$TIMEOUT" =~ ^[0-9]+$ ]]; then
|
||||||
|
TIMEOUT=105
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( TIMEOUT >= 30 )); then
|
||||||
|
term_grace=5
|
||||||
|
cleanup_grace=5
|
||||||
|
elif (( TIMEOUT >= 15 )); then
|
||||||
|
term_grace=4
|
||||||
|
cleanup_grace=4
|
||||||
|
fi
|
||||||
|
|
||||||
|
local elapsed=$((SECONDS - start))
|
||||||
|
local timeout_left=$((TIMEOUT - elapsed))
|
||||||
|
|
||||||
|
local min=$((term_grace + cleanup_grace + 1))
|
||||||
|
(( timeout_left < min )) && timeout_left=$min
|
||||||
|
|
||||||
|
wait_until=$((timeout_left - cleanup_grace))
|
||||||
|
sigterm_at=$((wait_until - term_grace))
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForShutdown() {
|
||||||
|
|
||||||
local cnt=0
|
local cnt=0
|
||||||
|
local pid="$1"
|
||||||
|
local name="$APP"
|
||||||
|
|
||||||
while [ "$cnt" -lt "$QEMU_TIMEOUT" ]; do
|
while (( cnt <= wait_until && SHUTDOWN_SKIP == 0 )); do
|
||||||
|
|
||||||
! isAlive "$pid" && break
|
sleep 1 &
|
||||||
|
local slp=$!
|
||||||
|
|
||||||
sleep 1
|
# Stop waiting if the process has exited
|
||||||
cnt=$((cnt+1))
|
isAlive "$pid" || break
|
||||||
|
|
||||||
[[ "$DEBUG" == [Yy1]* ]] && info "Shutting down, waiting... ($cnt/$QEMU_TIMEOUT)"
|
# The process state is authoritative, but disappearance of both pidfiles
|
||||||
|
# also ends the wait when a wrapper exits before process reaping completes.
|
||||||
|
# Workaround for stale/zombie QEMU pid file
|
||||||
|
[ ! -s "$QEMU_START_PID" ] && [ ! -s "$QEMU_PID" ] && break
|
||||||
|
|
||||||
# Workaround for zombie pid
|
if (( cnt == sigterm_at )); then
|
||||||
[ ! -s "$QEMU_PID" ] && break
|
info "${name^} is still running, sending SIGTERM... ($cnt/$wait_until)"
|
||||||
|
kill -15 -- "$pid" 2>/dev/null || :
|
||||||
|
elif (( cnt > 0 )) && enabled "${DEBUG:-}"; then
|
||||||
|
info "Waiting for $name to shut down... ($cnt/$wait_until)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
wait "$slp" || :
|
||||||
|
(( cnt++ ))
|
||||||
|
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ "$cnt" -ge "$QEMU_TIMEOUT" ]; then
|
return 0
|
||||||
echo && error "Shutdown timeout reached, aborting..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
finish "$code" && return "$code"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MON_OPTS="\
|
gracefulShutdown() {
|
||||||
-pidfile $QEMU_PID \
|
|
||||||
-name $PROCESS,process=$PROCESS,debug-threads=on \
|
|
||||||
-monitor telnet:localhost:$QEMU_PORT,server,nowait,nodelay"
|
|
||||||
|
|
||||||
if [[ "$CONSOLE" != [Yy]* ]]; then
|
local sig="$1"
|
||||||
|
local pid code
|
||||||
|
|
||||||
MON_OPTS+=" -daemonize -D $QEMU_LOG"
|
[[ $BASHPID != "$TRAP_PID" ]] && return
|
||||||
|
|
||||||
_trap _graceful_shutdown SIGTERM SIGHUP SIGINT SIGABRT SIGQUIT
|
code=$(signalCode "$sig")
|
||||||
|
|
||||||
fi
|
if (( SHUTDOWN_SIGNAL != 0 )); then
|
||||||
|
|
||||||
|
# A second Ctrl-C is the explicit user request to skip the remaining
|
||||||
|
# graceful-shutdown wait and proceed to forced cleanup.
|
||||||
|
if (( code == 130 && SHUTDOWN_SIGNAL == code )); then
|
||||||
|
SHUTDOWN_SKIP=1
|
||||||
|
echo && info "Received SIGINT again, forcing shutdown..."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo && info "Received $sig signal while already shutting down..."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
start=$SECONDS
|
||||||
|
SHUTDOWN_SIGNAL=$code
|
||||||
|
|
||||||
|
# Shutdown handlers must continue through missing processes and failed cleanup
|
||||||
|
# commands instead of being aborted by errexit.
|
||||||
|
set +e
|
||||||
|
touch "$QEMU_END"
|
||||||
|
|
||||||
|
echo && info "Received $sig signal, sending shutdown command..."
|
||||||
|
|
||||||
|
if ! readQemuPid pid; then
|
||||||
|
if ! interactive || ! waitQemuPid pid; then
|
||||||
|
warn "QEMU PID file does not exist?"
|
||||||
|
finish "$code"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$pid" ] || ! isAlive "$pid"; then
|
||||||
|
warn "QEMU process with PID $pid does not exist?"
|
||||||
|
finish "$code"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sendGuestShutdown "$pid"
|
||||||
|
normalizeTimeout
|
||||||
|
waitForShutdown "$pid"
|
||||||
|
|
||||||
|
finish "$code"
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
+173
-55
@@ -1,108 +1,226 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# shellcheck disable=SC2329
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
: "${DHCP:="N"}"
|
: "${DHCP:="N"}"
|
||||||
: "${NETWORK:="Y"}"
|
: "${NETWORK:="Y"}"
|
||||||
|
|
||||||
[[ "$NETWORK" == [Nn]* ]] && exit 0
|
cd /run
|
||||||
|
. 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
|
||||||
|
|
||||||
file="/run/shm/dsm.url"
|
file="/run/shm/dsm.url"
|
||||||
info="/run/shm/msg.html"
|
msgs="/run/shm/msg.html"
|
||||||
|
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"
|
||||||
jq_err="Failed to parse response from guest: jq error"
|
jq_err="Failed to parse response from guest: jq error"
|
||||||
|
|
||||||
while [ ! -s "$file" ]
|
exitIfShuttingDown() {
|
||||||
do
|
|
||||||
|
|
||||||
# Check if not shutting down
|
|
||||||
[ -f "$shutdown" ] && exit 1
|
|
||||||
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
[ -f "$shutdown" ] && exit 1
|
[ -f "$shutdown" ] && exit 1
|
||||||
[ -s "$file" ] && break
|
|
||||||
|
|
||||||
# Retrieve network info from guest VM
|
return 0
|
||||||
{ json=$(curl -m 20 -sk "$url"); rc=$?; } || :
|
}
|
||||||
|
|
||||||
[ -f "$shutdown" ] && exit 1
|
queryGuest() {
|
||||||
(( rc != 0 )) && error "$curl_err $rc" && continue
|
|
||||||
|
|
||||||
{ result=$(echo "$json" | jq -r '.status'); rc=$?; } || :
|
# Query DSM through the qemu-host sidecar rather than the guest network,
|
||||||
(( rc != 0 )) && error "$jq_err $rc ( $json )" && continue
|
# which may not be configured yet.
|
||||||
[[ "$result" == "null" ]] && error "$resp_err $json" && continue
|
{ json=$(curl --unix-socket "$socket" -m 20 -sk "$url"); local rc=$?; } || :
|
||||||
|
|
||||||
if [[ "$result" != "success" ]] ; then
|
exitIfShuttingDown
|
||||||
{ msg=$(echo "$json" | jq -r '.message'); rc=$?; } || :
|
|
||||||
error "Guest replied $result: $msg" && continue
|
if (( rc != 0 )); then
|
||||||
|
error "$curl_err $rc"
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
{ port=$(echo "$json" | jq -r '.data.data.dsm_setting.data.http_port'); rc=$?; } || :
|
return 0
|
||||||
(( rc != 0 )) && error "$jq_err $rc ( $json )" && continue
|
}
|
||||||
[[ "$port" == "null" ]] && error "$resp_err $json" && continue
|
|
||||||
[ -z "$port" ] && continue
|
|
||||||
|
|
||||||
{ ip=$(echo "$json" | jq -r '.data.data.ip.data[] | select((.name=="eth0") and has("ip")).ip'); rc=$?; } || :
|
readJsonField() {
|
||||||
(( rc != 0 )) && error "$jq_err $rc ( $json )" && continue
|
|
||||||
[[ "$ip" == "null" ]] && error "$resp_err $json" && continue
|
|
||||||
|
|
||||||
if [ -z "$ip" ]; then
|
local query="$1"
|
||||||
[[ "$DHCP" == [Yy1]* ]] && continue
|
local result
|
||||||
ip="20.20.20.21"
|
|
||||||
|
{ result=$(jq -r "$query" <<< "$json"); local rc=$?; } || :
|
||||||
|
|
||||||
|
if (( rc != 0 )); then
|
||||||
|
error "$jq_err $rc ( $json )"
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$ip:$port" > $file
|
if [[ "$result" == "null" ]]; then
|
||||||
|
error "$resp_err $json"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
done
|
printf '%s\n' "$result"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
[ -f "$shutdown" ] && exit 1
|
readGuestStatus() {
|
||||||
|
|
||||||
location=$(<"$file")
|
local result msg
|
||||||
|
|
||||||
if [[ "$location" != "20.20"* ]]; then
|
result=$(readJsonField '.status') || return 1
|
||||||
|
|
||||||
|
if [[ "$result" != "success" ]]; then
|
||||||
|
{ msg=$(jq -r '.message // empty' <<< "$json"); local rc=$?; } || :
|
||||||
|
|
||||||
|
if (( rc != 0 )); then
|
||||||
|
error "$jq_err $rc ( $json )"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
error "Guest replied $result: $msg"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
readGuestPort() {
|
||||||
|
|
||||||
|
port=$(readJsonField '.data.data.dsm_setting.data.http_port') || return 1
|
||||||
|
[ -z "$port" ] && return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
readGuestIp() {
|
||||||
|
|
||||||
|
ip=$(readJsonField '.data.data.ip.data[] | select(.name=="eth0" and has("ip")) | .ip | select(test("^[0-9]+\\."))') || return 1
|
||||||
|
[ -z "$ip" ] && return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
writeDsmLocation() {
|
||||||
|
|
||||||
|
echo "$ip:$port" > "$file"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pollGuestLocation() {
|
||||||
|
|
||||||
|
# Keep polling until the guest reports a usable address, but stop promptly
|
||||||
|
# when container shutdown begins.
|
||||||
|
while [ ! -s "$file" ]; do
|
||||||
|
|
||||||
|
# Check if not shutting down
|
||||||
|
exitIfShuttingDown
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
exitIfShuttingDown
|
||||||
|
[ -s "$file" ] && break
|
||||||
|
|
||||||
|
# Retrieve network info from guest VM
|
||||||
|
queryGuest || continue
|
||||||
|
readGuestStatus || continue
|
||||||
|
readGuestPort || continue
|
||||||
|
readGuestIp || continue
|
||||||
|
writeDsmLocation
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAddressConflict() {
|
||||||
|
|
||||||
|
local guest_ip="${location%:*}"
|
||||||
|
local container_ip=""
|
||||||
|
|
||||||
|
[ -s "$address" ] && container_ip=$(<"$address")
|
||||||
|
[ -z "$container_ip" ] && return 0
|
||||||
|
[[ "$guest_ip" != "$container_ip" ]] && return 0
|
||||||
|
|
||||||
|
warn "DSM is using the same IP as the container, this will cause connectivity issues."
|
||||||
|
warn "change the container's macvlan IP or assign DSM a different address in your router."
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
writeDhcpPage() {
|
||||||
|
|
||||||
|
local 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}"
|
||||||
HTML="${HTML/\[2\]/$script}"
|
html="${html/\[2\]/$script}"
|
||||||
HTML="${HTML/\[3\]/$body}"
|
html="${html/\[3\]/$body}"
|
||||||
HTML="${HTML/\[4\]/}"
|
html="${html/\[4\]/}"
|
||||||
HTML="${HTML/\[5\]/}"
|
html="${html/\[5\]/}"
|
||||||
|
|
||||||
echo "$HTML" > "$page"
|
echo "$html" > "$page"
|
||||||
echo "$body" > "$info"
|
echo "$body" > "$msgs"
|
||||||
|
|
||||||
else
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
buildStaticMessage() {
|
||||||
|
|
||||||
|
local nic ip
|
||||||
|
|
||||||
|
nic=$(<"$driver")
|
||||||
ip=$(<"$address")
|
ip=$(<"$address")
|
||||||
port="${location##*:}"
|
local port="${location##*:}"
|
||||||
|
|
||||||
if [[ "$ip" == "172."* ]]; then
|
# NAT and user-mode networking are reached through a forwarded host port;
|
||||||
|
# macvlan exposes DSM directly on the container-facing LAN address.
|
||||||
|
if [[ "${nic,,}" != "macvlan" ]]; then
|
||||||
msg="port $port"
|
msg="port $port"
|
||||||
else
|
else
|
||||||
msg="http://$ip:$port"
|
msg="http://$ip:$port"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
printLoginMessage() {
|
||||||
|
|
||||||
|
echo "" >&2
|
||||||
|
info "-----------------------------------------------------------"
|
||||||
|
info " You can now login to DSM at $msg"
|
||||||
|
info "-----------------------------------------------------------"
|
||||||
|
echo "" >&2
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pollGuestLocation
|
||||||
|
exitIfShuttingDown
|
||||||
|
|
||||||
|
location=$(<"$file")
|
||||||
|
|
||||||
|
if enabled "$DHCP"; then
|
||||||
|
checkAddressConflict
|
||||||
|
writeDhcpPage
|
||||||
|
else
|
||||||
|
buildStaticMessage
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "" >&2
|
printLoginMessage
|
||||||
info "-----------------------------------------------------------"
|
|
||||||
info " You can now login to DSM at $msg"
|
|
||||||
info "-----------------------------------------------------------"
|
|
||||||
echo "" >&2
|
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
+147
-82
@@ -3,85 +3,112 @@ set -Eeuo pipefail
|
|||||||
|
|
||||||
# Docker environment variables
|
# Docker environment variables
|
||||||
|
|
||||||
: "${KVM:="Y"}"
|
|
||||||
: "${HOST_CPU:=""}"
|
: "${HOST_CPU:=""}"
|
||||||
: "${CPU_FLAGS:=""}"
|
: "${CPU_FLAGS:=""}"
|
||||||
: "${CPU_MODEL:=""}"
|
: "${CPU_MODEL:=""}"
|
||||||
: "${DEF_MODEL:="qemu64"}"
|
|
||||||
|
|
||||||
if [[ "${ARCH,,}" != "amd64" ]]; then
|
HOST_CPU=$(strip "$HOST_CPU")
|
||||||
KVM="N"
|
CPU_FLAGS=$(strip "$CPU_FLAGS")
|
||||||
warn "your CPU architecture is ${ARCH^^} and cannot provide KVM acceleration for x64 instructions, this will cause a major loss of performance."
|
CPU_MODEL=$(strip "$CPU_MODEL")
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$KVM" != [Nn]* ]]; then
|
selectClocksource() {
|
||||||
|
|
||||||
KVM_ERR=""
|
CLOCKSOURCE="tsc"
|
||||||
|
[[ "${ARCH,,}" == "arm64" ]] && CLOCKSOURCE="arch_sys_counter"
|
||||||
|
CLOCK="/sys/devices/system/clocksource/clocksource0/current_clocksource"
|
||||||
|
|
||||||
if [ ! -e /dev/kvm ]; then
|
return 0
|
||||||
KVM_ERR="(device file missing)"
|
}
|
||||||
else
|
|
||||||
if ! sh -c 'echo -n > /dev/kvm' &> /dev/null; then
|
checkClocksource() {
|
||||||
KVM_ERR="(no write access)"
|
|
||||||
else
|
local result
|
||||||
flags=$(sed -ne '/^flags/s/^.*: //p' /proc/cpuinfo)
|
|
||||||
if ! grep -qw "vmx\|svm" <<< "$flags"; then
|
if [ ! -f "$CLOCK" ]; then
|
||||||
KVM_ERR="(vmx/svm disabled)"
|
warn "file \"$CLOCK\" cannot be found?"
|
||||||
fi
|
return 0
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$KVM_ERR" ]; then
|
result=$(<"$CLOCK")
|
||||||
KVM="N"
|
result="${result//[![:print:]]/}"
|
||||||
if [[ "$OSTYPE" =~ ^darwin ]]; then
|
|
||||||
warn "you are using macOS which has no KVM support, this will cause a major loss of performance."
|
case "${result,,}" in
|
||||||
else
|
"${CLOCKSOURCE,,}" ) ;;
|
||||||
error "KVM acceleration not available $KVM_ERR, this will cause a major loss of performance."
|
"kvm-clock" ) info "Nested KVM virtualization detected.." ;;
|
||||||
error "See the FAQ on how to diagnose the cause, or continue without KVM by setting KVM=N (not recommended)."
|
"hyperv_clocksource_tsc_page" ) info "Nested Hyper-V virtualization detected.." ;;
|
||||||
[[ "$DEBUG" != [Yy1]* ]] && exit 88
|
"hpet" ) warn "unsupported clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;;
|
||||||
fi
|
*) warn "unexpected clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
checkSse42() {
|
||||||
|
|
||||||
|
if ! hasFlag "sse4_2"; then
|
||||||
|
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
|
||||||
|
enabled "$DEBUG" || exit 88
|
||||||
fi
|
fi
|
||||||
|
|
||||||
fi
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
if [[ "$KVM" != [Nn]* ]]; then
|
trimSpaces() {
|
||||||
|
|
||||||
|
local value="$1"
|
||||||
|
|
||||||
|
value="${value#"${value%%[![:space:]]*}"}"
|
||||||
|
value="${value%"${value##*[![:space:]]}"}"
|
||||||
|
|
||||||
|
echo "$value"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCpuArgument() {
|
||||||
|
|
||||||
|
# CPU configuration has dedicated variables. Remove raw -cpu arguments so
|
||||||
|
# option ordering cannot silently override the validated model and flags.
|
||||||
|
local args=" ${ARGUMENTS:-} "
|
||||||
|
|
||||||
|
while [[ "$args" =~ [[:space:]]-cpu([[:space:]][^[:space:]]+|=[^[:space:]]+)? ]]; do
|
||||||
|
local cpu="${BASH_REMATCH[0]}"
|
||||||
|
args="${args/$cpu/ }"
|
||||||
|
warn "Ignoring '${cpu#" "}' from ARGUMENTS, use CPU_MODEL and CPU_FLAGS instead."
|
||||||
|
done
|
||||||
|
|
||||||
|
ARGUMENTS=$(trimSpaces "$args")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
configureKvmCpuModel() {
|
||||||
|
|
||||||
CPU_FEATURES="kvm=on,l3-cache=on,+hypervisor"
|
CPU_FEATURES="kvm=on,l3-cache=on,+hypervisor"
|
||||||
CLOCK="/sys/devices/system/clocksource/clocksource0/current_clocksource"
|
|
||||||
KVM_OPTS=",accel=kvm -enable-kvm -global kvm-pit.lost_tick_policy=discard"
|
KVM_OPTS=",accel=kvm -enable-kvm -global kvm-pit.lost_tick_policy=discard"
|
||||||
|
|
||||||
if ! grep -qw "sse4_2" <<< "$flags"; then
|
|
||||||
info "Your CPU does not have the SSE4 instruction set that Virtual DSM requires, it will be emulated..."
|
|
||||||
[ -z "$CPU_MODEL" ] && CPU_MODEL="$DEF_MODEL"
|
|
||||||
CPU_FEATURES+=",+ssse3,+sse4.1,+sse4.2"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$CPU_MODEL" ]; then
|
if [ -z "$CPU_MODEL" ]; then
|
||||||
CPU_MODEL="host"
|
CPU_MODEL="host"
|
||||||
CPU_FEATURES+=",migratable=no"
|
CPU_FEATURES+=",migratable=no"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "$CLOCK" ]; then
|
return 0
|
||||||
CLOCK=$(<"$CLOCK")
|
}
|
||||||
if [[ "${CLOCK,,}" != "tsc" ]]; then
|
|
||||||
warn "unexpected clocksource: $CLOCK"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
warn "file \"$CLOCK\" cannot not found?"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if grep -qw "svm" <<< "$flags"; then
|
appendKvmInvtscFeature() {
|
||||||
|
|
||||||
|
# invtsc is safe only when the active accelerator can scale the host TSC;
|
||||||
|
# AMD and Intel expose that capability through different host flags.
|
||||||
|
if hasFlag "svm"; then
|
||||||
|
|
||||||
# AMD processor
|
# AMD processor
|
||||||
|
if hasFlag "tsc_scale"; then
|
||||||
if grep -qw "tsc_scale" <<< "$flags"; then
|
|
||||||
CPU_FEATURES+=",+invtsc"
|
CPU_FEATURES+=",+invtsc"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|
||||||
# Intel processor
|
# Intel processor
|
||||||
|
local vmx
|
||||||
vmx=$(sed -ne '/^vmx flags/s/^.*: //p' /proc/cpuinfo)
|
vmx=$(sed -ne '/^vmx flags/s/^.*: //p' /proc/cpuinfo)
|
||||||
|
|
||||||
if grep -qw "tsc_scaling" <<< "$vmx"; then
|
if grep -qw "tsc_scaling" <<< "$vmx"; then
|
||||||
@@ -90,7 +117,37 @@ if [[ "$KVM" != [Nn]* ]]; then
|
|||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
else
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
configureKvm() {
|
||||||
|
|
||||||
|
configureKvmCpuModel
|
||||||
|
checkSse42
|
||||||
|
appendKvmInvtscFeature
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
configureTcgCpuModel() {
|
||||||
|
|
||||||
|
if [ -n "$CPU_MODEL" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# TCG uses the broad max model on native x86, but qemu64 is the compatible
|
||||||
|
# cross-architecture fallback.
|
||||||
|
if [[ "$ARCH" == "amd64" ]]; then
|
||||||
|
CPU_MODEL="max"
|
||||||
|
CPU_FEATURES+=",migratable=no"
|
||||||
|
else
|
||||||
|
CPU_MODEL="qemu64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
configureTcg() {
|
||||||
|
|
||||||
KVM_OPTS=""
|
KVM_OPTS=""
|
||||||
CPU_FEATURES="l3-cache=on,+hypervisor"
|
CPU_FEATURES="l3-cache=on,+hypervisor"
|
||||||
@@ -99,46 +156,54 @@ else
|
|||||||
KVM_OPTS=" -accel tcg,thread=multi"
|
KVM_OPTS=" -accel tcg,thread=multi"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$CPU_MODEL" ]; then
|
configureTcgCpuModel
|
||||||
if [[ "$ARCH" == "amd64" ]]; then
|
CPU_FEATURES+=",+ssse3,+sse4.1,+sse4.2"
|
||||||
CPU_MODEL="max"
|
|
||||||
CPU_FEATURES+=",migratable=no"
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
composeCpuFlags() {
|
||||||
|
|
||||||
|
# Compose one -cpu value in precedence order: model, required features,
|
||||||
|
# then user-provided overrides.
|
||||||
|
CPU_FLAGS="${CPU_MODEL}${CPU_FEATURES:+,$CPU_FEATURES}${CPU_FLAGS:+,$CPU_FLAGS}"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
configureHostCpuName() {
|
||||||
|
|
||||||
|
if [ -z "$HOST_CPU" ]; then
|
||||||
|
[[ "${CPU,,}" != "unknown" ]] && HOST_CPU="$CPU"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$HOST_CPU" ]; then
|
||||||
|
# qemu-host expects a comma-separated CPU description with empty family
|
||||||
|
# and suffix fields, not QEMU's -cpu syntax.
|
||||||
|
HOST_CPU="${HOST_CPU%%,*},,"
|
||||||
|
else
|
||||||
|
HOST_CPU="QEMU, Virtual CPU,"
|
||||||
|
if [ "$ARCH" == "amd64" ]; then
|
||||||
|
HOST_CPU+=" X86_64"
|
||||||
else
|
else
|
||||||
CPU_MODEL="$DEF_MODEL"
|
HOST_CPU+=" $ARCH"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
CPU_FEATURES+=",+ssse3,+sse4.1,+sse4.2"
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
fi
|
selectClocksource
|
||||||
|
checkClocksource
|
||||||
|
|
||||||
if [ -z "$CPU_FLAGS" ]; then
|
if ! disabled "$KVM"; then
|
||||||
if [ -z "$CPU_FEATURES" ]; then
|
configureKvm
|
||||||
CPU_FLAGS="$CPU_MODEL"
|
|
||||||
else
|
|
||||||
CPU_FLAGS="$CPU_MODEL,$CPU_FEATURES"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
if [ -z "$CPU_FEATURES" ]; then
|
configureTcg
|
||||||
CPU_FLAGS="$CPU_MODEL,$CPU_FLAGS"
|
|
||||||
else
|
|
||||||
CPU_FLAGS="$CPU_MODEL,$CPU_FEATURES,$CPU_FLAGS"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$HOST_CPU" ]; then
|
removeCpuArgument
|
||||||
HOST_CPU=$(lscpu | grep -m 1 'Model name' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
|
composeCpuFlags
|
||||||
fi
|
configureHostCpuName
|
||||||
|
|
||||||
if [ -n "$HOST_CPU" ]; then
|
|
||||||
HOST_CPU="${HOST_CPU%%,*},,"
|
|
||||||
else
|
|
||||||
HOST_CPU="QEMU, Virtual CPU,"
|
|
||||||
if [ "$ARCH" == "amd64" ]; then
|
|
||||||
HOST_CPU+=" X86_64"
|
|
||||||
else
|
|
||||||
HOST_CPU+=" $ARCH"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
+297
-25
@@ -1,38 +1,310 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
escape () {
|
info="/run/shm/msg.html"
|
||||||
local s
|
info_tmp="${info}.${BASHPID}.tmp"
|
||||||
s=${1//&/\&}
|
|
||||||
s=${s//</\<}
|
escape() {
|
||||||
s=${s//>/\>}
|
|
||||||
s=${s//'"'/\"}
|
local s
|
||||||
printf -- %s "$s"
|
|
||||||
return 0
|
s=${1//&/\&}
|
||||||
|
s=${s//</\<}
|
||||||
|
s=${s//>/\>}
|
||||||
|
s=${s//'"'/\"}
|
||||||
|
s=${s//"'"/\'}
|
||||||
|
|
||||||
|
printf '%s' "$s"
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
file="$1"
|
writeInfo() {
|
||||||
|
|
||||||
|
local content="$1"
|
||||||
|
|
||||||
|
# Replace the web status atomically so websocket readers never observe a
|
||||||
|
# partially written HTML fragment.
|
||||||
|
if ! printf '%s\n' "$content" > "$info_tmp"; then
|
||||||
|
rm -f -- "$info_tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! mv -f -- "$info_tmp" "$info"; then
|
||||||
|
rm -f -- "$info_tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
getBytes() {
|
||||||
|
|
||||||
|
local path="$1"
|
||||||
|
local mode="$2"
|
||||||
|
local bytes="0"
|
||||||
|
|
||||||
|
if [[ "$mode" == "counter" ]]; then
|
||||||
|
if [ -r "$path" ]; then
|
||||||
|
read -r bytes < "$path" || bytes="0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$bytes" =~ ^[0-9]+$ ]] || bytes="0"
|
||||||
|
printf '%s\n' "$bytes"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -s "$path" ] && [ ! -d "$path" ]; then
|
||||||
|
printf '0\n'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$mode" == "allocated" ]]; then
|
||||||
|
bytes=$(du -sB1 -- "$path" 2>/dev/null | cut -f1) || bytes="0"
|
||||||
|
else
|
||||||
|
bytes=$(du -sb -- "$path" 2>/dev/null | cut -f1) || bytes="0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$bytes"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus() {
|
||||||
|
|
||||||
|
local file="$1"
|
||||||
|
local bytes total extra=""
|
||||||
|
|
||||||
|
[ -r "$file" ] || return 1
|
||||||
|
read -r bytes total extra < "$file" || return 1
|
||||||
|
|
||||||
|
if [[ ! "$bytes" =~ ^[0-9]+$ ||
|
||||||
|
! "$total" =~ ^[0-9]+$ ||
|
||||||
|
-n "$extra" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s %s\n' "$bytes" "$total"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
formatSize() {
|
||||||
|
|
||||||
|
local bytes="$1"
|
||||||
|
local size
|
||||||
|
|
||||||
|
size=$(numfmt --to=iec --suffix=B "$bytes" |
|
||||||
|
sed -r 's/([A-Z])/ \1/') ||
|
||||||
|
size="${bytes} bytes"
|
||||||
|
|
||||||
|
printf '%s' "$size"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
printPercentProgress() {
|
||||||
|
|
||||||
|
local percent="$1"
|
||||||
|
|
||||||
|
while (( next_percent <= percent && next_percent <= 100 )); do
|
||||||
|
if [[ "$printed" == "Y" ]]; then
|
||||||
|
printf ' → %s%%' "$next_percent"
|
||||||
|
else
|
||||||
|
printf '%s%%' "$next_percent"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printed="Y"
|
||||||
|
next_percent=$((next_percent + 10))
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
printCurrentSize() {
|
||||||
|
|
||||||
|
local bytes="$1"
|
||||||
|
local size
|
||||||
|
|
||||||
|
size=$(formatSize "$bytes")
|
||||||
|
|
||||||
|
if [[ "$printed" == "Y" ]]; then
|
||||||
|
printf ' → %s' "$size"
|
||||||
|
else
|
||||||
|
printf '%s' "$size"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printed="Y"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
printSizeProgress() {
|
||||||
|
|
||||||
|
local bytes="$1"
|
||||||
|
local size
|
||||||
|
|
||||||
|
while (( bytes >= next_bytes )); do
|
||||||
|
size=$(formatSize "$next_bytes")
|
||||||
|
|
||||||
|
if [[ "$printed" == "Y" ]]; then
|
||||||
|
printf ' → %s' "$size"
|
||||||
|
else
|
||||||
|
printf '%s' "$size"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printed="Y"
|
||||||
|
next_bytes=$((next_bytes + step_bytes))
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
stopProgress() {
|
||||||
|
|
||||||
|
if [ -z "$status_file" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
stopping="Y"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
finishProgress() {
|
||||||
|
|
||||||
|
rm -f -- "$info_tmp"
|
||||||
|
|
||||||
|
if [[ "$output" == "log" && "$printed" == "Y" ]]; then
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
path="$1"
|
||||||
total="$2"
|
total="$2"
|
||||||
body=$(escape "$3")
|
body=$(escape "$3")
|
||||||
info="/run/shm/msg.html"
|
output="${4:-}"
|
||||||
|
step_bytes="${5:-536870912}"
|
||||||
|
mode="${6:-apparent}"
|
||||||
|
status_file="${7:-}"
|
||||||
|
|
||||||
if [[ "$body" == *"..." ]]; then
|
if [[ -n "$total" && ! "$total" =~ ^(0|[1-9][0-9]*)$ ]]; then
|
||||||
body="<p class=\"loading\">${body/.../}</p>"
|
printf 'Invalid total size: %s\n' "$total" >&2
|
||||||
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
while true
|
if [[ ! "$step_bytes" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
do
|
printf 'Invalid progress interval: %s\n' "$step_bytes" >&2
|
||||||
if [ -s "$file" ]; then
|
exit 2
|
||||||
bytes=$(du -sb "$file" | cut -f1)
|
fi
|
||||||
if (( bytes > 1000 )); then
|
|
||||||
if [ -z "$total" ] || [[ "$total" == "0" ]]; then
|
case "$mode" in
|
||||||
size=$(numfmt --to=iec --suffix=B "$bytes" | sed -r 's/([A-Z])/ \1/')
|
apparent | allocated | counter ) ;;
|
||||||
else
|
* )
|
||||||
size="$(echo "$bytes" "$total" | awk '{printf "%.1f", $1 * 100 / $2}')"
|
printf 'Invalid progress mode: %s\n' "$mode" >&2
|
||||||
size="$size%"
|
exit 2
|
||||||
fi
|
;;
|
||||||
echo "${body//(\[P\])/($size)}"> "$info"
|
esac
|
||||||
|
|
||||||
|
case "$output" in
|
||||||
|
"" | log ) ;;
|
||||||
|
* )
|
||||||
|
printf 'Invalid progress output: %s\n' "$output" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
printed="N"
|
||||||
|
next_percent=10
|
||||||
|
next_bytes="$step_bytes"
|
||||||
|
log_mode="percent"
|
||||||
|
stopping="N"
|
||||||
|
|
||||||
|
if [ -z "$total" ] || [[ "$total" == "0" ]]; then
|
||||||
|
log_mode="size"
|
||||||
|
fi
|
||||||
|
|
||||||
|
trap finishProgress EXIT
|
||||||
|
trap 'exit 0' HUP INT QUIT
|
||||||
|
# SIGTERM requests one final measurement and web update rather than
|
||||||
|
# terminating between progress samples.
|
||||||
|
trap stopProgress TERM
|
||||||
|
|
||||||
|
if [[ "$body" == *"..." ]]; then
|
||||||
|
body="<p class=\"loading\">${body::-3}</p>"
|
||||||
|
fi
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
|
||||||
|
final_pass="${stopping:-}"
|
||||||
|
bytes=$(getBytes "$path" "$mode")
|
||||||
|
effective_total="$total"
|
||||||
|
|
||||||
|
# An external downloader may provide authoritative completed and total byte
|
||||||
|
# counters; use them instead of filesystem size when available.
|
||||||
|
if [ -n "$status_file" ] && status=$(getStatus "$status_file"); then
|
||||||
|
read -r status_bytes status_total <<< "$status"
|
||||||
|
bytes="$status_bytes"
|
||||||
|
|
||||||
|
if (( status_total > 0 )); then
|
||||||
|
effective_total="$status_total"
|
||||||
fi
|
fi
|
||||||
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
|
||||||
|
|||||||
-291
@@ -1,291 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -Eeuo pipefail
|
|
||||||
|
|
||||||
info () { printf "%b%s%b" "\E[1;34m❯ \E[1;36m" "${1:-}" "\E[0m\n"; }
|
|
||||||
error () { printf "%b%s%b" "\E[1;31m❯ " "ERROR: ${1:-}" "\E[0m\n" >&2; }
|
|
||||||
warn () { printf "%b%s%b" "\E[1;31m❯ " "Warning: ${1:-}" "\E[0m\n" >&2; }
|
|
||||||
|
|
||||||
trap 'error "Status $? while: $BASH_COMMAND (line $LINENO/$BASH_LINENO)"' ERR
|
|
||||||
|
|
||||||
[ ! -f "/run/entry.sh" ] && error "Script must run inside Docker container!" && exit 11
|
|
||||||
[ "$(id -u)" -ne "0" ] && error "Script must be executed with root privileges." && exit 12
|
|
||||||
|
|
||||||
echo "❯ Starting $APP for Docker v$(</run/version)..."
|
|
||||||
echo "❯ For support visit $SUPPORT"
|
|
||||||
|
|
||||||
# Docker environment variables
|
|
||||||
|
|
||||||
: "${TZ:=""}" # System local timezone
|
|
||||||
: "${DEBUG:="N"}" # Disable debugging mode
|
|
||||||
: "${COMMIT:="N"}" # Commit to local image
|
|
||||||
: "${COUNTRY:=""}" # Country code for mirror
|
|
||||||
: "${CONSOLE:="N"}" # Disable console mode
|
|
||||||
: "${ALLOCATE:=""}" # Preallocate diskspace
|
|
||||||
: "${ARGUMENTS:=""}" # Extra QEMU parameters
|
|
||||||
: "${CPU_CORES:="1"}" # Amount of CPU cores
|
|
||||||
: "${RAM_SIZE:="1G"}" # Maximum RAM amount
|
|
||||||
: "${RAM_CHECK:="Y"}" # Check available RAM
|
|
||||||
: "${DISK_SIZE:="16G"}" # Initial data disk size
|
|
||||||
: "${STORAGE:="/storage"}" # Storage folder location
|
|
||||||
|
|
||||||
# Helper variables
|
|
||||||
|
|
||||||
PROCESS="${APP,,}"
|
|
||||||
PROCESS="${PROCESS// /-}"
|
|
||||||
|
|
||||||
INFO="/run/shm/msg.html"
|
|
||||||
PAGE="/run/shm/index.html"
|
|
||||||
TEMPLATE="/var/www/index.html"
|
|
||||||
FOOTER1="$APP for Docker v$(</run/version)"
|
|
||||||
FOOTER2="<a href='$SUPPORT'>$SUPPORT</a>"
|
|
||||||
|
|
||||||
CPI=$(lscpu)
|
|
||||||
SYS=$(uname -r)
|
|
||||||
HOST=$(hostname -s)
|
|
||||||
KERNEL=$(echo "$SYS" | cut -b 1)
|
|
||||||
MINOR=$(echo "$SYS" | cut -d '.' -f2)
|
|
||||||
ARCH=$(dpkg --print-architecture)
|
|
||||||
CORES=$(grep -c '^processor' /proc/cpuinfo)
|
|
||||||
|
|
||||||
if ! grep -qi "socket(s)" <<< "$CPI"; then
|
|
||||||
SOCKETS=1
|
|
||||||
else
|
|
||||||
SOCKETS=$(echo "$CPI" | grep -m 1 -i 'socket(s)' | awk '{print $(2)}')
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! grep -qi "model name" <<< "$CPI"; then
|
|
||||||
CPU=""
|
|
||||||
else
|
|
||||||
CPU=$(echo "$CPI" | grep -m 1 -i 'model name' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${CPU// /}" ] && grep -qi "model:" <<< "$CPI"; then
|
|
||||||
CPU=$(echo "$CPI" | grep -m 1 -i 'model:' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
|
|
||||||
fi
|
|
||||||
|
|
||||||
CPU="${CPU// CPU/}"
|
|
||||||
CPU="${CPU// 16 Core/}"
|
|
||||||
CPU="${CPU// 32 Core/}"
|
|
||||||
CPU="${CPU// 64 Core/}"
|
|
||||||
CPU="${CPU// Processor/}"
|
|
||||||
CPU="${CPU// Quad core/}"
|
|
||||||
CPU="${CPU// Core TM/ Core}"
|
|
||||||
CPU="${CPU// with Radeon Graphics/}"
|
|
||||||
[ -z "${CPU// /}" ] && CPU="Unknown"
|
|
||||||
|
|
||||||
# Check system
|
|
||||||
|
|
||||||
if [ ! -d "/dev/shm" ]; then
|
|
||||||
error "Directory /dev/shm not found!" && exit 14
|
|
||||||
else
|
|
||||||
[ ! -d "/run/shm" ] && ln -s /dev/shm /run/shm
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check folder
|
|
||||||
|
|
||||||
if [[ "$COMMIT" != [Nn]* ]]; then
|
|
||||||
STORAGE="/local"
|
|
||||||
mkdir -p "$STORAGE"
|
|
||||||
else
|
|
||||||
if [ ! -d "$STORAGE" ]; then
|
|
||||||
error "Storage folder ($STORAGE) not found!" && exit 13
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check filesystem
|
|
||||||
FS=$(stat -f -c %T "$STORAGE")
|
|
||||||
|
|
||||||
if [[ "${FS,,}" == "ecryptfs" ]] || [[ "${FS,,}" == "tmpfs" ]]; then
|
|
||||||
DISK_IO="threads"
|
|
||||||
DISK_CACHE="writeback"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Read memory
|
|
||||||
RAM_SPARE=500000000
|
|
||||||
RAM_AVAIL=$(free -b | grep -m 1 Mem: | awk '{print $7}')
|
|
||||||
RAM_TOTAL=$(free -b | grep -m 1 Mem: | awk '{print $2}')
|
|
||||||
RAM_SIZE=$(echo "${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
|
|
||||||
RAM_WANTED=$(numfmt --from=iec "$RAM_SIZE")
|
|
||||||
AVAIL_GB=$(( RAM_AVAIL/1073741824 ))
|
|
||||||
TOTAL_GB=$(( (RAM_TOTAL + 1073741823)/1073741824 ))
|
|
||||||
WANTED_GB=$(( (RAM_WANTED + 1073741823)/1073741824 ))
|
|
||||||
|
|
||||||
# Print system info
|
|
||||||
SYS="${SYS/-generic/}"
|
|
||||||
FS="${FS/UNKNOWN //}"
|
|
||||||
FS="${FS/ext2\/ext3/ext4}"
|
|
||||||
FS=$(echo "$FS" | sed 's/[)(]//g')
|
|
||||||
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
|
|
||||||
SPACE_GB=$(( (SPACE + 1073741823)/1073741824 ))
|
|
||||||
|
|
||||||
echo "❯ CPU: ${CPU} | RAM: $AVAIL_GB/$TOTAL_GB GB | DISK: $SPACE_GB GB (${FS}) | KERNEL: ${SYS}..."
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Check memory
|
|
||||||
|
|
||||||
if [[ "$RAM_CHECK" != [Nn]* ]]; then
|
|
||||||
if (( (RAM_WANTED + RAM_SPARE) > RAM_AVAIL )); then
|
|
||||||
error "Your configured RAM_SIZE of $WANTED_GB GB is too high for the $AVAIL_GB GB of memory available, please set a lower value."
|
|
||||||
exit 17
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Cleanup files
|
|
||||||
rm -f /run/shm/qemu.*
|
|
||||||
rm -f /run/shm/dsm.url
|
|
||||||
|
|
||||||
# Cleanup dirs
|
|
||||||
rm -rf /tmp/dsm
|
|
||||||
rm -rf "$STORAGE/tmp"
|
|
||||||
|
|
||||||
# Helper functions
|
|
||||||
|
|
||||||
isAlive() {
|
|
||||||
local pid=$1
|
|
||||||
|
|
||||||
if kill -0 "$pid" 2>/dev/null; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
pKill() {
|
|
||||||
local pid=$1
|
|
||||||
|
|
||||||
{ kill -15 "$pid" || true; } 2>/dev/null
|
|
||||||
|
|
||||||
while isAlive "$pid"; do
|
|
||||||
sleep 0.2
|
|
||||||
done
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fWait() {
|
|
||||||
local name=$1
|
|
||||||
|
|
||||||
while pgrep -f -l "$name" >/dev/null; do
|
|
||||||
sleep 0.2
|
|
||||||
done
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fKill() {
|
|
||||||
local name=$1
|
|
||||||
|
|
||||||
{ pkill -f "$name" || true; } 2>/dev/null
|
|
||||||
fWait "$name"
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
escape () {
|
|
||||||
local s
|
|
||||||
s=${1//&/\&}
|
|
||||||
s=${s//</\<}
|
|
||||||
s=${s//>/\>}
|
|
||||||
s=${s//'"'/\"}
|
|
||||||
printf -- %s "$s"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
html()
|
|
||||||
{
|
|
||||||
local title
|
|
||||||
local body
|
|
||||||
local script
|
|
||||||
local footer
|
|
||||||
|
|
||||||
title=$(escape "$APP")
|
|
||||||
title="<title>$title</title>"
|
|
||||||
footer=$(escape "$FOOTER1")
|
|
||||||
|
|
||||||
body=$(escape "$1")
|
|
||||||
if [[ "$body" == *"..." ]]; then
|
|
||||||
body="<p class=\"loading\">${body/.../}</p>"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -n "${2:-}" ] && script="$2" || script=""
|
|
||||||
|
|
||||||
local HTML
|
|
||||||
HTML=$(<"$TEMPLATE")
|
|
||||||
HTML="${HTML/\[1\]/$title}"
|
|
||||||
HTML="${HTML/\[2\]/$script}"
|
|
||||||
HTML="${HTML/\[3\]/$body}"
|
|
||||||
HTML="${HTML/\[4\]/$footer}"
|
|
||||||
HTML="${HTML/\[5\]/$FOOTER2}"
|
|
||||||
|
|
||||||
echo "$HTML" > "$PAGE"
|
|
||||||
echo "$body" > "$INFO"
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
getCountry() {
|
|
||||||
local url=$1
|
|
||||||
local query=$2
|
|
||||||
local rc json result
|
|
||||||
|
|
||||||
{ json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); rc=$?; } || :
|
|
||||||
(( rc != 0 )) && return 0
|
|
||||||
|
|
||||||
{ result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || :
|
|
||||||
(( rc != 0 )) && return 0
|
|
||||||
|
|
||||||
[[ ${#result} -ne 2 ]] && return 0
|
|
||||||
[[ "${result^^}" == "XX" ]] && return 0
|
|
||||||
|
|
||||||
COUNTRY="${result^^}"
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
setCountry() {
|
|
||||||
|
|
||||||
[[ "${TZ,,}" == "asia/harbin" ]] && COUNTRY="CN"
|
|
||||||
[[ "${TZ,,}" == "asia/beijing" ]] && COUNTRY="CN"
|
|
||||||
[[ "${TZ,,}" == "asia/urumqi" ]] && COUNTRY="CN"
|
|
||||||
[[ "${TZ,,}" == "asia/kashgar" ]] && COUNTRY="CN"
|
|
||||||
[[ "${TZ,,}" == "asia/shanghai" ]] && COUNTRY="CN"
|
|
||||||
[[ "${TZ,,}" == "asia/chongqing" ]] && COUNTRY="CN"
|
|
||||||
|
|
||||||
[ -z "$COUNTRY" ] && getCountry "https://api.ipapi.is" ".location.country_code"
|
|
||||||
[ -z "$COUNTRY" ] && getCountry "https://ifconfig.co/json" ".country_iso"
|
|
||||||
[ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code"
|
|
||||||
[ -z "$COUNTRY" ] && getCountry "https://ipinfo.io/json" ".country"
|
|
||||||
[ -z "$COUNTRY" ] && getCountry "https://api.myip.com" ".cc"
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
addPackage() {
|
|
||||||
local pkg=$1
|
|
||||||
local desc=$2
|
|
||||||
|
|
||||||
if apt-mark showinstall | grep -qx "$pkg"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
MSG="Installing $desc..."
|
|
||||||
info "$MSG" && html "$MSG"
|
|
||||||
|
|
||||||
[ -z "$COUNTRY" ] && setCountry
|
|
||||||
|
|
||||||
if [[ "${COUNTRY^^}" == "CN" ]]; then
|
|
||||||
sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
|
|
||||||
fi
|
|
||||||
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get -qq update
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get -qq --no-install-recommends -y install "$pkg" > /dev/null
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Start webserver
|
|
||||||
cp -r /var/www/* /run/shm
|
|
||||||
html "Starting $APP for Docker..."
|
|
||||||
nginx -e stderr
|
|
||||||
|
|
||||||
return 0
|
|
||||||
+118
-40
@@ -5,11 +5,26 @@ set -Eeuo pipefail
|
|||||||
|
|
||||||
: "${HOST_MAC:=""}"
|
: "${HOST_MAC:=""}"
|
||||||
: "${HOST_DEBUG:=""}"
|
: "${HOST_DEBUG:=""}"
|
||||||
: "${HOST_SERIAL:=""}"
|
|
||||||
: "${HOST_MODEL:=""}"
|
: "${HOST_MODEL:=""}"
|
||||||
|
: "${HOST_SERIAL:=""}"
|
||||||
: "${GUEST_SERIAL:=""}"
|
: "${GUEST_SERIAL:=""}"
|
||||||
|
|
||||||
if [ -n "$HOST_MAC" ]; then
|
# Sanitize variables
|
||||||
|
HOST_MAC=$(strip "$HOST_MAC")
|
||||||
|
HOST_MODEL=$(strip "$HOST_MODEL")
|
||||||
|
HOST_SERIAL=$(strip "$HOST_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() {
|
||||||
|
local m
|
||||||
|
|
||||||
|
if [ -z "$HOST_MAC" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
HOST_MAC="${HOST_MAC//-/:}"
|
HOST_MAC="${HOST_MAC//-/:}"
|
||||||
|
|
||||||
@@ -22,54 +37,117 @@ if [ -n "$HOST_MAC" ]; then
|
|||||||
error "Invalid HOST_MAC address: '$HOST_MAC', should be 12 or 17 digits long!" && exit 28
|
error "Invalid HOST_MAC address: '$HOST_MAC', should be 12 or 17 digits long!" && exit 28
|
||||||
fi
|
fi
|
||||||
|
|
||||||
fi
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
HOST_ARGS=()
|
buildHostArguments() {
|
||||||
HOST_ARGS+=("-cpu=$CPU_CORES")
|
|
||||||
HOST_ARGS+=("-cpu_arch=$HOST_CPU")
|
|
||||||
|
|
||||||
[ -n "$HOST_MAC" ] && HOST_ARGS+=("-mac=$HOST_MAC")
|
# qemu-host is a sidecar that bridges DSM's proprietary serial agent to
|
||||||
[ -n "$HOST_MODEL" ] && HOST_ARGS+=("-model=$HOST_MODEL")
|
# Unix sockets used by shutdown and post-boot discovery helpers.
|
||||||
[ -n "$HOST_SERIAL" ] && HOST_ARGS+=("-hostsn=$HOST_SERIAL")
|
HOST_ARGS=()
|
||||||
[ -n "$GUEST_SERIAL" ] && HOST_ARGS+=("-guestsn=$GUEST_SERIAL")
|
HOST_ARGS+=("-cpu=$CPU_CORES")
|
||||||
|
HOST_ARGS+=("-cpu_arch=$HOST_CPU")
|
||||||
|
HOST_ARGS+=("-api=$HOST_API_SOCKET")
|
||||||
|
HOST_ARGS+=("-addr=$HOST_AGENT_SOCKET")
|
||||||
|
|
||||||
if [[ "$HOST_DEBUG" == [Yy1]* ]]; then
|
[ -n "$HOST_MAC" ] && HOST_ARGS+=("-mac=$HOST_MAC")
|
||||||
set -x
|
[ -n "$HOST_MODEL" ] && HOST_ARGS+=("-model=$HOST_MODEL")
|
||||||
./host.bin "${HOST_ARGS[@]}" &
|
[ -n "$HOST_SERIAL" ] && HOST_ARGS+=("-hostsn=$HOST_SERIAL")
|
||||||
{ set +x; } 2>/dev/null
|
[ -n "$GUEST_SERIAL" ] && HOST_ARGS+=("-guestsn=$GUEST_SERIAL")
|
||||||
echo
|
|
||||||
else
|
|
||||||
./host.bin "${HOST_ARGS[@]}" >/dev/null &
|
|
||||||
fi
|
|
||||||
|
|
||||||
cnt=0
|
return 0
|
||||||
sleep 0.2
|
}
|
||||||
|
|
||||||
while ! nc -z -w2 127.0.0.1 2210 > /dev/null 2>&1; do
|
startHostBinary() {
|
||||||
sleep 0.1
|
|
||||||
cnt=$((cnt + 1))
|
|
||||||
(( cnt > 50 )) && error "Failed to connect to qemu-host.." && exit 58
|
|
||||||
done
|
|
||||||
|
|
||||||
cnt=0
|
local pid
|
||||||
|
|
||||||
while ! nc -z -w2 127.0.0.1 12345 > /dev/null 2>&1; do
|
# Remove stale sockets and pid state before starting the sidecar; a Unix
|
||||||
sleep 0.1
|
# socket path cannot be rebound while an old filesystem entry remains.
|
||||||
cnt=$((cnt + 1))
|
rm -f -- "$HOST_PID" "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET" || return 1
|
||||||
(( cnt > 50 )) && error "Failed to connect to qemu-host.." && exit 59
|
|
||||||
done
|
|
||||||
|
|
||||||
# Configure serial ports
|
if enabled "$HOST_DEBUG"; then
|
||||||
|
set -x
|
||||||
|
./host.bin "${HOST_ARGS[@]}" &
|
||||||
|
{ set +x; } 2>/dev/null
|
||||||
|
pid=$!
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
./host.bin "${HOST_ARGS[@]}" >/dev/null &
|
||||||
|
pid=$!
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$CONSOLE" != [Yy]* ]]; then
|
printf '%s\n' "$pid" > "$HOST_PID"
|
||||||
SERIAL_OPTS="-serial pty"
|
|
||||||
else
|
|
||||||
SERIAL_OPTS="-serial mon:stdio"
|
|
||||||
fi
|
|
||||||
|
|
||||||
SERIAL_OPTS+=" \
|
return 0
|
||||||
-device virtio-serial-pci,id=virtio-serial0,bus=pcie.0,addr=0x3 \
|
}
|
||||||
-chardev socket,id=charchannel0,host=127.0.0.1,port=12345,reconnect=10 \
|
|
||||||
|
waitForSocket() {
|
||||||
|
|
||||||
|
local socket="$1"
|
||||||
|
local exit_code="$2"
|
||||||
|
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
|
||||||
|
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
configureSerialPorts() {
|
||||||
|
|
||||||
|
local bus
|
||||||
|
bus=$(getPciBus)
|
||||||
|
|
||||||
|
# Managed interactive mode separates the console and QEMU monitor into
|
||||||
|
# reconnecting sockets; other runs keep the simple combined stdio monitor.
|
||||||
|
if enabled "${SHUTDOWN:-Y}" && interactive; then
|
||||||
|
|
||||||
|
CONSOLE_SOCKET="$QEMU_DIR/console.sock"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
validateHostMac
|
||||||
|
|
||||||
|
buildHostArguments
|
||||||
|
startHostBinary
|
||||||
|
|
||||||
|
waitForSocket "$HOST_API_SOCKET" 58
|
||||||
|
waitForSocket "$HOST_AGENT_SOCKET" 59
|
||||||
|
|
||||||
|
configureSerialPorts
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
: "${WEB_PORT:="5000"}" # Webserver port
|
||||||
|
|
||||||
|
# Sanitize port variables
|
||||||
|
WEB_PORT=$(strip "$WEB_PORT")
|
||||||
|
|
||||||
|
WEB_PID="/run/nginx.pid"
|
||||||
|
WSD_LOG="/var/log/websocketd.log"
|
||||||
|
WSD_PID="$QEMU_DIR/websocketd.pid"
|
||||||
|
WSD_SOCKET="$QEMU_DIR/status-ws.sock"
|
||||||
|
|
||||||
|
prepareWebFiles() {
|
||||||
|
|
||||||
|
cp -r /var/www/* "$QEMU_DIR" || return 1
|
||||||
|
rm -f -- "$WSD_PID" "$WSD_SOCKET" "$WEB_PID" "$WSD_LOG" || return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
configureWebPorts() {
|
||||||
|
|
||||||
|
if ! sed -i \
|
||||||
|
-e "s|listen 5000 default_server;|listen $WEB_PORT default_server;|g" \
|
||||||
|
/etc/nginx/sites-enabled/web.conf; then
|
||||||
|
error "Failed to configure webserver port!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
configureIpv6Listen() {
|
||||||
|
|
||||||
|
# Use one dual-stack listener when IPv6 is active, avoiding separate IPv4
|
||||||
|
# and IPv6 sockets that can conflict on the same port.
|
||||||
|
if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]]; then
|
||||||
|
|
||||||
|
if ! sed -i \
|
||||||
|
"s/listen $WEB_PORT default_server;/listen [::]:$WEB_PORT default_server ipv6only=off;/g" \
|
||||||
|
/etc/nginx/sites-enabled/web.conf; then
|
||||||
|
error "Failed to configure IPv6 webserver listener!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
configureNginx() {
|
||||||
|
|
||||||
|
mkdir -p /etc/nginx/sites-enabled || return 1
|
||||||
|
rm -f /etc/nginx/sites-enabled/default || return 1
|
||||||
|
|
||||||
|
# TODO: Use setfacl to grant www-data access to the Unix sockets
|
||||||
|
# and restore unprivileged nginx workers.
|
||||||
|
if ! sed -i \
|
||||||
|
-e 's/^user .*/user root;/' \
|
||||||
|
-e 's/^worker_processes.*/worker_processes 1;/' \
|
||||||
|
/etc/nginx/nginx.conf; then
|
||||||
|
error "Failed to configure nginx!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! cp /etc/nginx/default.conf /etc/nginx/sites-enabled/web.conf; then
|
||||||
|
error "Failed to copy nginx config!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
configureWebServer() {
|
||||||
|
|
||||||
|
configureNginx || return 1
|
||||||
|
configureWebPorts || return 1
|
||||||
|
configureIpv6Listen || return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
stopWebServer() {
|
||||||
|
|
||||||
|
local pid
|
||||||
|
|
||||||
|
if readPidFile pid "$WEB_PID"; then
|
||||||
|
pKill "$pid" 2
|
||||||
|
|
||||||
|
# Escalate only after the normal termination grace period; stale nginx
|
||||||
|
# processes would otherwise keep the configured web port occupied.
|
||||||
|
if isAlive "$pid"; then
|
||||||
|
kill -9 -- "$pid" 2>/dev/null || :
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f -- "$WEB_PID"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
startWebServer() {
|
||||||
|
|
||||||
|
# Start webserver
|
||||||
|
nginx -e stderr || return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
stopWebsocketServer() {
|
||||||
|
|
||||||
|
local pid
|
||||||
|
|
||||||
|
if readPidFile pid "$WSD_PID"; then
|
||||||
|
pKill "$pid" 2
|
||||||
|
|
||||||
|
if isAlive "$pid"; then
|
||||||
|
kill -9 -- "$pid" 2>/dev/null || :
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f -- "$WSD_PID" "$WSD_SOCKET"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
startWebsocketServer() {
|
||||||
|
|
||||||
|
# Start websocket server
|
||||||
|
websocketd \
|
||||||
|
--unixsocket="$WSD_SOCKET" \
|
||||||
|
/run/socket.sh \
|
||||||
|
>"$WSD_LOG" 2>&1 &
|
||||||
|
|
||||||
|
local pid=$!
|
||||||
|
|
||||||
|
if ! echo "$pid" > "$WSD_PID"; then
|
||||||
|
kill "$pid" 2>/dev/null || :
|
||||||
|
rm -f -- "$WSD_PID"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local i
|
||||||
|
for (( i = 1; i <= 50; i++ )); do
|
||||||
|
|
||||||
|
if ! isAlive "$pid"; then
|
||||||
|
rm -f -- "$WSD_PID" "$WSD_SOCKET"
|
||||||
|
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
|
||||||
|
error "Failed to start websocket server!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -S "$WSD_SOCKET" ] && return 0
|
||||||
|
|
||||||
|
sleep 0.1
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
pKill "$pid" 2
|
||||||
|
|
||||||
|
if isAlive "$pid"; then
|
||||||
|
kill -9 -- "$pid" 2>/dev/null || :
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f -- "$WSD_PID" "$WSD_SOCKET"
|
||||||
|
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
|
||||||
|
error "Websocket server did not create its socket!"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareWebFiles
|
||||||
|
|
||||||
|
html "Starting $APP for $ENGINE..."
|
||||||
|
|
||||||
|
disabled "${WEB:-}" && return 0
|
||||||
|
|
||||||
|
configureWebServer
|
||||||
|
|
||||||
|
if startWebServer && startWebsocketServer; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
stopWebsocketServer || :
|
||||||
|
stopWebServer || :
|
||||||
|
|
||||||
|
return 1
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
lastmsg=""
|
||||||
|
path="/run/shm/msg.html"
|
||||||
|
dir=$(dirname -- "$path")
|
||||||
|
name=$(basename -- "$path")
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
|
||||||
|
[ ! -f "$path" ] && return 0
|
||||||
|
[ ! -s "$path" ] && return 0
|
||||||
|
|
||||||
|
msg=$(< "$path") || return 0
|
||||||
|
msg="${msg%$'\n'}"
|
||||||
|
|
||||||
|
[ -z "$msg" ] && return 0
|
||||||
|
[[ "$msg" == "$lastmsg" ]] && return 0
|
||||||
|
|
||||||
|
lastmsg="$msg"
|
||||||
|
# websocketd clients interpret s: as a status update and c: as a command;
|
||||||
|
# suppress unchanged status to avoid redundant browser work.
|
||||||
|
echo "s: $msg"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh
|
||||||
|
|
||||||
|
inotifywait \
|
||||||
|
-m -q \
|
||||||
|
-e close_write,moved_to,delete \
|
||||||
|
--format '%e %f' \
|
||||||
|
"$dir" |
|
||||||
|
while read -r event file; do
|
||||||
|
|
||||||
|
[[ "$file" == "$name" ]] || continue
|
||||||
|
|
||||||
|
case "${event,,}" in
|
||||||
|
"delete"* )
|
||||||
|
echo "c: vnc" ;;
|
||||||
|
# moved_to covers the atomic replacement used by html()/writeAtomic(),
|
||||||
|
# while close_write handles direct writers.
|
||||||
|
"close_write"* | "moved_to"* )
|
||||||
|
refresh ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
done
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# You can override this hook to execute a script before startup!
|
||||||
|
|
||||||
|
return 0
|
||||||
+646
@@ -0,0 +1,646 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
|
||||||
|
info () { printf "%b%s%b" "\E[1;34m❯ \E[1;36m" "${1:-}" "\E[0m\n"; }
|
||||||
|
error () { printf "%b%s%b" "\E[1;31m❯ " "ERROR: ${1:-}" "\E[0m\n" >&2; }
|
||||||
|
warn () { printf "%b%s%b" "\E[1;31m❯ " "Warning: ${1:-}" "\E[0m\n" >&2; }
|
||||||
|
|
||||||
|
app() {
|
||||||
|
|
||||||
|
echo "Virtual DSM"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
readPidFile() {
|
||||||
|
|
||||||
|
local -n _pid="$1"
|
||||||
|
_pid=""
|
||||||
|
|
||||||
|
if ! _pid=$(cat -- "$2" 2>/dev/null); then
|
||||||
|
_pid=""
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reject empty, zero, or nonnumeric pidfiles so cleanup can never signal an
|
||||||
|
# unintended process group.
|
||||||
|
if [[ ! "$_pid" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
_pid=""
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFlag() {
|
||||||
|
|
||||||
|
# Match a whitespace-delimited token in /proc/cpuinfo
|
||||||
|
grep -m1 '^flags[[:space:]]*:' /proc/cpuinfo | grep -Fqw -- "$1"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFeature() {
|
||||||
|
|
||||||
|
# Match a whitespace-delimited token in /proc/cpuinfo
|
||||||
|
grep -m1 '^Features[[:space:]]*:' /proc/cpuinfo | grep -Fqw -- "$1"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
isAmdCpu() {
|
||||||
|
|
||||||
|
local vendor
|
||||||
|
vendor=$(awk -F ': *' '/^vendor_id/{print $2; exit}' /proc/cpuinfo)
|
||||||
|
|
||||||
|
[[ "$vendor" == "AuthenticAMD" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
getPciBus() {
|
||||||
|
|
||||||
|
local machine="${1:-${MACHINE:-q35}}"
|
||||||
|
|
||||||
|
if [ -n "${PCI_BUS:-}" ]; then
|
||||||
|
echo "$PCI_BUS"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${machine,,}" in
|
||||||
|
pc|pc-i440fx*) echo "pci.0" ;;
|
||||||
|
*) echo "pcie.0" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
interactive() {
|
||||||
|
|
||||||
|
# A TTY on stdin is insufficient when /dev/tty is unavailable; require both
|
||||||
|
# before enabling interactive console handling.
|
||||||
|
[ -t 0 ] && : 2>/dev/null </dev/tty >/dev/tty
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
strip() {
|
||||||
|
|
||||||
|
local value="${1:-}"
|
||||||
|
|
||||||
|
# Remove surrounding whitespace
|
||||||
|
value="${value#"${value%%[![:space:]]*}"}"
|
||||||
|
value="${value%"${value##*[![:space:]]}"}"
|
||||||
|
|
||||||
|
# Remove leading/trailing single/double quotes
|
||||||
|
value="${value%\"}"
|
||||||
|
value="${value#\"}"
|
||||||
|
value="${value%\'}"
|
||||||
|
value="${value#\'}"
|
||||||
|
|
||||||
|
# Remove surrounding whitespace again
|
||||||
|
value="${value#"${value%%[![:space:]]*}"}"
|
||||||
|
value="${value%"${value##*[![:space:]]}"}"
|
||||||
|
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled() {
|
||||||
|
|
||||||
|
local value
|
||||||
|
value=$(strip "${1:-}")
|
||||||
|
|
||||||
|
case "${value,,}" in
|
||||||
|
y|yes|true|1|on|enable|enabled) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
disabled() {
|
||||||
|
|
||||||
|
local value
|
||||||
|
value=$(strip "${1:-}")
|
||||||
|
|
||||||
|
case "${value,,}" in
|
||||||
|
n|no|none|false|0|off|disable|disabled) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
formatBytes() {
|
||||||
|
|
||||||
|
local result
|
||||||
|
|
||||||
|
if ! result=$(numfmt --to=iec --suffix=B "$1" | sed -r 's/([A-Z])/ \1/' | sed 's/ B/ bytes/g;'); then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local unit="${result//[0-9. ]}"
|
||||||
|
result="${result//[a-zA-Z ]/}"
|
||||||
|
|
||||||
|
if [[ "${2:-}" == "up" ]]; then
|
||||||
|
if [[ "$result" == *"."* ]]; then
|
||||||
|
result="${result%%.*}"
|
||||||
|
result=$((result+1))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ "${2:-}" == "down" ]]; then
|
||||||
|
result="${result%%.*}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$result $unit"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
isAlive() {
|
||||||
|
|
||||||
|
local pid="$1"
|
||||||
|
[ -z "$pid" ] && return 1
|
||||||
|
|
||||||
|
if kill -0 "$pid" 2>/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
waitPid() {
|
||||||
|
|
||||||
|
local pid="$1"
|
||||||
|
local timeout="${2:-10}"
|
||||||
|
local deadline=$((SECONDS + timeout))
|
||||||
|
|
||||||
|
while [ -n "$pid" ] && isAlive "$pid"; do
|
||||||
|
(( SECONDS >= deadline )) && return 1
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
waitPidFile() {
|
||||||
|
|
||||||
|
local pid
|
||||||
|
local file="$1"
|
||||||
|
local timeout="${2:-10}"
|
||||||
|
local deadline=$((SECONDS + timeout))
|
||||||
|
|
||||||
|
readPidFile pid "$file" || return 0
|
||||||
|
|
||||||
|
while [ -s "$file" ] && isAlive "$pid"; do
|
||||||
|
(( SECONDS >= deadline )) && return 1
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
|
||||||
|
rm -f -- "$file"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pKill() {
|
||||||
|
|
||||||
|
local pid="$1"
|
||||||
|
local timeout="${2:-10}"
|
||||||
|
|
||||||
|
{ kill -15 -- "$pid" || :; } 2>/dev/null
|
||||||
|
|
||||||
|
if ! waitPid "$pid" "$timeout"; then
|
||||||
|
warn "Timed out while waiting for PID $pid"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fWait() {
|
||||||
|
|
||||||
|
local name="$1"
|
||||||
|
local timeout="${2:-10}"
|
||||||
|
local deadline=$((SECONDS + timeout))
|
||||||
|
|
||||||
|
[ -z "$name" ] && return 0
|
||||||
|
|
||||||
|
while pgrep -f -l "$name" >/dev/null; do
|
||||||
|
if (( SECONDS >= deadline )); then
|
||||||
|
warn "Timed out while waiting for process: $name"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fKill() {
|
||||||
|
|
||||||
|
local name="$1"
|
||||||
|
local timeout="${2:-10}"
|
||||||
|
|
||||||
|
[ -z "$name" ] && return 0
|
||||||
|
|
||||||
|
{ pkill -f "$name" || :; } 2>/dev/null
|
||||||
|
fWait "$name" "$timeout"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
sKill() {
|
||||||
|
|
||||||
|
local pid
|
||||||
|
local file="$1"
|
||||||
|
|
||||||
|
readPidFile pid "$file" || return 0
|
||||||
|
|
||||||
|
if isAlive "$pid"; then
|
||||||
|
{ kill -15 -- "$pid" || :; } 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
mKill() {
|
||||||
|
|
||||||
|
local timeout=10
|
||||||
|
local files=("$@")
|
||||||
|
|
||||||
|
for file in "${files[@]}"; do
|
||||||
|
sKill "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
for file in "${files[@]}"; do
|
||||||
|
if ! waitPidFile "$file" "$timeout"; then
|
||||||
|
warn "Timed out while waiting for PID file: $file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
setOwner() {
|
||||||
|
|
||||||
|
local file="$1"
|
||||||
|
local dir uid gid
|
||||||
|
|
||||||
|
[ ! -f "$file" ] && return 1
|
||||||
|
|
||||||
|
# Match generated files to the owner of their bind-mounted parent directory
|
||||||
|
# instead of assuming a fixed container or host UID.
|
||||||
|
dir=$(dirname -- "$file")
|
||||||
|
uid=$(stat -c '%u' "$dir") || return 1
|
||||||
|
gid=$(stat -c '%g' "$dir") || return 1
|
||||||
|
|
||||||
|
chown "$uid:$gid" "$file" || return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
makeDir() {
|
||||||
|
|
||||||
|
local path="$1"
|
||||||
|
local dir uid gid
|
||||||
|
|
||||||
|
[ -d "$path" ] && return 0
|
||||||
|
mkdir -p "$path" || return 1
|
||||||
|
|
||||||
|
dir=$(dirname -- "$path")
|
||||||
|
|
||||||
|
if ! uid=$(stat -c '%u' "$dir") || ! gid=$(stat -c '%g' "$dir"); then
|
||||||
|
warn "failed to determine the owner for \"$path\"."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! chown "$uid:$gid" "$path"; then
|
||||||
|
warn "failed to set the owner for \"$path\"."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
finiteMemoryLimit() {
|
||||||
|
|
||||||
|
local limit="$1"
|
||||||
|
# cgroup v1 commonly reports this enormous sentinel for an unlimited memory
|
||||||
|
# limit; compare as decimal strings to avoid shell integer overflow.
|
||||||
|
local sentinel="4611686018427387904"
|
||||||
|
local i
|
||||||
|
|
||||||
|
[[ "$limit" =~ ^[0-9]+$ ]] || return 1
|
||||||
|
|
||||||
|
(( ${#limit} < ${#sentinel} )) && return 0
|
||||||
|
(( ${#limit} > ${#sentinel} )) && return 1
|
||||||
|
|
||||||
|
for (( i=0; i<${#sentinel}; i++ )); do
|
||||||
|
local left="${limit:i:1}"
|
||||||
|
local right="${sentinel:i:1}"
|
||||||
|
|
||||||
|
(( left < right )) && return 0
|
||||||
|
(( left > right )) && return 1
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
getMemoryInfo() {
|
||||||
|
|
||||||
|
local host_total
|
||||||
|
local host_avail
|
||||||
|
local limit=""
|
||||||
|
local current=""
|
||||||
|
|
||||||
|
host_total=$(free -b | awk '/^Mem:/ {print $2; exit}')
|
||||||
|
host_avail=$(free -b | awk '/^Mem:/ {print $7; exit}')
|
||||||
|
|
||||||
|
RAM_TOTAL="$host_total"
|
||||||
|
RAM_AVAIL="$host_avail"
|
||||||
|
|
||||||
|
if [ -r /sys/fs/cgroup/memory.max ] && [ -r /sys/fs/cgroup/memory.current ]; then
|
||||||
|
limit=$(< /sys/fs/cgroup/memory.max)
|
||||||
|
current=$(< /sys/fs/cgroup/memory.current)
|
||||||
|
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ] && [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then
|
||||||
|
limit=$(< /sys/fs/cgroup/memory/memory.limit_in_bytes)
|
||||||
|
current=$(< /sys/fs/cgroup/memory/memory.usage_in_bytes)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use the tighter of host availability and the container's remaining cgroup
|
||||||
|
# allowance so RAM sizing cannot exceed either boundary.
|
||||||
|
if finiteMemoryLimit "$limit" && [[ "$current" =~ ^[0-9]+$ ]]; then
|
||||||
|
(( limit < RAM_TOTAL )) && RAM_TOTAL="$limit"
|
||||||
|
|
||||||
|
local available=$(( limit - current ))
|
||||||
|
(( available < 0 )) && available=0
|
||||||
|
(( available < RAM_AVAIL )) && RAM_AVAIL="$available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
stateFile() {
|
||||||
|
|
||||||
|
local name="$1"
|
||||||
|
local prefix="${2:-$PROCESS}"
|
||||||
|
|
||||||
|
[[ "$name" == */* ]] && printf '%s\n' "$name" && return 0
|
||||||
|
|
||||||
|
printf '%s/%s.%s\n' "$STORAGE" "$prefix" "$name"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile() {
|
||||||
|
|
||||||
|
local txt="$1"
|
||||||
|
local path="$2"
|
||||||
|
|
||||||
|
if ! printf '%s\n' "$txt" > "$path"; then
|
||||||
|
error "Failed to write file \"$path\" !"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! setOwner "$path"; then
|
||||||
|
warn "failed to set the owner for \"$path\"."
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
writeAtomic() {
|
||||||
|
|
||||||
|
local path="$1"
|
||||||
|
local content="$2"
|
||||||
|
# Use a per-process temporary file and rename so readers see either the old
|
||||||
|
# complete value or the new complete value.
|
||||||
|
local tmp="${path}.${BASHPID}.tmp"
|
||||||
|
|
||||||
|
if ! printf '%s\n' "$content" > "$tmp"; then
|
||||||
|
rm -f -- "$tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! mv -f -- "$tmp" "$path"; then
|
||||||
|
rm -f -- "$tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
readFile() {
|
||||||
|
|
||||||
|
local path="$1"
|
||||||
|
local value
|
||||||
|
|
||||||
|
[ -s "$path" ] || return 0
|
||||||
|
|
||||||
|
value=$(<"$path") || return 1
|
||||||
|
value="${value//[![:print:]]/}"
|
||||||
|
|
||||||
|
printf '%s\n' "$value"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
writeState() {
|
||||||
|
|
||||||
|
local name="$1"
|
||||||
|
local value="$2"
|
||||||
|
local prefix="${3:-$PROCESS}"
|
||||||
|
local path
|
||||||
|
|
||||||
|
[ -z "$value" ] && return 0
|
||||||
|
|
||||||
|
path=$(stateFile "$name" "$prefix") || return 1
|
||||||
|
writeFile "$value" "$path"
|
||||||
|
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
readState() {
|
||||||
|
|
||||||
|
local name="$1"
|
||||||
|
local prefix="${2:-$PROCESS}"
|
||||||
|
local path
|
||||||
|
|
||||||
|
path=$(stateFile "$name" "$prefix") || return 1
|
||||||
|
readFile "$path"
|
||||||
|
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreState() {
|
||||||
|
|
||||||
|
local var="$1"
|
||||||
|
local name="$2"
|
||||||
|
local force="${3:-N}"
|
||||||
|
local prefix="${4:-$PROCESS}"
|
||||||
|
local value
|
||||||
|
|
||||||
|
# Persistent state fills only unset variables unless force is requested,
|
||||||
|
# preserving explicit environment overrides.
|
||||||
|
if ! enabled "$force"; then
|
||||||
|
[ -z "${!var:-}" ] || return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
value=$(readState "$name" "$prefix") || return 1
|
||||||
|
[ -n "$value" ] || return 0
|
||||||
|
|
||||||
|
printf -v "$var" '%s' "$value" || return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
escape () {
|
||||||
|
|
||||||
|
local s=${1//&/\&}
|
||||||
|
s=${s//</\<}
|
||||||
|
s=${s//>/\>}
|
||||||
|
s=${s//'"'/\"}
|
||||||
|
|
||||||
|
printf -- %s "$s"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeXML() {
|
||||||
|
|
||||||
|
printf '%s' "$1" | sed \
|
||||||
|
-e 's/&/\&/g' \
|
||||||
|
-e 's/</\</g' \
|
||||||
|
-e 's/>/\>/g' \
|
||||||
|
-e 's/"/\"/g' \
|
||||||
|
-e "s/'/\'/g"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
html() {
|
||||||
|
|
||||||
|
local title
|
||||||
|
local body
|
||||||
|
local script="${2:-}"
|
||||||
|
local footer
|
||||||
|
|
||||||
|
title=$(escape "$APP")
|
||||||
|
title="<title>$title</title>"
|
||||||
|
footer=$(escape "$FOOTER1")
|
||||||
|
|
||||||
|
body=$(escape "$1")
|
||||||
|
if [[ "$body" == *"..." ]]; then
|
||||||
|
body="<p class=\"loading\">${body/.../}</p>"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local HTML
|
||||||
|
HTML=$(<"$TEMPLATE")
|
||||||
|
HTML="${HTML/\[1\]/$title}"
|
||||||
|
HTML="${HTML/\[2\]/$script}"
|
||||||
|
HTML="${HTML/\[3\]/$body}"
|
||||||
|
HTML="${HTML/\[4\]/$footer}"
|
||||||
|
HTML="${HTML/\[5\]/$FOOTER2}"
|
||||||
|
|
||||||
|
# Publish both the full page and websocket fragment atomically because nginx
|
||||||
|
# and websocketd may read them concurrently.
|
||||||
|
writeAtomic "$PAGE" "$HTML" || return 1
|
||||||
|
writeAtomic "$INFO" "$body" || return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu() {
|
||||||
|
|
||||||
|
local ret
|
||||||
|
local cpu=""
|
||||||
|
|
||||||
|
ret=$(lscpu)
|
||||||
|
|
||||||
|
if grep -qi "model name" <<< "$ret"; then
|
||||||
|
cpu=$(echo "$ret" | grep -m 1 -i 'model name' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${cpu// /}" ] && grep -qi "model:" <<< "$ret"; then
|
||||||
|
cpu=$(echo "$ret" | grep -m 1 -i 'model:' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
|
||||||
|
fi
|
||||||
|
|
||||||
|
cpu="${cpu// CPU/}"
|
||||||
|
cpu="${cpu// [0-9][0-9][0-9] Core}"
|
||||||
|
cpu="${cpu// [0-9][0-9] Core}"
|
||||||
|
cpu="${cpu// [0-9] Core}"
|
||||||
|
cpu="${cpu//[0-9][0-9]th Gen }"
|
||||||
|
cpu="${cpu//[0-9]th Gen }"
|
||||||
|
cpu="${cpu// Processor/}"
|
||||||
|
cpu="${cpu// Quad core/}"
|
||||||
|
cpu="${cpu// Dual core/}"
|
||||||
|
cpu="${cpu// Octa core/}"
|
||||||
|
cpu="${cpu// Hexa core/}"
|
||||||
|
cpu="${cpu// Core TM/ Core}"
|
||||||
|
cpu="${cpu// with Radeon Graphics/}"
|
||||||
|
cpu="${cpu// with Radeon Vega Graphics/}"
|
||||||
|
cpu="${cpu// with Radeon Vega Mobile Gfx/}"
|
||||||
|
cpu="${cpu// w Radeon [0-9][0-9][0-9]M Graphics/}"
|
||||||
|
|
||||||
|
[ -z "${cpu// /}" ] && cpu="Unknown"
|
||||||
|
|
||||||
|
echo "$cpu"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
getCountry() {
|
||||||
|
|
||||||
|
local url=$1
|
||||||
|
local query=$2
|
||||||
|
local json result
|
||||||
|
|
||||||
|
{ json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); local rc=$?; } || :
|
||||||
|
(( rc != 0 )) && return 0
|
||||||
|
|
||||||
|
{ result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || :
|
||||||
|
(( rc != 0 )) && return 0
|
||||||
|
|
||||||
|
[[ ${#result} -ne 2 ]] && return 0
|
||||||
|
[[ "${result^^}" == "XX" ]] && return 0
|
||||||
|
|
||||||
|
COUNTRY="${result^^}"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
setCountry() {
|
||||||
|
|
||||||
|
[[ "${TZ,,}" == "asia/harbin" ]] && COUNTRY="CN"
|
||||||
|
[[ "${TZ,,}" == "asia/beijing" ]] && COUNTRY="CN"
|
||||||
|
[[ "${TZ,,}" == "asia/urumqi" ]] && COUNTRY="CN"
|
||||||
|
[[ "${TZ,,}" == "asia/kashgar" ]] && COUNTRY="CN"
|
||||||
|
[[ "${TZ,,}" == "asia/shanghai" ]] && COUNTRY="CN"
|
||||||
|
[[ "${TZ,,}" == "asia/chongqing" ]] && COUNTRY="CN"
|
||||||
|
|
||||||
|
# Country detection is best-effort and tries independent services in order;
|
||||||
|
# failure leaves mirror selection at its global default.
|
||||||
|
[ -z "$COUNTRY" ] && getCountry "https://api.ipapi.is" ".location.country_code"
|
||||||
|
[ -z "$COUNTRY" ] && getCountry "https://ifconfig.co/json" ".country_iso"
|
||||||
|
[ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code"
|
||||||
|
[ -z "$COUNTRY" ] && getCountry "https://ipinfo.io/json" ".country"
|
||||||
|
[ -z "$COUNTRY" ] && getCountry "https://api.ipquery.io/?format=json" ".location.country_code"
|
||||||
|
[ -z "$COUNTRY" ] && getCountry "https://api.myip.com" ".cc"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
addPackage() {
|
||||||
|
|
||||||
|
local pkg=$1
|
||||||
|
local desc=$2
|
||||||
|
|
||||||
|
if apt-mark showinstall | grep -qx "$pkg"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
MSG="Installing $desc..."
|
||||||
|
info "$MSG" && html "$MSG"
|
||||||
|
|
||||||
|
[ -z "$COUNTRY" ] && setCountry
|
||||||
|
|
||||||
|
# Use a mainland mirror only for on-demand package installation, avoiding
|
||||||
|
# slow or inaccessible Debian endpoints in that region.
|
||||||
|
if [[ "${COUNTRY^^}" == "CN" ]]; then
|
||||||
|
sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get -qq update || return 1
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get -qq --no-install-recommends -y install "$pkg" > /dev/null || return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
|
||||||
listen 5000 default_server;
|
listen 5000 default_server;
|
||||||
|
|
||||||
autoindex on;
|
autoindex on;
|
||||||
@@ -28,4 +27,18 @@ server {
|
|||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /status {
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
|
||||||
|
proxy_pass http://unix:/run/shm/status-ws.sock:/;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+155
-37
@@ -1,16 +1,32 @@
|
|||||||
|
var timer;
|
||||||
var request;
|
var request;
|
||||||
|
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 {
|
||||||
|
abortRequest();
|
||||||
|
|
||||||
if (window.XMLHttpRequest) {
|
if (window.XMLHttpRequest) {
|
||||||
request = new XMLHttpRequest();
|
request = new XMLHttpRequest();
|
||||||
} else {
|
} else {
|
||||||
throw "XMLHttpRequest not available!";
|
throw new Error("XMLHttpRequest not available!");
|
||||||
}
|
}
|
||||||
|
|
||||||
request.onreadystatechange = processInfo;
|
request.onreadystatechange = processInfo;
|
||||||
@@ -18,45 +34,73 @@ function getInfo() {
|
|||||||
request.send();
|
request.send();
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
var err = "Error: " + e.message;
|
setError("Error: " + e.message);
|
||||||
console.log(err);
|
|
||||||
setError(err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getURL() {
|
||||||
|
|
||||||
|
var protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
var path = window.location.pathname.replace(/[^/]*$/, '').replace(/\/$/, '');
|
||||||
|
|
||||||
|
return protocol + "//" + window.location.host + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function processMsg(msg) {
|
||||||
|
|
||||||
|
if (msg.toLowerCase().indexOf("href=") !== -1) {
|
||||||
|
var div = document.createElement("div");
|
||||||
|
div.innerHTML = msg;
|
||||||
|
var url = div.querySelector("a").href;
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.assign(url);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
setInfo(msg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function processInfo() {
|
function processInfo() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (request.readyState != 4) {
|
if (request.readyState != 4) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var msg = request.responseText;
|
var response = request;
|
||||||
if (msg == null || msg.length == 0) {
|
request = null;
|
||||||
setInfo("Booting DSM instance", true);
|
|
||||||
|
var status = response.status;
|
||||||
|
|
||||||
|
if (status == 502 || status == 503 || status == 504) {
|
||||||
schedule();
|
schedule();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = response.responseText;
|
||||||
|
if (msg == null || msg.length == 0) {
|
||||||
|
|
||||||
|
if (booting) {
|
||||||
|
schedule();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
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 {
|
||||||
if (msg.toLowerCase().indexOf("href=") !== -1) {
|
processMsg(msg);
|
||||||
var div = document.createElement("div");
|
if (msg.toLowerCase().indexOf("href=") == -1) {
|
||||||
div.innerHTML = msg;
|
|
||||||
var url = div.querySelector("a").href;
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.assign(url);
|
|
||||||
}, 3000);
|
|
||||||
setInfo(msg);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
setInfo(msg);
|
|
||||||
schedule();
|
schedule();
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,45 +110,61 @@ function processInfo() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setError("Error: Received statuscode " + request.status);
|
setError("Error: Received statuscode " + status);
|
||||||
schedule();
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
var err = "Error: " + e.message;
|
setError("Error: " + e.message);
|
||||||
console.log(err);
|
|
||||||
setError(err);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setInfo(msg, loading, error) {
|
function extractContent(s) {
|
||||||
|
var span = document.createElement('span');
|
||||||
|
span.innerHTML = s;
|
||||||
|
return span.textContent || span.innerText;
|
||||||
|
};
|
||||||
|
|
||||||
|
function setInfo(msg, loading, error) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (msg == null || msg.length == 0) {
|
if (msg == null || msg.length == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var el = document.getElementById("spinner");
|
if (msg.includes("Booting ")) {
|
||||||
|
booting = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var el = document.getElementById("info");
|
||||||
|
|
||||||
|
if (el.innerText == msg || el.innerHTML == msg) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var spin = document.getElementById("spinner");
|
||||||
|
|
||||||
error = !!error;
|
error = !!error;
|
||||||
if (!error) {
|
if (!error) {
|
||||||
el.style.visibility = 'visible';
|
spin.style.visibility = 'visible';
|
||||||
} else {
|
} else {
|
||||||
el.style.visibility = 'hidden';
|
spin.style.visibility = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var p = "<p class=\"loading\">";
|
||||||
loading = !!loading;
|
loading = !!loading;
|
||||||
if (loading) {
|
if (loading) {
|
||||||
msg = "<p class=\"loading\">" + msg + "</p>";
|
msg = p + msg + "</p>";
|
||||||
}
|
}
|
||||||
|
|
||||||
el = document.getElementById("info");
|
if (msg.includes(p)) {
|
||||||
|
if (el.innerHTML.includes(p)) {
|
||||||
if (el.innerHTML != msg) {
|
el.getElementsByClassName('loading')[0].textContent = extractContent(msg);
|
||||||
el.innerHTML = msg;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
el.innerHTML = msg;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -114,17 +174,75 @@ function setInfo(msg, loading, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setError(text) {
|
function setError(text) {
|
||||||
|
console.warn(text);
|
||||||
return setInfo(text, false, true);
|
return setInfo(text, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function schedule() {
|
function schedule() {
|
||||||
setTimeout(getInfo, interval);
|
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(getInfo, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.location.reload();
|
window.location.reload();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
|
||||||
|
var wsUrl = getURL() + "/status";
|
||||||
|
var ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
ws.onmessage = function(e) {
|
||||||
|
|
||||||
|
var pos = e.data.indexOf(":");
|
||||||
|
var cmd = e.data.substring(0, pos);
|
||||||
|
var msg = e.data.substring(pos + 2);
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case "s":
|
||||||
|
|
||||||
|
var aborted = abortRequest();
|
||||||
|
|
||||||
|
processMsg(msg);
|
||||||
|
|
||||||
|
if (aborted &&
|
||||||
|
msg.toLowerCase().indexOf("href=") == -1) {
|
||||||
|
schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "e":
|
||||||
|
|
||||||
|
if (abortRequest()) {
|
||||||
|
schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn("Unknown event: " + cmd);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = function(e) {
|
||||||
|
setTimeout(function() {
|
||||||
|
connect();
|
||||||
|
}, interval);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = function(e) {
|
||||||
|
ws.close();
|
||||||
|
if (!booting) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
schedule();
|
schedule();
|
||||||
|
connect();
|
||||||
|
|||||||
Reference in New Issue
Block a user