@synopackageland/cli 0.14.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.
- package/CHANGELOG.md +10 -0
- package/README.md +26 -16
- package/dist/build-spk/generator.d.ts +3 -2
- package/dist/build-spk/generator.js +6 -846
- package/dist/build-spk/resource-worker.d.ts +2 -14
- package/dist/build-spk/resource-worker.js +3 -166
- package/dist/cli.js +208 -56
- package/dist/compose/analyze.d.ts +3 -61
- package/dist/compose/analyze.js +35 -776
- package/dist/compose/env.d.ts +5 -3
- package/dist/compose/env.js +17 -25
- package/dist/compose/runner.js +23 -6
- package/dist/contract/web-launch.d.ts +1 -12
- package/dist/contract/web-launch.js +10 -74
- package/dist/ctl/cli.d.ts +36 -0
- package/dist/ctl/cli.js +218 -0
- package/dist/ctl/descriptor.d.ts +81 -0
- package/dist/ctl/descriptor.js +93 -0
- package/dist/ctl/dsm-auth.d.ts +10 -0
- package/dist/ctl/dsm-auth.js +30 -0
- package/dist/ctl/errors.d.ts +17 -0
- package/dist/ctl/errors.js +32 -0
- package/dist/ctl/flows.d.ts +11 -0
- package/dist/ctl/flows.js +52 -0
- package/dist/ctl/input.d.ts +20 -0
- package/dist/ctl/input.js +128 -0
- package/dist/ctl/json.d.ts +2 -0
- package/dist/ctl/json.js +38 -0
- package/dist/ctl/redaction.d.ts +15 -0
- package/dist/ctl/redaction.js +198 -0
- package/dist/ctl/service.d.ts +38 -0
- package/dist/ctl/service.js +156 -0
- package/dist/ctl/store.d.ts +51 -0
- package/dist/ctl/store.js +93 -0
- package/dist/ctl/transfers.d.ts +14 -0
- package/dist/ctl/transfers.js +74 -0
- package/dist/ctl/transport.d.ts +11 -0
- package/dist/ctl/transport.js +79 -0
- package/dist/deploy/deploy.d.ts +4 -3
- package/dist/deploy/deploy.js +5 -210
- package/dist/deploy/plan.js +26 -7
- package/dist/dev.js +5 -4
- package/dist/error-registry.js +1020 -834
- package/dist/info.js +120 -28
- package/dist/init.js +188 -64
- package/dist/inspect.js +21 -6
- package/dist/runtime/executor-digests.json +1 -12
- package/dist/skill.js +51 -9
- package/dist/standalone/api-description-inspect.d.ts +4 -0
- package/dist/standalone/api-description-inspect.js +12 -0
- package/dist/standalone/api-description.d.ts +7 -0
- package/dist/standalone/api-description.js +73 -0
- package/dist/standalone/build.d.ts +3 -0
- package/dist/standalone/build.js +140 -33
- package/dist/standalone/compose-errors.d.ts +3 -0
- package/dist/standalone/compose-errors.js +243 -0
- package/dist/standalone/compose.d.ts +97 -10
- package/dist/standalone/compose.js +306 -75
- package/dist/standalone/daemon.js +111 -29
- package/dist/standalone/dsm-window.js +39 -8
- package/dist/standalone/identity-gateway.d.ts +9 -2
- package/dist/standalone/identity-gateway.js +282 -66
- package/dist/standalone/inspect-spk.d.ts +12 -0
- package/dist/standalone/inspect-spk.js +666 -136
- package/dist/standalone/lifecycle.js +124 -42
- package/dist/standalone/manifest.d.ts +8 -1
- package/dist/standalone/manifest.js +303 -86
- package/dist/standalone/resource.js +6 -2
- package/dist/standalone/spk.d.ts +4 -1
- package/dist/standalone/spk.js +138 -39
- package/dist/template-paths.d.ts +0 -2
- package/dist/template-paths.js +0 -5
- package/dist/test-command.d.ts +1 -1
- package/dist/test-command.js +1 -1
- package/dist/validate.d.ts +3 -3
- package/dist/validate.js +32 -140
- package/package.json +5 -3
- package/schema/synopkgland-api-v1.schema.json +423 -0
- package/skills/SKILL.md +3 -1
- package/skills/reference/compose.md +349 -0
- package/skills/reference/create.md +8 -17
- package/skills/reference/ctl.md +41 -0
- package/skills/reference/deploy.md +1 -1
- package/skills/reference/files.md +4 -0
- package/skills/reference/identity-gateway.md +8 -144
- package/skills/reference/wrap.md +10 -39
- package/assets/go/daemon/fixture_linux_test.go +0 -15
- package/assets/go/daemon/fixture_other_test.go +0 -7
- package/assets/go/daemon/go.mod +0 -5
- package/assets/go/daemon/main.go +0 -636
- package/assets/go/daemon/main_test.go +0 -315
- package/assets/go/daemon/network.go +0 -107
- package/assets/go/daemon/network_test.go +0 -86
- package/assets/go/daemon/process_linux.go +0 -55
- package/assets/go/daemon/process_linux_test.go +0 -161
- package/assets/go/daemon/process_other.go +0 -14
- package/assets/go/daemon/testdata/child.go +0 -44
- package/assets/go/gateway/auth.go +0 -101
- package/assets/go/gateway/auth_test.go +0 -216
- package/assets/go/gateway/cgi.go +0 -133
- package/assets/go/gateway/cgi_test.go +0 -156
- package/assets/go/gateway/csrf.go +0 -73
- package/assets/go/gateway/csrf_test.go +0 -155
- package/assets/go/gateway/directory.go +0 -108
- package/assets/go/gateway/directory_test.go +0 -202
- package/assets/go/gateway/gateway.go +0 -412
- package/assets/go/gateway/gateway_test.go +0 -676
- package/assets/go/gateway/go.mod +0 -5
- package/assets/go/gateway/groups.go +0 -66
- package/assets/go/gateway/main.go +0 -45
- package/dist/build-spk/gateway.d.ts +0 -31
- package/dist/build-spk/gateway.js +0 -60
- package/dist/compose/privilege.d.ts +0 -14
- package/dist/compose/privilege.js +0 -102
package/assets/go/daemon/main.go
DELETED
|
@@ -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
|
-
}
|