@synopackageland/cli 0.3.10 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -9
- package/dist/bin.js +1 -2
- package/dist/build-spk/generator.d.ts +0 -3
- package/dist/build-spk/generator.js +135 -287
- package/dist/build-spk/native-lifecycle.js +0 -3
- package/dist/build-spk/native-payload.js +0 -1
- package/dist/cli.js +171 -151
- package/dist/compose/analyze.d.ts +1 -2
- package/dist/compose/analyze.js +5 -48
- package/dist/contract/unsupported-types.d.ts +10 -0
- package/dist/contract/unsupported-types.js +49 -0
- package/dist/contract/validate.js +25 -2
- package/dist/contract/web-launch.js +0 -2
- package/dist/deploy/deploy.d.ts +1 -4
- package/dist/deploy/deploy.js +45 -156
- package/dist/deploy/homes-values.js +1 -1
- package/dist/deploy/plan.d.ts +18 -0
- package/dist/deploy/plan.js +54 -0
- package/dist/deploy/spk-version.d.ts +2 -14
- package/dist/deploy/spk-version.js +25 -111
- package/dist/dev.d.ts +1 -3
- package/dist/dev.js +14 -28
- package/dist/error-registry.js +810 -862
- package/dist/host/dsm-client.js +12 -272
- package/dist/host/dsm-port-profile.d.ts +0 -1
- package/dist/host/dsm-port-profile.js +0 -2
- package/dist/host/types.d.ts +1 -83
- package/dist/index.d.ts +0 -9
- package/dist/index.js +0 -6
- package/dist/info.d.ts +0 -1
- package/dist/info.js +3 -10
- package/dist/init.d.ts +11 -1
- package/dist/init.js +112 -4
- package/dist/log.js +2 -3
- package/dist/native/validate.js +0 -1
- package/dist/standalone/binary.d.ts +9 -0
- package/dist/standalone/binary.js +67 -0
- package/dist/standalone/build.d.ts +25 -0
- package/dist/standalone/build.js +108 -0
- package/dist/standalone/dsm-window.d.ts +26 -0
- package/dist/standalone/dsm-window.js +251 -0
- package/dist/standalone/helper.d.ts +31 -0
- package/dist/standalone/helper.js +104 -0
- package/dist/standalone/i18n.d.ts +28 -0
- package/dist/standalone/i18n.js +161 -0
- package/dist/standalone/inspect-spk.d.ts +21 -0
- package/dist/standalone/inspect-spk.js +160 -0
- package/dist/standalone/manifest.d.ts +71 -0
- package/dist/standalone/manifest.js +276 -0
- package/dist/standalone/root-messages.d.ts +7 -0
- package/dist/standalone/root-messages.js +250 -0
- package/dist/standalone/root-ops.d.ts +42 -0
- package/dist/standalone/root-ops.js +101 -0
- package/dist/standalone/root-window.d.ts +35 -0
- package/dist/standalone/root-window.js +290 -0
- package/dist/standalone/spk.d.ts +22 -0
- package/dist/standalone/spk.js +205 -0
- package/dist/templates/binary/synology-app.yaml +7 -0
- package/dist/templates/hello-files/compose.yaml +0 -1
- package/dist/templates/hello-web/compose.yaml +13 -10
- package/dist/templates/hello-web/html/app.js +1 -23
- package/dist/templates/hello-web/html/index.html +1 -3
- package/dist/templates/hello-web-page/compose.yaml +13 -10
- package/dist/templates/hello-web-page/html/app.js +1 -23
- package/dist/templates/hello-web-page/html/index.html +1 -3
- package/dist/templates/native-go/README.md +268 -0
- package/dist/templates/native-go/backend/cmd/app/main.go +49 -0
- package/dist/templates/native-go/backend/cmd/helper/main.go +38 -0
- package/dist/templates/native-go/backend/go.mod +3 -0
- package/dist/templates/native-go/backend/internal/api/errors.go +33 -0
- package/dist/templates/native-go/backend/internal/api/registry.go +94 -0
- package/dist/templates/native-go/backend/internal/api/registry_test.go +329 -0
- package/dist/templates/native-go/backend/internal/api/server.go +155 -0
- package/dist/templates/native-go/backend/internal/app/root_none.go +9 -0
- package/dist/templates/native-go/backend/internal/app/routes.go +127 -0
- package/dist/templates/native-go/backend/internal/app/routes_test.go +167 -0
- package/dist/templates/native-go/backend/internal/dsm/auth.go +118 -0
- package/dist/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
- package/dist/templates/native-go/backend/internal/helper/control.go +219 -0
- package/dist/templates/native-go/backend/internal/helper/control_test.go +242 -0
- package/dist/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
- package/dist/templates/native-go/backend/internal/helper/peer_other.go +7 -0
- package/dist/templates/native-go/backend/internal/helper/process_linux.go +167 -0
- package/dist/templates/native-go/backend/internal/helper/process_other.go +5 -0
- package/dist/templates/native-go/backend/internal/helper/server_linux.go +83 -0
- package/dist/templates/native-go/backend/internal/helper/status.go +271 -0
- package/dist/templates/native-go/backend/internal/helper/status_test.go +205 -0
- package/dist/templates/native-go/backend/internal/helper/supervision.go +84 -0
- package/dist/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
- package/dist/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
- package/dist/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
- package/dist/templates/native-go/dsm-package.yaml +58 -0
- package/dist/templates/native-go/gitignore +1 -0
- package/dist/templates/native-go/i18n/glossary.json +662 -0
- package/dist/templates/native-go/i18n/messages/de.json +28 -0
- package/dist/templates/native-go/i18n/messages/en.json +28 -0
- package/dist/templates/native-go/i18n/messages/es.json +28 -0
- package/dist/templates/native-go/i18n/messages/fr.json +28 -0
- package/dist/templates/native-go/i18n/messages/it.json +28 -0
- package/dist/templates/native-go/i18n/messages/ja.json +28 -0
- package/dist/templates/native-go/i18n/messages/ko.json +28 -0
- package/dist/templates/native-go/i18n/messages/zh-CN.json +28 -0
- package/dist/templates/native-go/i18n/messages/zh-TW.json +28 -0
- package/dist/templates/native-go/ui/app.css +35 -0
- package/dist/templates/native-go/ui/app.js +94 -0
- package/dist/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
- package/dist/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
- package/dist/templates/native-go-better-auth/node/app.mjs +72 -0
- package/dist/templates/native-go-better-auth/node/build.mjs +2 -0
- package/dist/templates/native-go-better-auth/node/package-lock.json +862 -0
- package/dist/templates/native-go-better-auth/node/package.json +1 -0
- package/dist/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
- package/dist/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
- package/dist/templates/native-go-integration/backend/internal/app/personal.go +203 -0
- package/dist/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
- package/dist/templates/native-go-root/backend/internal/app/root.go +107 -0
- package/dist/templates/native-go-root/backend/internal/app/root_test.go +124 -0
- package/dist/templates/native-go-root/root/whoami.sh +6 -0
- package/dist/templates/native-go-vue/README.md +110 -0
- package/dist/templates/native-go-vue/frontend/DESIGN.md +78 -0
- package/dist/templates/native-go-vue/frontend/package-lock.json +2507 -0
- package/dist/templates/native-go-vue/frontend/package.json +6 -0
- package/dist/templates/native-go-vue/frontend/src/App.vue +132 -0
- package/dist/templates/native-go-vue/frontend/src/context.ts +17 -0
- package/dist/templates/native-go-vue/frontend/src/main.ts +26 -0
- package/dist/templates/native-go-vue/frontend/src/style.css +14 -0
- package/dist/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
- package/dist/templates/native-go-vue/frontend/tsconfig.json +1 -0
- package/dist/templates/native-go-vue/frontend/vite.config.ts +8 -0
- package/dist/templates/native-go-vue/gitignore +2 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +11 -7
- package/package.json +3 -7
- package/schema/synology-app.schema.json +36 -37
- package/skills/SKILL.md +4 -48
- package/skills/reference/create.md +11 -83
- package/skills/reference/deploy.md +7 -78
- package/skills/reference/diagnose.md +4 -101
- package/skills/reference/files.md +5 -41
- package/skills/reference/identity.md +3 -29
- package/skills/reference/wrap.md +0 -2
- package/templates/binary/synology-app.yaml +7 -0
- package/templates/hello-files/compose.yaml +0 -1
- package/templates/hello-web/compose.yaml +13 -10
- package/templates/hello-web/html/app.js +1 -23
- package/templates/hello-web/html/index.html +1 -3
- package/templates/hello-web-page/compose.yaml +13 -10
- package/templates/hello-web-page/html/app.js +1 -23
- package/templates/hello-web-page/html/index.html +1 -3
- package/templates/native-go/README.md +268 -0
- package/templates/native-go/backend/cmd/app/main.go +49 -0
- package/templates/native-go/backend/cmd/helper/main.go +38 -0
- package/templates/native-go/backend/go.mod +3 -0
- package/templates/native-go/backend/internal/api/errors.go +33 -0
- package/templates/native-go/backend/internal/api/registry.go +94 -0
- package/templates/native-go/backend/internal/api/registry_test.go +329 -0
- package/templates/native-go/backend/internal/api/server.go +155 -0
- package/templates/native-go/backend/internal/app/root_none.go +9 -0
- package/templates/native-go/backend/internal/app/routes.go +127 -0
- package/templates/native-go/backend/internal/app/routes_test.go +167 -0
- package/templates/native-go/backend/internal/dsm/auth.go +118 -0
- package/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
- package/templates/native-go/backend/internal/helper/control.go +219 -0
- package/templates/native-go/backend/internal/helper/control_test.go +242 -0
- package/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
- package/templates/native-go/backend/internal/helper/peer_other.go +7 -0
- package/templates/native-go/backend/internal/helper/process_linux.go +167 -0
- package/templates/native-go/backend/internal/helper/process_other.go +5 -0
- package/templates/native-go/backend/internal/helper/server_linux.go +83 -0
- package/templates/native-go/backend/internal/helper/status.go +271 -0
- package/templates/native-go/backend/internal/helper/status_test.go +205 -0
- package/templates/native-go/backend/internal/helper/supervision.go +84 -0
- package/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
- package/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
- package/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
- package/templates/native-go/dsm-package.yaml +58 -0
- package/templates/native-go/gitignore +1 -0
- package/templates/native-go/i18n/glossary.json +662 -0
- package/templates/native-go/i18n/messages/de.json +28 -0
- package/templates/native-go/i18n/messages/en.json +28 -0
- package/templates/native-go/i18n/messages/es.json +28 -0
- package/templates/native-go/i18n/messages/fr.json +28 -0
- package/templates/native-go/i18n/messages/it.json +28 -0
- package/templates/native-go/i18n/messages/ja.json +28 -0
- package/templates/native-go/i18n/messages/ko.json +28 -0
- package/templates/native-go/i18n/messages/zh-CN.json +28 -0
- package/templates/native-go/i18n/messages/zh-TW.json +28 -0
- package/templates/native-go/ui/app.css +35 -0
- package/templates/native-go/ui/app.js +94 -0
- package/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
- package/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
- package/templates/native-go-better-auth/node/app.mjs +72 -0
- package/templates/native-go-better-auth/node/build.mjs +2 -0
- package/templates/native-go-better-auth/node/package-lock.json +862 -0
- package/templates/native-go-better-auth/node/package.json +1 -0
- package/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
- package/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
- package/templates/native-go-integration/backend/internal/app/personal.go +203 -0
- package/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
- package/templates/native-go-root/backend/internal/app/root.go +107 -0
- package/templates/native-go-root/backend/internal/app/root_test.go +124 -0
- package/templates/native-go-root/root/whoami.sh +6 -0
- package/templates/native-go-vue/README.md +110 -0
- package/templates/native-go-vue/frontend/DESIGN.md +78 -0
- package/templates/native-go-vue/frontend/package-lock.json +2507 -0
- package/templates/native-go-vue/frontend/package.json +6 -0
- package/templates/native-go-vue/frontend/src/App.vue +132 -0
- package/templates/native-go-vue/frontend/src/context.ts +17 -0
- package/templates/native-go-vue/frontend/src/main.ts +26 -0
- package/templates/native-go-vue/frontend/src/style.css +14 -0
- package/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
- package/templates/native-go-vue/frontend/tsconfig.json +1 -0
- package/templates/native-go-vue/frontend/vite.config.ts +8 -0
- package/templates/native-go-vue/gitignore +2 -0
- package/dist/attestation/app-package.d.ts +0 -86
- package/dist/attestation/app-package.js +0 -206
- package/dist/attestation/install-on-host.d.ts +0 -14
- package/dist/attestation/install-on-host.js +0 -52
- package/dist/attestation/keyring.d.ts +0 -21
- package/dist/attestation/keyring.js +0 -168
- package/dist/attestation/platform-service.d.ts +0 -70
- package/dist/attestation/platform-service.js +0 -250
- package/dist/build-spk/broker-glue.d.ts +0 -16
- package/dist/build-spk/broker-glue.js +0 -40
- package/dist/deploy/catalog-publish.d.ts +0 -3
- package/dist/deploy/catalog-publish.js +0 -7
- package/dist/doctor.d.ts +0 -22
- package/dist/doctor.js +0 -822
- package/dist/error-collector.d.ts +0 -2
- package/dist/error-collector.js +0 -46
- package/dist/host/broker-paths.d.ts +0 -1
- package/dist/host/broker-paths.js +0 -1
- package/dist/host/broker-upgrade-preflight.d.ts +0 -61
- package/dist/host/broker-upgrade-preflight.js +0 -156
- package/dist/host/digests.d.ts +0 -5
- package/dist/host/digests.js +0 -63
- package/dist/host/fake-nas.d.ts +0 -49
- package/dist/host/fake-nas.js +0 -193
- package/dist/mock-broker/bootstrap.d.ts +0 -2
- package/dist/mock-broker/bootstrap.js +0 -8
- package/dist/mock-broker/files.d.ts +0 -60
- package/dist/mock-broker/files.js +0 -408
- package/dist/mock-broker/oidc.d.ts +0 -31
- package/dist/mock-broker/oidc.js +0 -161
- package/dist/mock-broker/path.d.ts +0 -9
- package/dist/mock-broker/path.js +0 -50
- package/dist/mock-broker/server.d.ts +0 -19
- package/dist/mock-broker/server.js +0 -350
- package/dist/publish/build-ledger.d.ts +0 -22
- package/dist/publish/build-ledger.js +0 -26
- package/dist/publish/community-path.d.ts +0 -6
- package/dist/publish/community-path.js +0 -41
- package/dist/publish/curated-release.d.ts +0 -78
- package/dist/publish/curated-release.js +0 -883
- package/dist/publish/file-build-ledger.d.ts +0 -18
- package/dist/publish/file-build-ledger.js +0 -268
- package/dist/publish/github-pr.d.ts +0 -25
- package/dist/publish/github-pr.js +0 -85
- package/dist/publish/github-repo.d.ts +0 -8
- package/dist/publish/github-repo.js +0 -21
- package/dist/publish/platform-service-release.d.ts +0 -60
- package/dist/publish/platform-service-release.js +0 -333
- package/dist/publish/preflight.d.ts +0 -24
- package/dist/publish/preflight.js +0 -138
- package/dist/publish/publish.d.ts +0 -27
- package/dist/publish/publish.js +0 -136
- package/dist/publish/sync-definition.d.ts +0 -4
- package/dist/publish/sync-definition.js +0 -48
- package/dist/release-bin.d.ts +0 -2
- package/dist/release-bin.js +0 -23
- package/dist/templates/hello-web/html/sdk.js +0 -68
- package/dist/templates/hello-web-page/html/sdk.js +0 -68
- package/dist/templates/native/synology-app.yaml +0 -7
- package/dist/test-host.d.ts +0 -15
- package/dist/test-host.js +0 -37
- package/templates/hello-web/html/sdk.js +0 -68
- package/templates/hello-web-page/html/sdk.js +0 -68
- package/templates/native/synology-app.yaml +0 -7
- /package/dist/templates/{native → binary}/native.yaml +0 -0
- /package/templates/{native → binary}/native.yaml +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
package helper
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"crypto/rand"
|
|
6
|
+
"encoding/hex"
|
|
7
|
+
"encoding/json"
|
|
8
|
+
"errors"
|
|
9
|
+
"io"
|
|
10
|
+
"net"
|
|
11
|
+
"os"
|
|
12
|
+
"os/exec"
|
|
13
|
+
"path/filepath"
|
|
14
|
+
"runtime"
|
|
15
|
+
"strconv"
|
|
16
|
+
"syscall"
|
|
17
|
+
"time"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
func setParentDeath() error {
|
|
21
|
+
_, _, err := syscall.Syscall6(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0, 0, 0, 0)
|
|
22
|
+
if err != 0 {
|
|
23
|
+
return err
|
|
24
|
+
}
|
|
25
|
+
return nil
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type ownedLinuxChild struct {
|
|
29
|
+
process *os.Process
|
|
30
|
+
reaped bool
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func (c *ownedLinuxChild) exited() (bool, error) {
|
|
34
|
+
if c.reaped {
|
|
35
|
+
return true, nil
|
|
36
|
+
}
|
|
37
|
+
var status syscall.WaitStatus
|
|
38
|
+
pid, err := syscall.Wait4(c.process.Pid, &status, syscall.WNOHANG, nil)
|
|
39
|
+
if errors.Is(err, syscall.EINTR) {
|
|
40
|
+
return false, nil
|
|
41
|
+
}
|
|
42
|
+
if err != nil {
|
|
43
|
+
return false, err
|
|
44
|
+
} // ECHILD never permits signalling.
|
|
45
|
+
if pid == c.process.Pid {
|
|
46
|
+
c.reaped = true
|
|
47
|
+
c.process.Release()
|
|
48
|
+
}
|
|
49
|
+
return c.reaped, nil
|
|
50
|
+
}
|
|
51
|
+
func (c *ownedLinuxChild) signal(s syscall.Signal) error {
|
|
52
|
+
if c.reaped {
|
|
53
|
+
return errTrust
|
|
54
|
+
}
|
|
55
|
+
// This PID came from our own Start, and ONLY this event loop calls Wait4.
|
|
56
|
+
// Even if it exits here it remains an unreaped zombie, pinning its PID.
|
|
57
|
+
return syscall.Kill(c.process.Pid, s)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type supervisorRequest struct {
|
|
61
|
+
Process Process `json:"process"`
|
|
62
|
+
Op string `json:"op"`
|
|
63
|
+
Nonce string `json:"nonce"`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The thread creating the child must remain alive: PDEATHSIG is tied to that
|
|
67
|
+
// Linux thread, not merely the supervisor's thread group / Go process.
|
|
68
|
+
func superviseLinux(c Client, binary string) error {
|
|
69
|
+
runtime.LockOSThread()
|
|
70
|
+
defer runtime.UnlockOSThread()
|
|
71
|
+
r, err := readRecord(c.StateDir, 0)
|
|
72
|
+
if err != nil || !r.Enabled || r.Inventory != c.Expected {
|
|
73
|
+
return errTrust
|
|
74
|
+
}
|
|
75
|
+
identity, err := inspectSupervisor(os.Getpid(), c.Expected)
|
|
76
|
+
if err != nil {
|
|
77
|
+
return err
|
|
78
|
+
}
|
|
79
|
+
if err := writeProcess(c.RunDir, identity); err != nil {
|
|
80
|
+
return err
|
|
81
|
+
}
|
|
82
|
+
socket := filepath.Join(c.RunDir, "supervisor.sock")
|
|
83
|
+
l, err := net.ListenUnix("unix", &net.UnixAddr{Name: socket, Net: "unix"})
|
|
84
|
+
if err != nil {
|
|
85
|
+
return err
|
|
86
|
+
}
|
|
87
|
+
l.SetUnlinkOnClose(false)
|
|
88
|
+
defer l.Close()
|
|
89
|
+
if err := os.Chmod(socket, 0600); err != nil {
|
|
90
|
+
return err
|
|
91
|
+
}
|
|
92
|
+
cmd := exec.Command(binary, "serve-child")
|
|
93
|
+
cmd.Env = []string{"PATH=/usr/sbin:/usr/bin:/sbin:/bin", "SYNOPKGLAND_PARENT=" + strconv.Itoa(os.Getpid())}
|
|
94
|
+
// Go sets prctl before exec and rechecks the parent immediately afterward.
|
|
95
|
+
cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL}
|
|
96
|
+
if err := cmd.Start(); err != nil {
|
|
97
|
+
return err
|
|
98
|
+
}
|
|
99
|
+
child := &ownedLinuxChild{process: cmd.Process}
|
|
100
|
+
// No cmd.Wait goroutine: reaping and signalling are serialized below.
|
|
101
|
+
for {
|
|
102
|
+
if _, err := child.exited(); err != nil {
|
|
103
|
+
return err
|
|
104
|
+
}
|
|
105
|
+
l.SetDeadline(time.Now().Add(20 * time.Millisecond))
|
|
106
|
+
conn, err := l.AcceptUnix()
|
|
107
|
+
if err != nil {
|
|
108
|
+
if e, ok := err.(net.Error); ok && e.Timeout() {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
return err
|
|
112
|
+
}
|
|
113
|
+
conn.SetDeadline(time.Now().Add(3 * time.Second))
|
|
114
|
+
peer, err := peerCredentials(conn)
|
|
115
|
+
if err != nil || peer.Uid != 0 {
|
|
116
|
+
conn.Close()
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
var req supervisorRequest
|
|
120
|
+
d := json.NewDecoder(io.LimitReader(conn, 4097))
|
|
121
|
+
d.DisallowUnknownFields()
|
|
122
|
+
if d.Decode(&req) != nil || req.Process != identity || req.Op != "stop" || len(req.Nonce) != 64 || !digestPattern.MatchString(req.Nonce) {
|
|
123
|
+
conn.Close()
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
127
|
+
err = stopChild(ctx, child, time.Second)
|
|
128
|
+
cancel()
|
|
129
|
+
// Kernel process-exit fd cleanup is not ordered: close the listener
|
|
130
|
+
// before ACK, so connection EOF cannot race a still-listening socket.
|
|
131
|
+
if err == nil {
|
|
132
|
+
err = l.Close()
|
|
133
|
+
}
|
|
134
|
+
res := Response{Inventory: c.Expected, Nonce: req.Nonce, OK: err == nil}
|
|
135
|
+
if err != nil {
|
|
136
|
+
res.Error = "cleanup-required"
|
|
137
|
+
}
|
|
138
|
+
json.NewEncoder(conn).Encode(res)
|
|
139
|
+
if err == nil {
|
|
140
|
+
// Keep this connection open until OS exit. An ACK plus EOF proves both
|
|
141
|
+
// that the child was reaped and this supervisor finished cooperatively.
|
|
142
|
+
os.Exit(0)
|
|
143
|
+
}
|
|
144
|
+
conn.Close()
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
func (p LinuxProcesses) Stop(want Process) error {
|
|
149
|
+
if want.Mode != "supervised" {
|
|
150
|
+
return errTrust
|
|
151
|
+
}
|
|
152
|
+
// Loss/reboot/PID reuse is not sufficient proof of child teardown. Preserve
|
|
153
|
+
// the record for fixed admin repair; never signal this recorded PID.
|
|
154
|
+
actual, err := inspectSupervisor(want.PID, want.Inventory)
|
|
155
|
+
if err != nil || actual != want {
|
|
156
|
+
return errTrust
|
|
157
|
+
}
|
|
158
|
+
socket := filepath.Join(p.RunDir, "supervisor.sock")
|
|
159
|
+
st, err := os.Lstat(socket)
|
|
160
|
+
if err != nil {
|
|
161
|
+
return err
|
|
162
|
+
}
|
|
163
|
+
stat, ok := st.Sys().(*syscall.Stat_t)
|
|
164
|
+
if !ok || stat.Uid != 0 || st.Mode()&os.ModeSocket == 0 || st.Mode().Perm() != 0600 {
|
|
165
|
+
return errTrust
|
|
166
|
+
}
|
|
167
|
+
conn, err := net.DialTimeout("unix", socket, time.Second)
|
|
168
|
+
if err != nil {
|
|
169
|
+
return err
|
|
170
|
+
}
|
|
171
|
+
defer conn.Close()
|
|
172
|
+
peer, err := peerCredentials(conn.(*net.UnixConn))
|
|
173
|
+
if err != nil || peer.Uid != 0 || int(peer.Pid) != want.PID {
|
|
174
|
+
return errTrust
|
|
175
|
+
}
|
|
176
|
+
// Recheck after binding the connection too. A connected socket cannot be
|
|
177
|
+
// substituted by a later process that reused the path or PID.
|
|
178
|
+
actual, err = inspectSupervisor(want.PID, want.Inventory)
|
|
179
|
+
if err != nil || actual != want {
|
|
180
|
+
return errTrust
|
|
181
|
+
}
|
|
182
|
+
conn.SetDeadline(time.Now().Add(3 * time.Second))
|
|
183
|
+
var nonce [32]byte
|
|
184
|
+
if _, err := rand.Read(nonce[:]); err != nil {
|
|
185
|
+
return err
|
|
186
|
+
}
|
|
187
|
+
req := supervisorRequest{Process: want, Op: "stop", Nonce: hex.EncodeToString(nonce[:])}
|
|
188
|
+
if err := json.NewEncoder(conn).Encode(req); err != nil {
|
|
189
|
+
return err
|
|
190
|
+
}
|
|
191
|
+
return readSupervisorReply(conn, want.Inventory, req.Nonce)
|
|
192
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
package helper
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
"net"
|
|
7
|
+
"os"
|
|
8
|
+
"os/exec"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"strconv"
|
|
11
|
+
"strings"
|
|
12
|
+
"syscall"
|
|
13
|
+
"testing"
|
|
14
|
+
"time"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// This opt-in fixture uses a root-only, disposable Linux VM and an independently
|
|
18
|
+
// cross-compiled cmd/helper. Never creates accounts or accesses a NAS.
|
|
19
|
+
func linuxFixture(t *testing.T) LinuxProcesses {
|
|
20
|
+
t.Helper()
|
|
21
|
+
source := os.Getenv("M1C_HELPER_FIXTURE")
|
|
22
|
+
if source == "" {
|
|
23
|
+
t.Skip("requires disposable Linux root VM and M1C_HELPER_FIXTURE")
|
|
24
|
+
}
|
|
25
|
+
if os.Geteuid() != 0 {
|
|
26
|
+
t.Fatal("VM fixture requires root")
|
|
27
|
+
}
|
|
28
|
+
digest, err := executableDigest(source)
|
|
29
|
+
if err != nil {
|
|
30
|
+
t.Fatal(err)
|
|
31
|
+
}
|
|
32
|
+
i := Inventory{Package: "root", Version: "1.0.0-0001", Protocol: 1, PayloadDigest: digest, RegistryDigest: strings.Repeat("b", 64)}
|
|
33
|
+
c := NewClient(i)
|
|
34
|
+
for _, dir := range []string{c.StateDir, c.RunDir} {
|
|
35
|
+
if err := os.Mkdir(dir, 0700); err != nil {
|
|
36
|
+
t.Fatal(err)
|
|
37
|
+
}
|
|
38
|
+
t.Cleanup(func() { os.RemoveAll(dir) })
|
|
39
|
+
}
|
|
40
|
+
exe := filepath.Join(c.StateDir, "payload-"+digest)
|
|
41
|
+
b, err := os.ReadFile(source)
|
|
42
|
+
if err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
if err := os.WriteFile(exe, b, 0700); err != nil {
|
|
46
|
+
t.Fatal(err)
|
|
47
|
+
}
|
|
48
|
+
return LinuxProcesses{Client: c, Binary: exe}
|
|
49
|
+
}
|
|
50
|
+
func startVMHelper(t *testing.T, p LinuxProcesses) Process {
|
|
51
|
+
t.Helper()
|
|
52
|
+
ctl := Controller{Client: p.Client, Processes: p}
|
|
53
|
+
if err := ctl.Control(context.Background(), "enable"); err != nil {
|
|
54
|
+
t.Fatal(err)
|
|
55
|
+
}
|
|
56
|
+
record, err := readProcess(p.RunDir, 0)
|
|
57
|
+
if err != nil {
|
|
58
|
+
t.Fatal(err)
|
|
59
|
+
}
|
|
60
|
+
t.Cleanup(func() { ctl.Control(context.Background(), "disable") })
|
|
61
|
+
return record
|
|
62
|
+
}
|
|
63
|
+
func supervisedPID(t *testing.T, pid int) int {
|
|
64
|
+
t.Helper()
|
|
65
|
+
paths, _ := filepath.Glob("/proc/" + strconv.Itoa(pid) + "/task/*/children")
|
|
66
|
+
for _, path := range paths {
|
|
67
|
+
b, _ := os.ReadFile(path)
|
|
68
|
+
for _, s := range strings.Fields(string(b)) {
|
|
69
|
+
child, _ := strconv.Atoi(s)
|
|
70
|
+
if child > 1 {
|
|
71
|
+
return child
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
t.Fatal("supervisor has no child")
|
|
76
|
+
return 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Observation only: an absent process or zombie has exited. No signal is sent
|
|
80
|
+
// to this PID, including if it has been reused. These fixtures run in a fresh VM.
|
|
81
|
+
func waitPIDExit(t *testing.T, pid int) {
|
|
82
|
+
t.Helper()
|
|
83
|
+
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
84
|
+
defer cancel()
|
|
85
|
+
if waitHealthy(ctx, func(context.Context) bool {
|
|
86
|
+
b, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat")
|
|
87
|
+
if os.IsNotExist(err) {
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
end := strings.LastIndex(string(b), ") ")
|
|
91
|
+
return err == nil && end >= 0 && strings.HasPrefix(string(b)[end+2:], "Z ")
|
|
92
|
+
}) != nil {
|
|
93
|
+
t.Fatal("owned process survived")
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
func TestLinuxActivationAlwaysUsesSupervisorAndNormalStop(t *testing.T) {
|
|
97
|
+
p := linuxFixture(t)
|
|
98
|
+
for n := 0; n < 20; n++ {
|
|
99
|
+
t.Run("cycle-"+strconv.Itoa(n), func(t *testing.T) {
|
|
100
|
+
proc := startVMHelper(t, p)
|
|
101
|
+
if proc.Mode != "supervised" {
|
|
102
|
+
t.Fatalf("activation did not select supervisor: %+v", proc)
|
|
103
|
+
}
|
|
104
|
+
child := supervisedPID(t, proc.PID)
|
|
105
|
+
if p.Status(context.Background()).State != "ready" {
|
|
106
|
+
t.Fatal("missing live handshake")
|
|
107
|
+
}
|
|
108
|
+
if err := (Controller{Client: p.Client, Processes: p}).Control(context.Background(), "disable"); err != nil {
|
|
109
|
+
t.Fatal(err)
|
|
110
|
+
}
|
|
111
|
+
waitPIDExit(t, child)
|
|
112
|
+
if err := p.CheckStopped(context.Background()); err != nil {
|
|
113
|
+
t.Fatal(err)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
func TestLinuxHelperSelfExitIsReapedWithoutSignallingReusedPID(t *testing.T) {
|
|
120
|
+
p := linuxFixture(t)
|
|
121
|
+
proc := startVMHelper(t, p)
|
|
122
|
+
if proc.Mode != "supervised" {
|
|
123
|
+
t.Fatal("not supervised")
|
|
124
|
+
}
|
|
125
|
+
child := supervisedPID(t, proc.PID)
|
|
126
|
+
// The VM-only helper fixture exits itself when this root-owned marker exists.
|
|
127
|
+
if err := os.WriteFile(filepath.Join(p.RunDir, "fixture-exit"), nil, 0600); err != nil {
|
|
128
|
+
t.Fatal(err)
|
|
129
|
+
}
|
|
130
|
+
waitPIDExit(t, child)
|
|
131
|
+
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
132
|
+
defer cancel()
|
|
133
|
+
if waitHealthy(ctx, func(context.Context) bool {
|
|
134
|
+
_, err := os.Stat("/proc/" + strconv.Itoa(child))
|
|
135
|
+
return os.IsNotExist(err)
|
|
136
|
+
}) != nil {
|
|
137
|
+
t.Fatal("supervisor did not reap")
|
|
138
|
+
}
|
|
139
|
+
if err := p.Stop(proc); err != nil {
|
|
140
|
+
t.Fatal(err)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
func TestLinuxSupervisorDeathKillsHelperAndPreservesFailedCleanup(t *testing.T) {
|
|
144
|
+
p := linuxFixture(t)
|
|
145
|
+
record(t, p.Client, Record{Inventory: p.Expected, Enabled: true})
|
|
146
|
+
// Keep the supervisor as our own unreaped child; only its parent signals it.
|
|
147
|
+
cmd := exec.Command(p.Binary, "supervise")
|
|
148
|
+
if err := cmd.Start(); err != nil {
|
|
149
|
+
t.Fatal(err)
|
|
150
|
+
}
|
|
151
|
+
owned := &ownedLinuxChild{process: cmd.Process}
|
|
152
|
+
t.Cleanup(func() { stopChild(context.Background(), owned, time.Millisecond) })
|
|
153
|
+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
154
|
+
err := waitHealthy(ctx, func(ctx context.Context) bool { return p.Status(ctx).State == "ready" })
|
|
155
|
+
cancel()
|
|
156
|
+
if err != nil {
|
|
157
|
+
t.Fatal(err)
|
|
158
|
+
}
|
|
159
|
+
proc, err := readProcess(p.RunDir, 0)
|
|
160
|
+
if err != nil || proc.Mode != "supervised" || proc.PID != cmd.Process.Pid {
|
|
161
|
+
t.Fatalf("%+v %v", proc, err)
|
|
162
|
+
}
|
|
163
|
+
child := supervisedPID(t, proc.PID)
|
|
164
|
+
if err := owned.signal(syscall.SIGKILL); err != nil {
|
|
165
|
+
t.Fatal(err)
|
|
166
|
+
}
|
|
167
|
+
waitPIDExit(t, child)
|
|
168
|
+
waitPIDExit(t, proc.PID)
|
|
169
|
+
ctl := Controller{Client: p.Client, Processes: p}
|
|
170
|
+
if ctl.Control(context.Background(), "teardown") == nil {
|
|
171
|
+
t.Fatal("lost supervisor falsely cleaned")
|
|
172
|
+
}
|
|
173
|
+
s := p.Status(context.Background())
|
|
174
|
+
if s.State != "failed" || s.Cleanup != "required" {
|
|
175
|
+
t.Fatal(s)
|
|
176
|
+
}
|
|
177
|
+
if ctl.Control(context.Background(), "disable") == nil {
|
|
178
|
+
t.Fatal("lost supervisor falsely disabled cleanly")
|
|
179
|
+
}
|
|
180
|
+
r, err := readRecord(p.StateDir, 0)
|
|
181
|
+
if err != nil || r.Enabled || !r.Residual {
|
|
182
|
+
t.Fatalf("%+v %v", r, err)
|
|
183
|
+
}
|
|
184
|
+
if _, err := os.Stat(filepath.Join(p.RunDir, "process.json")); err != nil {
|
|
185
|
+
t.Fatal("failure evidence removed")
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
func TestLinuxUnsupervisedRecordAndDirectServeRefuse(t *testing.T) {
|
|
189
|
+
p := linuxFixture(t)
|
|
190
|
+
proc := startVMHelper(t, p)
|
|
191
|
+
for _, mode := range []string{"", "unknown"} {
|
|
192
|
+
foreign := proc
|
|
193
|
+
foreign.Mode = mode
|
|
194
|
+
if p.Stop(foreign) == nil {
|
|
195
|
+
t.Fatal("unsupervised record accepted")
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Remove stopped sockets before testing the retired action, so a bind
|
|
199
|
+
// conflict cannot masquerade as refusal of unsupervised startup.
|
|
200
|
+
if err := (Controller{Client: p.Client, Processes: p}).Control(context.Background(), "teardown"); err != nil {
|
|
201
|
+
t.Fatal(err)
|
|
202
|
+
}
|
|
203
|
+
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
204
|
+
defer cancel()
|
|
205
|
+
cmd := exec.CommandContext(ctx, p.Binary, "serve")
|
|
206
|
+
if cmd.Run() == nil || ctx.Err() != nil {
|
|
207
|
+
t.Fatal("direct unsupervised serve accepted")
|
|
208
|
+
}
|
|
209
|
+
if _, err := os.Stat(filepath.Join(p.RunDir, "helper.sock")); !os.IsNotExist(err) {
|
|
210
|
+
t.Fatal("direct serve opened socket")
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
func TestLinuxOwnedChildSelfExitAndECHILDRefusal(t *testing.T) {
|
|
215
|
+
for _, externallyReaped := range []bool{false, true} {
|
|
216
|
+
cmd := exec.Command("/bin/sh", "-c", "exit 0")
|
|
217
|
+
if err := cmd.Start(); err != nil {
|
|
218
|
+
t.Fatal(err)
|
|
219
|
+
}
|
|
220
|
+
child := &ownedLinuxChild{process: cmd.Process}
|
|
221
|
+
if externallyReaped {
|
|
222
|
+
if err := cmd.Wait(); err != nil {
|
|
223
|
+
t.Fatal(err)
|
|
224
|
+
}
|
|
225
|
+
if err := stopChild(context.Background(), child, time.Millisecond); !errors.Is(err, syscall.ECHILD) {
|
|
226
|
+
t.Fatal("ECHILD did not refuse", err)
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
230
|
+
err := waitHealthy(ctx, func(context.Context) bool { done, err := child.exited(); return done && err == nil })
|
|
231
|
+
cancel()
|
|
232
|
+
if err != nil {
|
|
233
|
+
t.Fatal(err)
|
|
234
|
+
}
|
|
235
|
+
if err := stopChild(context.Background(), child, time.Millisecond); err != nil {
|
|
236
|
+
t.Fatal(err)
|
|
237
|
+
}
|
|
238
|
+
if child.signal(syscall.SIGTERM) == nil {
|
|
239
|
+
t.Fatal("reaped PID signalled")
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
func TestLinuxSupervisorIdentityConflictsAndSocketSubstitutionRefuse(t *testing.T) {
|
|
245
|
+
p := linuxFixture(t)
|
|
246
|
+
proc := startVMHelper(t, p)
|
|
247
|
+
for _, field := range []string{"boot", "start", "inode", "digest", "mode"} {
|
|
248
|
+
foreign := proc
|
|
249
|
+
switch field {
|
|
250
|
+
case "boot":
|
|
251
|
+
foreign.Boot = "reboot"
|
|
252
|
+
case "start":
|
|
253
|
+
foreign.Start = "reuse"
|
|
254
|
+
case "inode":
|
|
255
|
+
foreign.Inode++
|
|
256
|
+
case "digest":
|
|
257
|
+
foreign.Inventory.PayloadDigest = strings.Repeat("c", 64)
|
|
258
|
+
case "mode":
|
|
259
|
+
foreign.Mode = "unknown"
|
|
260
|
+
}
|
|
261
|
+
if p.Stop(foreign) == nil {
|
|
262
|
+
t.Fatal("foreign supervisor accepted", field)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
socket := filepath.Join(p.RunDir, "supervisor.sock")
|
|
266
|
+
old := socket + ".owned"
|
|
267
|
+
if err := os.Rename(socket, old); err != nil {
|
|
268
|
+
t.Fatal(err)
|
|
269
|
+
}
|
|
270
|
+
fake, err := net.ListenUnix("unix", &net.UnixAddr{Name: socket, Net: "unix"})
|
|
271
|
+
if err != nil {
|
|
272
|
+
t.Fatal(err)
|
|
273
|
+
}
|
|
274
|
+
if err := os.Chmod(socket, 0600); err != nil {
|
|
275
|
+
t.Fatal(err)
|
|
276
|
+
}
|
|
277
|
+
if p.Stop(proc) == nil {
|
|
278
|
+
t.Fatal("other root PID substituted supervisor")
|
|
279
|
+
}
|
|
280
|
+
fake.Close()
|
|
281
|
+
if err := os.Rename(old, socket); err != nil {
|
|
282
|
+
t.Fatal(err)
|
|
283
|
+
}
|
|
284
|
+
if p.Status(context.Background()).State != "ready" {
|
|
285
|
+
t.Fatal("refusal stopped real helper")
|
|
286
|
+
}
|
|
287
|
+
if err := p.Stop(proc); err != nil {
|
|
288
|
+
t.Fatal(err)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
func TestLinuxReparentedChildRefusesBeforeSocket(t *testing.T) {
|
|
292
|
+
p := linuxFixture(t)
|
|
293
|
+
record(t, p.Client, Record{Inventory: p.Expected, Enabled: true})
|
|
294
|
+
cmd := exec.Command(p.Binary, "serve-child")
|
|
295
|
+
cmd.Env = []string{"PATH=/usr/sbin:/usr/bin:/sbin:/bin", "SYNOPKGLAND_PARENT=1"}
|
|
296
|
+
if cmd.Run() == nil {
|
|
297
|
+
t.Fatal("already reparented child served")
|
|
298
|
+
}
|
|
299
|
+
if _, err := os.Stat(filepath.Join(p.RunDir, "helper.sock")); !os.IsNotExist(err) {
|
|
300
|
+
t.Fatal("unsafe child opened socket")
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Standalone native DSM package: an unsigned SPK that runs as its own package
|
|
2
|
+
# `synopkgland build-spk` reads this file.
|
|
3
|
+
schema: 1
|
|
4
|
+
|
|
5
|
+
# INFO.package: letters and digits, starting with a letter. It also names the
|
|
6
|
+
# DSM app (SYNO.SDS.<package>) and the CGI URL /webman/3rdparty/<package>/.
|
|
7
|
+
package: __PACKAGE__
|
|
8
|
+
version: 0.1.0
|
|
9
|
+
# Increase for every SPK you hand out with the same version (INFO 0.1.0-0001).
|
|
10
|
+
build: 1
|
|
11
|
+
displayname: __DISPLAY_NAME__
|
|
12
|
+
description: A native DSM package with a Go backend and a DSM window UI.
|
|
13
|
+
maintainer: Your Name
|
|
14
|
+
os_min_ver: "7.2-64570"
|
|
15
|
+
|
|
16
|
+
# One SPK per architecture. Supported: x86_64, aarch64.
|
|
17
|
+
arches: [x86_64, aarch64]
|
|
18
|
+
|
|
19
|
+
backend:
|
|
20
|
+
# Build the Go module in ./backend (package ./cmd/app) as the CGI.
|
|
21
|
+
go:
|
|
22
|
+
dir: backend
|
|
23
|
+
main: ./cmd/app
|
|
24
|
+
# Or ship a CGI executable you already have (remove `go:` above). Each file
|
|
25
|
+
# must be a static Linux ELF for its arch; it then owns authentication itself.
|
|
26
|
+
# binary:
|
|
27
|
+
# x86_64: prebuilt/api-x86_64
|
|
28
|
+
# aarch64: prebuilt/api-aarch64
|
|
29
|
+
|
|
30
|
+
ui:
|
|
31
|
+
# Static files served at /webman/3rdparty/<package>/app/. app.js and app.css
|
|
32
|
+
# are required; anything else (images, chunks) is copied as is.
|
|
33
|
+
dir: ui
|
|
34
|
+
|
|
35
|
+
# UI languages: i18n/messages/<locale>.json for en, zh-TW, zh-CN, ja, ko, de,
|
|
36
|
+
# fr, es and it (DSM's other languages fall back to English). build-spk checks
|
|
37
|
+
# that every language has the en.json keys and placeholders, and that the keys
|
|
38
|
+
# listed in the glossary use Synology's own DSM wording.
|
|
39
|
+
i18n:
|
|
40
|
+
messages: i18n/messages
|
|
41
|
+
glossary: i18n/glossary.json
|
|
42
|
+
# Package Center text per language (INFO displayname_<lang>/description_<lang>).
|
|
43
|
+
# English comes from displayname/description above.
|
|
44
|
+
info:
|
|
45
|
+
zh-TW: { description: 以 Go 後端與 DSM 視窗介面打造的原生 DSM 套件。 }
|
|
46
|
+
zh-CN: { description: 以 Go 后端与 DSM 窗口界面构建的原生 DSM 套件。 }
|
|
47
|
+
ja: { description: Go バックエンドと DSM ウィンドウ UI を備えたネイティブ DSM パッケージ。 }
|
|
48
|
+
ko: { description: Go 백엔드와 DSM 창 UI를 갖춘 네이티브 DSM 패키지. }
|
|
49
|
+
de: { description: Ein natives DSM-Paket mit Go-Backend und DSM-Fenster-Oberfläche. }
|
|
50
|
+
fr: { description: Un paquet DSM natif avec un backend Go et une interface en fenêtre DSM. }
|
|
51
|
+
es: { description: Un paquete nativo de DSM con backend en Go e interfaz en ventana de DSM. }
|
|
52
|
+
it: { description: Un pacchetto DSM nativo con backend Go e interfaccia in una finestra DSM. }
|
|
53
|
+
|
|
54
|
+
# none: everything runs as the package account.
|
|
55
|
+
# root: also fixed root operations, one root/<name>.sh each, that run only
|
|
56
|
+
# after a DSM administrator approves them in the package window.
|
|
57
|
+
# See README.md "Root access" before using it.
|
|
58
|
+
privilege: __PRIVILEGE__
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dist/
|