@synopackageland/cli 0.13.1 → 0.15.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 (119) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +44 -1
  3. package/dist/build-spk/generator.d.ts +3 -2
  4. package/dist/build-spk/generator.js +6 -850
  5. package/dist/build-spk/resource-worker.d.ts +2 -14
  6. package/dist/build-spk/resource-worker.js +3 -166
  7. package/dist/cli.js +208 -56
  8. package/dist/compose/analyze.d.ts +3 -61
  9. package/dist/compose/analyze.js +35 -776
  10. package/dist/compose/env.d.ts +5 -3
  11. package/dist/compose/env.js +17 -25
  12. package/dist/compose/runner.js +23 -6
  13. package/dist/contract/app-launch.d.ts +11 -0
  14. package/dist/contract/app-launch.js +18 -0
  15. package/dist/contract/web-launch.d.ts +3 -13
  16. package/dist/contract/web-launch.js +14 -81
  17. package/dist/ctl/cli.d.ts +36 -0
  18. package/dist/ctl/cli.js +218 -0
  19. package/dist/ctl/descriptor.d.ts +81 -0
  20. package/dist/ctl/descriptor.js +93 -0
  21. package/dist/ctl/dsm-auth.d.ts +10 -0
  22. package/dist/ctl/dsm-auth.js +30 -0
  23. package/dist/ctl/errors.d.ts +17 -0
  24. package/dist/ctl/errors.js +32 -0
  25. package/dist/ctl/flows.d.ts +11 -0
  26. package/dist/ctl/flows.js +52 -0
  27. package/dist/ctl/input.d.ts +20 -0
  28. package/dist/ctl/input.js +128 -0
  29. package/dist/ctl/json.d.ts +2 -0
  30. package/dist/ctl/json.js +38 -0
  31. package/dist/ctl/redaction.d.ts +15 -0
  32. package/dist/ctl/redaction.js +198 -0
  33. package/dist/ctl/service.d.ts +38 -0
  34. package/dist/ctl/service.js +156 -0
  35. package/dist/ctl/store.d.ts +51 -0
  36. package/dist/ctl/store.js +93 -0
  37. package/dist/ctl/transfers.d.ts +14 -0
  38. package/dist/ctl/transfers.js +74 -0
  39. package/dist/ctl/transport.d.ts +11 -0
  40. package/dist/ctl/transport.js +79 -0
  41. package/dist/deploy/deploy.d.ts +4 -3
  42. package/dist/deploy/deploy.js +5 -210
  43. package/dist/deploy/plan.js +26 -7
  44. package/dist/dev.js +5 -4
  45. package/dist/error-registry.js +1020 -831
  46. package/dist/info.js +120 -28
  47. package/dist/init.js +188 -64
  48. package/dist/inspect.js +21 -6
  49. package/dist/runtime/executor-digests.json +1 -12
  50. package/dist/skill.js +51 -9
  51. package/dist/standalone/api-description-inspect.d.ts +4 -0
  52. package/dist/standalone/api-description-inspect.js +12 -0
  53. package/dist/standalone/api-description.d.ts +7 -0
  54. package/dist/standalone/api-description.js +73 -0
  55. package/dist/standalone/build.d.ts +3 -0
  56. package/dist/standalone/build.js +140 -33
  57. package/dist/standalone/compose-errors.d.ts +3 -0
  58. package/dist/standalone/compose-errors.js +243 -0
  59. package/dist/standalone/compose.d.ts +97 -10
  60. package/dist/standalone/compose.js +306 -75
  61. package/dist/standalone/daemon.js +111 -29
  62. package/dist/standalone/dsm-window.js +55 -9
  63. package/dist/standalone/identity-gateway.d.ts +9 -2
  64. package/dist/standalone/identity-gateway.js +282 -66
  65. package/dist/standalone/inspect-spk.d.ts +15 -0
  66. package/dist/standalone/inspect-spk.js +714 -128
  67. package/dist/standalone/lifecycle.js +124 -42
  68. package/dist/standalone/manifest.d.ts +10 -1
  69. package/dist/standalone/manifest.js +308 -87
  70. package/dist/standalone/resource.js +6 -2
  71. package/dist/standalone/spk.d.ts +4 -1
  72. package/dist/standalone/spk.js +139 -39
  73. package/dist/template-paths.d.ts +0 -2
  74. package/dist/template-paths.js +0 -5
  75. package/dist/test-command.d.ts +1 -1
  76. package/dist/test-command.js +1 -1
  77. package/dist/validate.d.ts +3 -3
  78. package/dist/validate.js +32 -140
  79. package/package.json +5 -3
  80. package/schema/synology-app.schema.json +1 -1
  81. package/schema/synopkgland-api-v1.schema.json +423 -0
  82. package/skills/SKILL.md +34 -1
  83. package/skills/reference/compose.md +349 -0
  84. package/skills/reference/create.md +26 -4
  85. package/skills/reference/ctl.md +41 -0
  86. package/skills/reference/deploy.md +1 -1
  87. package/skills/reference/files.md +4 -0
  88. package/skills/reference/identity-gateway.md +8 -138
  89. package/skills/reference/portal.md +31 -0
  90. package/skills/reference/ui-vue.md +31 -0
  91. package/skills/reference/wrap.md +10 -30
  92. package/assets/go/daemon/fixture_linux_test.go +0 -15
  93. package/assets/go/daemon/fixture_other_test.go +0 -7
  94. package/assets/go/daemon/go.mod +0 -5
  95. package/assets/go/daemon/main.go +0 -636
  96. package/assets/go/daemon/main_test.go +0 -315
  97. package/assets/go/daemon/network.go +0 -107
  98. package/assets/go/daemon/network_test.go +0 -86
  99. package/assets/go/daemon/process_linux.go +0 -55
  100. package/assets/go/daemon/process_linux_test.go +0 -161
  101. package/assets/go/daemon/process_other.go +0 -14
  102. package/assets/go/daemon/testdata/child.go +0 -44
  103. package/assets/go/gateway/auth.go +0 -101
  104. package/assets/go/gateway/auth_test.go +0 -216
  105. package/assets/go/gateway/cgi.go +0 -133
  106. package/assets/go/gateway/cgi_test.go +0 -156
  107. package/assets/go/gateway/csrf.go +0 -73
  108. package/assets/go/gateway/csrf_test.go +0 -155
  109. package/assets/go/gateway/directory.go +0 -108
  110. package/assets/go/gateway/directory_test.go +0 -202
  111. package/assets/go/gateway/gateway.go +0 -412
  112. package/assets/go/gateway/gateway_test.go +0 -676
  113. package/assets/go/gateway/go.mod +0 -5
  114. package/assets/go/gateway/groups.go +0 -66
  115. package/assets/go/gateway/main.go +0 -45
  116. package/dist/build-spk/gateway.d.ts +0 -31
  117. package/dist/build-spk/gateway.js +0 -60
  118. package/dist/compose/privilege.d.ts +0 -14
  119. package/dist/compose/privilege.js +0 -102
