@synopackageland/cli 0.3.9 → 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.
Files changed (286) hide show
  1. package/README.md +20 -9
  2. package/dist/bin.js +1 -2
  3. package/dist/build-spk/generator.d.ts +0 -3
  4. package/dist/build-spk/generator.js +135 -287
  5. package/dist/build-spk/native-lifecycle.js +0 -3
  6. package/dist/build-spk/native-payload.js +0 -1
  7. package/dist/cli.js +171 -151
  8. package/dist/compose/analyze.d.ts +1 -2
  9. package/dist/compose/analyze.js +5 -48
  10. package/dist/contract/unsupported-types.d.ts +10 -0
  11. package/dist/contract/unsupported-types.js +49 -0
  12. package/dist/contract/validate.js +25 -2
  13. package/dist/contract/web-launch.js +0 -2
  14. package/dist/deploy/deploy.d.ts +1 -4
  15. package/dist/deploy/deploy.js +57 -156
  16. package/dist/deploy/homes-values.js +1 -1
  17. package/dist/deploy/plan.d.ts +18 -0
  18. package/dist/deploy/plan.js +54 -0
  19. package/dist/deploy/port-preflight.d.ts +21 -0
  20. package/dist/deploy/port-preflight.js +42 -0
  21. package/dist/deploy/spk-version.d.ts +2 -14
  22. package/dist/deploy/spk-version.js +25 -111
  23. package/dist/dev.d.ts +1 -3
  24. package/dist/dev.js +14 -28
  25. package/dist/error-registry.js +810 -857
  26. package/dist/host/dsm-client.js +23 -263
  27. package/dist/host/dsm-port-profile.d.ts +47 -0
  28. package/dist/host/dsm-port-profile.js +206 -0
  29. package/dist/host/port-info-lookup.d.ts +9 -0
  30. package/dist/host/port-info-lookup.js +87 -0
  31. package/dist/host/port-info.d.ts +33 -0
  32. package/dist/host/port-info.js +66 -0
  33. package/dist/host/types.d.ts +6 -78
  34. package/dist/index.d.ts +0 -9
  35. package/dist/index.js +0 -6
  36. package/dist/info.d.ts +0 -1
  37. package/dist/info.js +3 -10
  38. package/dist/init.d.ts +11 -1
  39. package/dist/init.js +112 -4
  40. package/dist/log.js +2 -3
  41. package/dist/native/validate.js +0 -1
  42. package/dist/standalone/binary.d.ts +9 -0
  43. package/dist/standalone/binary.js +67 -0
  44. package/dist/standalone/build.d.ts +25 -0
  45. package/dist/standalone/build.js +108 -0
  46. package/dist/standalone/dsm-window.d.ts +26 -0
  47. package/dist/standalone/dsm-window.js +251 -0
  48. package/dist/standalone/helper.d.ts +31 -0
  49. package/dist/standalone/helper.js +104 -0
  50. package/dist/standalone/i18n.d.ts +28 -0
  51. package/dist/standalone/i18n.js +161 -0
  52. package/dist/standalone/inspect-spk.d.ts +21 -0
  53. package/dist/standalone/inspect-spk.js +160 -0
  54. package/dist/standalone/manifest.d.ts +71 -0
  55. package/dist/standalone/manifest.js +276 -0
  56. package/dist/standalone/root-messages.d.ts +7 -0
  57. package/dist/standalone/root-messages.js +250 -0
  58. package/dist/standalone/root-ops.d.ts +42 -0
  59. package/dist/standalone/root-ops.js +101 -0
  60. package/dist/standalone/root-window.d.ts +35 -0
  61. package/dist/standalone/root-window.js +290 -0
  62. package/dist/standalone/spk.d.ts +22 -0
  63. package/dist/standalone/spk.js +205 -0
  64. package/dist/templates/binary/synology-app.yaml +7 -0
  65. package/dist/templates/hello-files/compose.yaml +0 -1
  66. package/dist/templates/hello-web/compose.yaml +13 -10
  67. package/dist/templates/hello-web/html/app.js +1 -23
  68. package/dist/templates/hello-web/html/index.html +1 -3
  69. package/dist/templates/hello-web-page/compose.yaml +13 -10
  70. package/dist/templates/hello-web-page/html/app.js +1 -23
  71. package/dist/templates/hello-web-page/html/index.html +1 -3
  72. package/dist/templates/native-go/README.md +268 -0
  73. package/dist/templates/native-go/backend/cmd/app/main.go +49 -0
  74. package/dist/templates/native-go/backend/cmd/helper/main.go +38 -0
  75. package/dist/templates/native-go/backend/go.mod +3 -0
  76. package/dist/templates/native-go/backend/internal/api/errors.go +33 -0
  77. package/dist/templates/native-go/backend/internal/api/registry.go +94 -0
  78. package/dist/templates/native-go/backend/internal/api/registry_test.go +329 -0
  79. package/dist/templates/native-go/backend/internal/api/server.go +155 -0
  80. package/dist/templates/native-go/backend/internal/app/root_none.go +9 -0
  81. package/dist/templates/native-go/backend/internal/app/routes.go +127 -0
  82. package/dist/templates/native-go/backend/internal/app/routes_test.go +167 -0
  83. package/dist/templates/native-go/backend/internal/dsm/auth.go +118 -0
  84. package/dist/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
  85. package/dist/templates/native-go/backend/internal/helper/control.go +219 -0
  86. package/dist/templates/native-go/backend/internal/helper/control_test.go +242 -0
  87. package/dist/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
  88. package/dist/templates/native-go/backend/internal/helper/peer_other.go +7 -0
  89. package/dist/templates/native-go/backend/internal/helper/process_linux.go +167 -0
  90. package/dist/templates/native-go/backend/internal/helper/process_other.go +5 -0
  91. package/dist/templates/native-go/backend/internal/helper/server_linux.go +83 -0
  92. package/dist/templates/native-go/backend/internal/helper/status.go +271 -0
  93. package/dist/templates/native-go/backend/internal/helper/status_test.go +205 -0
  94. package/dist/templates/native-go/backend/internal/helper/supervision.go +84 -0
  95. package/dist/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
  96. package/dist/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
  97. package/dist/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
  98. package/dist/templates/native-go/dsm-package.yaml +58 -0
  99. package/dist/templates/native-go/gitignore +1 -0
  100. package/dist/templates/native-go/i18n/glossary.json +662 -0
  101. package/dist/templates/native-go/i18n/messages/de.json +28 -0
  102. package/dist/templates/native-go/i18n/messages/en.json +28 -0
  103. package/dist/templates/native-go/i18n/messages/es.json +28 -0
  104. package/dist/templates/native-go/i18n/messages/fr.json +28 -0
  105. package/dist/templates/native-go/i18n/messages/it.json +28 -0
  106. package/dist/templates/native-go/i18n/messages/ja.json +28 -0
  107. package/dist/templates/native-go/i18n/messages/ko.json +28 -0
  108. package/dist/templates/native-go/i18n/messages/zh-CN.json +28 -0
  109. package/dist/templates/native-go/i18n/messages/zh-TW.json +28 -0
  110. package/dist/templates/native-go/ui/app.css +35 -0
  111. package/dist/templates/native-go/ui/app.js +94 -0
  112. package/dist/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
  113. package/dist/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
  114. package/dist/templates/native-go-better-auth/node/app.mjs +72 -0
  115. package/dist/templates/native-go-better-auth/node/build.mjs +2 -0
  116. package/dist/templates/native-go-better-auth/node/package-lock.json +862 -0
  117. package/dist/templates/native-go-better-auth/node/package.json +1 -0
  118. package/dist/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
  119. package/dist/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
  120. package/dist/templates/native-go-integration/backend/internal/app/personal.go +203 -0
  121. package/dist/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
  122. package/dist/templates/native-go-root/backend/internal/app/root.go +107 -0
  123. package/dist/templates/native-go-root/backend/internal/app/root_test.go +124 -0
  124. package/dist/templates/native-go-root/root/whoami.sh +6 -0
  125. package/dist/templates/native-go-vue/README.md +110 -0
  126. package/dist/templates/native-go-vue/frontend/DESIGN.md +78 -0
  127. package/dist/templates/native-go-vue/frontend/package-lock.json +2507 -0
  128. package/dist/templates/native-go-vue/frontend/package.json +6 -0
  129. package/dist/templates/native-go-vue/frontend/src/App.vue +132 -0
  130. package/dist/templates/native-go-vue/frontend/src/context.ts +17 -0
  131. package/dist/templates/native-go-vue/frontend/src/main.ts +26 -0
  132. package/dist/templates/native-go-vue/frontend/src/style.css +14 -0
  133. package/dist/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
  134. package/dist/templates/native-go-vue/frontend/tsconfig.json +1 -0
  135. package/dist/templates/native-go-vue/frontend/vite.config.ts +8 -0
  136. package/dist/templates/native-go-vue/gitignore +2 -0
  137. package/dist/types.d.ts +1 -1
  138. package/dist/validate.js +11 -7
  139. package/package.json +3 -7
  140. package/schema/synology-app.schema.json +36 -37
  141. package/skills/SKILL.md +4 -48
  142. package/skills/reference/create.md +11 -83
  143. package/skills/reference/deploy.md +7 -78
  144. package/skills/reference/diagnose.md +4 -101
  145. package/skills/reference/files.md +5 -41
  146. package/skills/reference/identity.md +3 -29
  147. package/skills/reference/wrap.md +0 -2
  148. package/templates/binary/synology-app.yaml +7 -0
  149. package/templates/hello-files/compose.yaml +0 -1
  150. package/templates/hello-web/compose.yaml +13 -10
  151. package/templates/hello-web/html/app.js +1 -23
  152. package/templates/hello-web/html/index.html +1 -3
  153. package/templates/hello-web-page/compose.yaml +13 -10
  154. package/templates/hello-web-page/html/app.js +1 -23
  155. package/templates/hello-web-page/html/index.html +1 -3
  156. package/templates/native-go/README.md +268 -0
  157. package/templates/native-go/backend/cmd/app/main.go +49 -0
  158. package/templates/native-go/backend/cmd/helper/main.go +38 -0
  159. package/templates/native-go/backend/go.mod +3 -0
  160. package/templates/native-go/backend/internal/api/errors.go +33 -0
  161. package/templates/native-go/backend/internal/api/registry.go +94 -0
  162. package/templates/native-go/backend/internal/api/registry_test.go +329 -0
  163. package/templates/native-go/backend/internal/api/server.go +155 -0
  164. package/templates/native-go/backend/internal/app/root_none.go +9 -0
  165. package/templates/native-go/backend/internal/app/routes.go +127 -0
  166. package/templates/native-go/backend/internal/app/routes_test.go +167 -0
  167. package/templates/native-go/backend/internal/dsm/auth.go +118 -0
  168. package/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
  169. package/templates/native-go/backend/internal/helper/control.go +219 -0
  170. package/templates/native-go/backend/internal/helper/control_test.go +242 -0
  171. package/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
  172. package/templates/native-go/backend/internal/helper/peer_other.go +7 -0
  173. package/templates/native-go/backend/internal/helper/process_linux.go +167 -0
  174. package/templates/native-go/backend/internal/helper/process_other.go +5 -0
  175. package/templates/native-go/backend/internal/helper/server_linux.go +83 -0
  176. package/templates/native-go/backend/internal/helper/status.go +271 -0
  177. package/templates/native-go/backend/internal/helper/status_test.go +205 -0
  178. package/templates/native-go/backend/internal/helper/supervision.go +84 -0
  179. package/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
  180. package/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
  181. package/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
  182. package/templates/native-go/dsm-package.yaml +58 -0
  183. package/templates/native-go/gitignore +1 -0
  184. package/templates/native-go/i18n/glossary.json +662 -0
  185. package/templates/native-go/i18n/messages/de.json +28 -0
  186. package/templates/native-go/i18n/messages/en.json +28 -0
  187. package/templates/native-go/i18n/messages/es.json +28 -0
  188. package/templates/native-go/i18n/messages/fr.json +28 -0
  189. package/templates/native-go/i18n/messages/it.json +28 -0
  190. package/templates/native-go/i18n/messages/ja.json +28 -0
  191. package/templates/native-go/i18n/messages/ko.json +28 -0
  192. package/templates/native-go/i18n/messages/zh-CN.json +28 -0
  193. package/templates/native-go/i18n/messages/zh-TW.json +28 -0
  194. package/templates/native-go/ui/app.css +35 -0
  195. package/templates/native-go/ui/app.js +94 -0
  196. package/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
  197. package/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
  198. package/templates/native-go-better-auth/node/app.mjs +72 -0
  199. package/templates/native-go-better-auth/node/build.mjs +2 -0
  200. package/templates/native-go-better-auth/node/package-lock.json +862 -0
  201. package/templates/native-go-better-auth/node/package.json +1 -0
  202. package/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
  203. package/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
  204. package/templates/native-go-integration/backend/internal/app/personal.go +203 -0
  205. package/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
  206. package/templates/native-go-root/backend/internal/app/root.go +107 -0
  207. package/templates/native-go-root/backend/internal/app/root_test.go +124 -0
  208. package/templates/native-go-root/root/whoami.sh +6 -0
  209. package/templates/native-go-vue/README.md +110 -0
  210. package/templates/native-go-vue/frontend/DESIGN.md +78 -0
  211. package/templates/native-go-vue/frontend/package-lock.json +2507 -0
  212. package/templates/native-go-vue/frontend/package.json +6 -0
  213. package/templates/native-go-vue/frontend/src/App.vue +132 -0
  214. package/templates/native-go-vue/frontend/src/context.ts +17 -0
  215. package/templates/native-go-vue/frontend/src/main.ts +26 -0
  216. package/templates/native-go-vue/frontend/src/style.css +14 -0
  217. package/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
  218. package/templates/native-go-vue/frontend/tsconfig.json +1 -0
  219. package/templates/native-go-vue/frontend/vite.config.ts +8 -0
  220. package/templates/native-go-vue/gitignore +2 -0
  221. package/dist/attestation/app-package.d.ts +0 -86
  222. package/dist/attestation/app-package.js +0 -206
  223. package/dist/attestation/install-on-host.d.ts +0 -14
  224. package/dist/attestation/install-on-host.js +0 -52
  225. package/dist/attestation/keyring.d.ts +0 -21
  226. package/dist/attestation/keyring.js +0 -168
  227. package/dist/attestation/platform-service.d.ts +0 -70
  228. package/dist/attestation/platform-service.js +0 -250
  229. package/dist/build-spk/broker-glue.d.ts +0 -16
  230. package/dist/build-spk/broker-glue.js +0 -40
  231. package/dist/deploy/catalog-publish.d.ts +0 -3
  232. package/dist/deploy/catalog-publish.js +0 -7
  233. package/dist/doctor.d.ts +0 -22
  234. package/dist/doctor.js +0 -721
  235. package/dist/error-collector.d.ts +0 -2
  236. package/dist/error-collector.js +0 -46
  237. package/dist/host/broker-paths.d.ts +0 -1
  238. package/dist/host/broker-paths.js +0 -1
  239. package/dist/host/broker-upgrade-preflight.d.ts +0 -61
  240. package/dist/host/broker-upgrade-preflight.js +0 -156
  241. package/dist/host/digests.d.ts +0 -5
  242. package/dist/host/digests.js +0 -63
  243. package/dist/host/fake-nas.d.ts +0 -41
  244. package/dist/host/fake-nas.js +0 -174
  245. package/dist/mock-broker/bootstrap.d.ts +0 -2
  246. package/dist/mock-broker/bootstrap.js +0 -8
  247. package/dist/mock-broker/files.d.ts +0 -60
  248. package/dist/mock-broker/files.js +0 -408
  249. package/dist/mock-broker/oidc.d.ts +0 -31
  250. package/dist/mock-broker/oidc.js +0 -161
  251. package/dist/mock-broker/path.d.ts +0 -9
  252. package/dist/mock-broker/path.js +0 -50
  253. package/dist/mock-broker/server.d.ts +0 -19
  254. package/dist/mock-broker/server.js +0 -350
  255. package/dist/publish/build-ledger.d.ts +0 -22
  256. package/dist/publish/build-ledger.js +0 -26
  257. package/dist/publish/community-path.d.ts +0 -6
  258. package/dist/publish/community-path.js +0 -41
  259. package/dist/publish/curated-release.d.ts +0 -78
  260. package/dist/publish/curated-release.js +0 -883
  261. package/dist/publish/file-build-ledger.d.ts +0 -18
  262. package/dist/publish/file-build-ledger.js +0 -268
  263. package/dist/publish/github-pr.d.ts +0 -25
  264. package/dist/publish/github-pr.js +0 -85
  265. package/dist/publish/github-repo.d.ts +0 -8
  266. package/dist/publish/github-repo.js +0 -21
  267. package/dist/publish/platform-service-release.d.ts +0 -60
  268. package/dist/publish/platform-service-release.js +0 -333
  269. package/dist/publish/preflight.d.ts +0 -24
  270. package/dist/publish/preflight.js +0 -138
  271. package/dist/publish/publish.d.ts +0 -27
  272. package/dist/publish/publish.js +0 -136
  273. package/dist/publish/sync-definition.d.ts +0 -4
  274. package/dist/publish/sync-definition.js +0 -48
  275. package/dist/release-bin.d.ts +0 -2
  276. package/dist/release-bin.js +0 -23
  277. package/dist/templates/hello-web/html/sdk.js +0 -68
  278. package/dist/templates/hello-web-page/html/sdk.js +0 -68
  279. package/dist/templates/native/synology-app.yaml +0 -7
  280. package/dist/test-host.d.ts +0 -15
  281. package/dist/test-host.js +0 -37
  282. package/templates/hello-web/html/sdk.js +0 -68
  283. package/templates/hello-web-page/html/sdk.js +0 -68
  284. package/templates/native/synology-app.yaml +0 -7
  285. /package/dist/templates/{native → binary}/native.yaml +0 -0
  286. /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/