@@ -1,636 +0,0 @@
1
- // Package-account supervisor for one fixed foreground executable. The pidfile
2
- // is inventory, never authority for kill(pid). Only the parent signals its child.
3
- package main
4
-
5
- import (
6
- "bufio"
7
- "crypto/sha256"
8
- "encoding/hex"
9
- "encoding/json"
10
- "errors"
11
- "fmt"
12
- "io"
13
- "net"
14
- "os"
15
- "os/exec"
16
- "os/signal"
17
- "os/user"
18
- "path/filepath"
19
- "runtime"
20
- "sort"
21
- "strconv"
22
- "strings"
23
- "syscall"
24
- "time"
25
- )
26
-
27
- type deadlines struct {
28
- Start int `json:"start"`
29
- Stop int `json:"stop"`
30
- Status int `json:"status"`
31
- // Uninstall bounds uninstall_args separately from Stop: a revoke on a
32
- // loaded NAS can take longer than stopping the daemon.
33
- Uninstall int `json:"uninstall"`
34
- }
35
- type config struct {
36
- Network *networkPolicy `json:"network,omitempty"`
37
- Package string `json:"package"`
38
- Exec string `json:"exec"`
39
- Args []string `json:"args"`
40
- UninstallArgs []string `json:"uninstall_args,omitempty"`
41
- Env map[string]string `json:"env"`
42
- Timeouts deadlines `json:"timeouts"`
43
- }
44
- type record struct {
45
- Supervisor int `json:"supervisor"`
46
- Child int `json:"child"`
47
- Birth string `json:"birth"`
48
- Digest string `json:"digest"`
49
- }
50
- type controller struct {
51
- config
52
- dest, data, dir, digest string
53
- }
54
-
55
- func main() {
56
- if len(os.Args) != 4 {
57
- os.Exit(1)
58
- }
59
- c, err := load(os.Args[2], os.Args[3])
60
- if err != nil {
61
- fmt.Fprintln(os.Stderr, err)
62
- os.Exit(1)
63
- }
64
- rc, err := c.run(os.Args[1])
65
- if err != nil {
66
- fmt.Fprintln(os.Stderr, err)
67
- }
68
- os.Exit(rc)
69
- }
70
-
71
- func load(dest, data string) (*controller, error) {
72
- if os.Geteuid() == 0 || os.Getuid() != os.Geteuid() {
73
- return nil, errors.New("daemon requires a non-root package account")
74
- }
75
- if !filepath.IsAbs(dest) || !filepath.IsAbs(data) {
76
- return nil, errors.New("daemon paths must be absolute")
77
- }
78
- raw, err := os.ReadFile(filepath.Join(dest, "daemon/config.json"))
79
- if err != nil || len(raw) > 512*1024 {
80
- return nil, errors.New("invalid daemon config")
81
- }
82
- var cfg config
83
- d := json.NewDecoder(strings.NewReader(string(raw)))
84
- d.DisallowUnknownFields()
85
- if err := d.Decode(&cfg); err != nil {
86
- return nil, err
87
- }
88
- account, err := user.Lookup(cfg.Package)
89
- if err != nil || account.Uid != strconv.Itoa(os.Geteuid()) {
90
- return nil, errors.New("daemon requires its configured package account")
91
- }
92
- if cfg.Exec == "" || filepath.IsAbs(cfg.Exec) || filepath.Clean(cfg.Exec) != cfg.Exec || strings.HasPrefix(cfg.Exec, "../") {
93
- return nil, errors.New("invalid fixed executable")
94
- }
95
- if cfg.Timeouts.Start < 2 || cfg.Timeouts.Start > 120 || cfg.Timeouts.Stop < 2 || cfg.Timeouts.Stop > 120 || cfg.Timeouts.Status < 1 || cfg.Timeouts.Status > 10 || cfg.Timeouts.Uninstall < 2 || cfg.Timeouts.Uninstall > 120 {
96
- return nil, errors.New("invalid daemon deadlines")
97
- }
98
- hash := sha256.Sum256(raw)
99
- c := &controller{config: cfg, dest: dest, data: data, dir: filepath.Join(data, "daemon"), digest: hex.EncodeToString(hash[:])}
100
- if err := os.Mkdir(c.dir, 0700); err != nil && !os.IsExist(err) {
101
- return nil, err
102
- }
103
- st, err := os.Lstat(c.dir)
104
- if err != nil || !st.IsDir() || st.Mode().Perm() != 0700 || !owned(st) {
105
- return nil, errors.New("foreign daemon state directory")
106
- }
107
- tmp := filepath.Join(c.dir, "tmp")
108
- if err := os.Mkdir(tmp, 0700); err != nil && !os.IsExist(err) {
109
- return nil, err
110
- }
111
- st, err = os.Lstat(tmp)
112
- if err != nil || !st.IsDir() || st.Mode().Perm() != 0700 || !owned(st) {
113
- return nil, errors.New("foreign daemon temporary directory")
114
- }
115
- return c, nil
116
- }
117
-
118
- func owned(st os.FileInfo) bool {
119
- s, ok := st.Sys().(*syscall.Stat_t)
120
- return ok && s.Uid == uint32(os.Geteuid())
121
- }
122
-
123
- func safeFile(path string) (*os.File, error) {
124
- f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0600)
125
- if err != nil {
126
- return nil, err
127
- }
128
- st, err := f.Stat()
129
- if err != nil || !st.Mode().IsRegular() || st.Mode().Perm() != 0600 || !owned(st) || st.Sys().(*syscall.Stat_t).Nlink != 1 {
130
- f.Close()
131
- return nil, errors.New("foreign daemon file")
132
- }
133
- return f, nil
134
- }
135
-
136
- func (c *controller) lock(name string, until time.Time) (*os.File, error) {
137
- f, err := safeFile(filepath.Join(c.dir, name))
138
- if err != nil {
139
- return nil, err
140
- }
141
- for {
142
- err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
143
- if err == nil {
144
- return f, nil
145
- }
146
- if !errors.Is(err, syscall.EWOULDBLOCK) && !errors.Is(err, syscall.EAGAIN) || !time.Now().Before(until) {
147
- f.Close()
148
- return nil, errors.New("daemon lock busy")
149
- }
150
- time.Sleep(20 * time.Millisecond)
151
- }
152
- }
153
-
154
- func (c *controller) run(action string) (int, error) {
155
- if action == "supervise" {
156
- return 1, c.supervise()
157
- }
158
- budget := c.Timeouts.Status
159
- if action == "start" {
160
- budget = c.Timeouts.Start
161
- }
162
- if action == "stop" || action == "restart" || action == "uninstall" {
163
- budget = c.Timeouts.Stop + 1
164
- }
165
- if action == "restart" {
166
- budget += c.Timeouts.Start
167
- }
168
- if action == "uninstall" {
169
- budget += c.Timeouts.Uninstall + 2
170
- }
171
- if action != "start" && action != "stop" && action != "status" && action != "restart" && action != "uninstall" {
172
- return 1, errors.New("unsupported daemon action")
173
- }
174
- until := time.Now().Add(time.Duration(budget) * time.Second)
175
- lock, err := c.lock("command.lock", until)
176
- if err != nil {
177
- return 1, err
178
- }
179
- defer lock.Close()
180
- if action == "status" {
181
- if err := c.request("status", until); err == nil {
182
- return 0, nil
183
- }
184
- if _, err := os.Lstat(filepath.Join(c.dir, "daemon.pid")); os.IsNotExist(err) {
185
- owner, err := c.lock("owner.lock", time.Now())
186
- if err == nil {
187
- owner.Close()
188
- return 3, nil
189
- }
190
- }
191
- return 1, errors.New("daemon failed or supervisor unavailable")
192
- }
193
- if action == "stop" || action == "restart" || action == "uninstall" {
194
- stopUntil := time.Now().Add(time.Duration(c.Timeouts.Stop+1) * time.Second)
195
- if until.Before(stopUntil) {
196
- stopUntil = until
197
- }
198
- if err := c.stop(stopUntil); err != nil {
199
- return 1, err
200
- }
201
- if action == "stop" {
202
- return 0, nil
203
- }
204
- if action == "uninstall" {
205
- if len(c.UninstallArgs) == 0 {
206
- return 0, nil
207
- }
208
- if err := c.uninstall(); err != nil {
209
- return 1, err
210
- }
211
- return 0, nil
212
- }
213
- }
214
- if err := c.request("status", time.Now().Add(time.Duration(c.Timeouts.Status)*time.Second)); err == nil {
215
- return 0, nil
216
- }
217
- if err := c.start(until); err != nil {
218
- return 1, err
219
- }
220
- return 0, nil
221
- }
222
-
223
- func (c *controller) readRecord() (record, error) {
224
- var r record
225
- f, err := safeFileExisting(filepath.Join(c.dir, "daemon.pid"))
226
- if err != nil {
227
- return r, err
228
- }
229
- defer f.Close()
230
- err = json.NewDecoder(io.LimitReader(f, 4096)).Decode(&r)
231
- if err == nil && (r.Supervisor < 1 || r.Child < 1 || r.Digest != c.digest) {
232
- err = errors.New("daemon inventory mismatch")
233
- }
234
- return r, err
235
- }
236
-
237
- func safeFileExisting(path string) (*os.File, error) {
238
- f, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0)
239
- if err != nil {
240
- return nil, err
241
- }
242
- st, err := f.Stat()
243
- if err != nil || !st.Mode().IsRegular() || !owned(st) || st.Mode().Perm() != 0600 || st.Size() > 4096 || st.Sys().(*syscall.Stat_t).Nlink != 1 {
244
- f.Close()
245
- return nil, errors.New("foreign daemon record")
246
- }
247
- return f, nil
248
- }
249
-
250
- func (c *controller) request(action string, until time.Time) error {
251
- r, err := c.readRecord()
252
- if err != nil {
253
- return err
254
- }
255
- conn, err := net.DialTimeout("unix", filepath.Join(c.dir, "ctl.sock"), time.Until(until))
256
- if err != nil {
257
- return err
258
- }
259
- defer conn.Close()
260
- if err := checkPeer(conn.(*net.UnixConn), r.Supervisor); err != nil {
261
- return err
262
- }
263
- conn.SetDeadline(until)
264
- if _, err := fmt.Fprintf(conn, "%s %s\n", action, c.digest); err != nil {
265
- return err
266
- }
267
- raw, err := bufio.NewReader(io.LimitReader(conn, 256)).ReadString('\n')
268
- if err != nil {
269
- return err
270
- }
271
- if raw != "ok\n" {
272
- return errors.New("daemon operation refused")
273
- }
274
- return nil
275
- }
276
-
277
- // With no supervisor lock held, never signal a PID read from inventory. A live
278
- // witnessed child means cleanup-required. Dead/zombie/reused PIDs can be retired.
279
- func (c *controller) retired() error {
280
- r, err := c.readRecord()
281
- if err != nil && !os.IsNotExist(err) {
282
- return err
283
- }
284
- if err == nil && recordedChildAlive(r.Child, r.Birth) {
285
- return errors.New("daemon cleanup-required: live child without supervisor")
286
- }
287
- if err != nil {
288
- if _, socketErr := os.Lstat(filepath.Join(c.dir, "ctl.sock")); !os.IsNotExist(socketErr) {
289
- return errors.New("foreign daemon socket")
290
- }
291
- }
292
- for _, name := range []string{"daemon.pid", "ctl.sock"} {
293
- if err := os.Remove(filepath.Join(c.dir, name)); err != nil && !os.IsNotExist(err) {
294
- return err
295
- }
296
- }
297
- return nil
298
- }
299
-
300
- func (c *controller) stop(until time.Time) error {
301
- if err := c.request("stop", until); err != nil {
302
- owner, lockErr := c.lock("owner.lock", time.Now())
303
- if lockErr != nil {
304
- return err
305
- }
306
- defer owner.Close()
307
- return c.retired()
308
- }
309
- owner, err := c.lock("owner.lock", until)
310
- if err != nil {
311
- return err
312
- }
313
- defer owner.Close()
314
- return c.retired()
315
- }
316
-
317
- func (c *controller) start(until time.Time) error {
318
- if !time.Now().Before(until) {
319
- return errors.New("daemon start timeout")
320
- }
321
- owner, err := c.lock("owner.lock", time.Now())
322
- if err != nil {
323
- return err
324
- }
325
- if err := c.retired(); err != nil {
326
- owner.Close()
327
- return err
328
- }
329
- // Pass the already-held lifetime lock to the new supervisor. No handoff gap.
330
- log, err := safeFile(filepath.Join(c.dir, "daemon.log"))
331
- if err != nil {
332
- owner.Close()
333
- return err
334
- }
335
- defer log.Close()
336
- if _, err := log.Seek(0, io.SeekEnd); err != nil {
337
- owner.Close()
338
- return err
339
- }
340
- reader, writer, err := os.Pipe()
341
- if err != nil {
342
- owner.Close()
343
- return err
344
- }
345
- defer reader.Close()
346
- self, err := os.Executable()
347
- if err != nil {
348
- owner.Close()
349
- writer.Close()
350
- return err
351
- }
352
- cmd := exec.Command(self, "supervise", c.dest, c.data)
353
- cmd.Env = []string{"PATH=/usr/bin:/bin"}
354
- cmd.Stdin, cmd.Stdout, cmd.Stderr = nil, log, log
355
- cmd.ExtraFiles = []*os.File{writer, owner}
356
- cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
357
- err = cmd.Start()
358
- writer.Close()
359
- owner.Close()
360
- if err != nil {
361
- return err
362
- }
363
- // Wait in a goroutine to reap if startup fails. Successful supervisors are
364
- // detached when this command exits; their directly-owned child stays attached.
365
- go cmd.Wait()
366
- ready := make(chan error, 1)
367
- go func() {
368
- raw, err := bufio.NewReader(io.LimitReader(reader, 32)).ReadString('\n')
369
- if err == nil && raw != "ready\n" {
370
- err = errors.New("daemon startup refused")
371
- }
372
- ready <- err
373
- }()
374
- select {
375
- case err := <-ready:
376
- return err
377
- case <-time.After(time.Until(until)):
378
- // Only our own still-retained supervisor can be signalled, never inventory.
379
- cmd.Process.Signal(syscall.SIGTERM)
380
- return errors.New("daemon start timeout")
381
- }
382
- }
383
-
384
- func (c *controller) expand(v string) string {
385
- return strings.ReplaceAll(strings.ReplaceAll(v, "${SYNOPKG_PKGDEST}", c.dest), "${SYNOPKG_PKGVAR}", c.data)
386
- }
387
-
388
- func (c *controller) child(args []string) *exec.Cmd {
389
- expanded := make([]string, len(args))
390
- for i, v := range args {
391
- expanded[i] = c.expand(v)
392
- }
393
- cmd := exec.Command(filepath.Join(c.dest, "daemon/payload", c.Exec), expanded...)
394
- cmd.Dir = c.data
395
- cmd.Env = []string{"PATH=/usr/bin:/bin", "HOME=" + c.data, "TMPDIR=" + filepath.Join(c.dir, "tmp")}
396
- keys := make([]string, 0, len(c.Env))
397
- for key := range c.Env {
398
- keys = append(keys, key)
399
- }
400
- sort.Strings(keys)
401
- for _, key := range keys {
402
- cmd.Env = append(cmd.Env, key+"="+c.expand(c.Env[key]))
403
- }
404
- cmd.SysProcAttr = childAttributes()
405
- return cmd
406
- }
407
-
408
- func (c *controller) uninstall() error {
409
- runtime.LockOSThread()
410
- defer runtime.UnlockOSThread()
411
- cmd := c.child(c.UninstallArgs)
412
- cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
413
- if err := cmd.Start(); err != nil {
414
- return err
415
- }
416
- done := make(chan error, 1)
417
- go func() { done <- cmd.Wait() }()
418
- select {
419
- case err := <-done:
420
- return err
421
- case <-time.After(time.Duration(c.Timeouts.Uninstall) * time.Second):
422
- cmd.Process.Signal(syscall.SIGTERM)
423
- }
424
- select {
425
- case <-done:
426
- return errors.New("daemon uninstall timeout")
427
- case <-time.After(time.Second):
428
- cmd.Process.Kill()
429
- }
430
- select {
431
- case <-done:
432
- return errors.New("daemon uninstall timeout")
433
- case <-time.After(time.Second):
434
- return errors.New("daemon uninstall cleanup-required")
435
- }
436
- }
437
-
438
- func (c *controller) supervise() error {
439
- runtime.LockOSThread()
440
- defer runtime.UnlockOSThread()
441
- ready, owner := os.NewFile(3, "ready"), os.NewFile(4, "owner.lock")
442
- if ready == nil || owner == nil {
443
- return errors.New("missing supervisor bootstrap")
444
- }
445
- defer ready.Close()
446
- defer owner.Close()
447
- syscall.CloseOnExec(3)
448
- syscall.CloseOnExec(4)
449
- // These descriptors must be the private pipe and exactly our already-held lock.
450
- st, err := owner.Stat()
451
- if err != nil {
452
- return err
453
- }
454
- pathStat, err := os.Lstat(filepath.Join(c.dir, "owner.lock"))
455
- if err != nil || !os.SameFile(st, pathStat) || !owned(st) {
456
- return errors.New("invalid supervisor lock")
457
- }
458
- pipeStat, err := ready.Stat()
459
- if err != nil || pipeStat.Mode()&os.ModeNamedPipe == 0 {
460
- return errors.New("invalid supervisor pipe")
461
- }
462
- if err := syscall.Flock(int(owner.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
463
- return err
464
- }
465
- signals := make(chan os.Signal, 1)
466
- signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
467
- defer signal.Stop(signals)
468
- if c.Network != nil {
469
- deadline := time.Now().Add(time.Duration(c.Timeouts.Start-2) * time.Second)
470
- for {
471
- err := checkNetwork("/proc/net", os.Geteuid(), c.Network)
472
- if err == nil {
473
- break
474
- }
475
- if !time.Now().Before(deadline) {
476
- return err
477
- }
478
- select {
479
- case <-signals:
480
- return errors.New("compose startup interrupted")
481
- case <-time.After(100 * time.Millisecond):
482
- }
483
- }
484
- }
485
- cmd := c.child(c.Args)
486
- cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
487
- if err := cmd.Start(); err != nil {
488
- return err
489
- }
490
- done := make(chan error, 1)
491
- go func() { done <- cmd.Wait() }()
492
- stop := func() error {
493
- cmd.Process.Signal(syscall.SIGTERM)
494
- select {
495
- case <-done:
496
- return nil
497
- case <-time.After(time.Duration(c.Timeouts.Stop-1) * time.Second):
498
- cmd.Process.Kill()
499
- }
500
- select {
501
- case <-done:
502
- return nil
503
- case <-time.After(time.Second):
504
- return errors.New("daemon child stop timeout")
505
- }
506
- }
507
- // Process stability is the readiness contract; it is not app HTTP readiness.
508
- select {
509
- case <-done:
510
- return errors.New("daemon exited during startup")
511
- case <-signals:
512
- return stop()
513
- case <-time.After(time.Second):
514
- }
515
- r := record{Supervisor: os.Getpid(), Child: cmd.Process.Pid, Birth: processBirth(cmd.Process.Pid), Digest: c.digest}
516
- raw, _ := json.Marshal(r)
517
- f, err := safeFile(filepath.Join(c.dir, "daemon.pid"))
518
- if err != nil {
519
- stop()
520
- return err
521
- }
522
- err = f.Truncate(0)
523
- if err == nil {
524
- _, err = f.Write(raw)
525
- }
526
- if err == nil {
527
- err = f.Sync()
528
- }
529
- f.Close()
530
- if err != nil {
531
- stop()
532
- return err
533
- }
534
- socket := filepath.Join(c.dir, "ctl.sock")
535
- ln, err := net.ListenUnix("unix", &net.UnixAddr{Name: socket, Net: "unix"})
536
- if err != nil {
537
- stop()
538
- os.Remove(filepath.Join(c.dir, "daemon.pid"))
539
- return err
540
- }
541
- ln.SetUnlinkOnClose(false)
542
- defer ln.Close()
543
- clean := false
544
- defer func() {
545
- os.Remove(socket)
546
- if clean {
547
- os.Remove(filepath.Join(c.dir, "daemon.pid"))
548
- }
549
- }()
550
- if err := os.Chmod(socket, 0600); err != nil {
551
- stop()
552
- return err
553
- }
554
- if _, err := ready.WriteString("ready\n"); err != nil {
555
- clean = stop() == nil
556
- return err
557
- }
558
- ready.Close()
559
- connections := make(chan *net.UnixConn)
560
- go func() {
561
- for {
562
- conn, err := ln.AcceptUnix()
563
- if err != nil {
564
- return
565
- }
566
- select {
567
- case connections <- conn:
568
- case <-time.After(time.Second):
569
- conn.Close()
570
- }
571
- }
572
- }()
573
- tick := time.NewTicker(time.Second)
574
- defer tick.Stop()
575
- for {
576
- select {
577
- case <-tick.C:
578
- if err := checkNetwork("/proc/net", os.Geteuid(), c.Network); err != nil {
579
- clean = stop() == nil
580
- return err
581
- }
582
- case <-done:
583
- return nil
584
- case <-signals:
585
- err := stop()
586
- clean = err == nil
587
- return err
588
- case conn := <-connections:
589
- conn.SetDeadline(time.Now().Add(time.Duration(c.Timeouts.Status) * time.Second))
590
- raw, err := bufio.NewReader(io.LimitReader(conn, 256)).ReadString('\n')
591
- if err != nil {
592
- conn.Close()
593
- continue
594
- }
595
- if raw == "status "+c.digest+"\n" {
596
- select {
597
- case <-done:
598
- conn.Write([]byte("failed\n"))
599
- conn.Close()
600
- return nil
601
- default:
602
- conn.Write([]byte("ok\n"))
603
- conn.Close()
604
- }
605
- } else if raw == "stop "+c.digest+"\n" {
606
- conn.SetDeadline(time.Now().Add(time.Duration(c.Timeouts.Stop+1) * time.Second))
607
- err := stop()
608
- clean = err == nil
609
- if err == nil {
610
- conn.Write([]byte("ok\n"))
611
- } else {
612
- conn.Write([]byte("failed\n"))
613
- }
614
- conn.Close()
615
- return err
616
- } else {
617
- conn.Write([]byte("refused\n"))
618
- conn.Close()
619
- }
620
- }
621
- }
622
- }
623
-
624
- // The proc parser handles spaces and ')' inside Linux executable names.
625
- func parseBirth(raw string) (string, bool) {
626
- end := strings.LastIndex(raw, ")")
627
- if end < 0 {
628
- return "", false
629
- }
630
- fields := strings.Fields(raw[end+1:])
631
- if len(fields) < 20 {
632
- return "", false
633
- }
634
- _, err := strconv.ParseUint(fields[19], 10, 64)
635
- return fields[19], err == nil && fields[0] != "Z" && fields[0] != "X"
636
- }