@synopackageland/cli 0.7.0 → 0.7.2
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 +301 -1
- package/assets/default-icon/README.md +19 -0
- package/assets/default-icon/default-icon-small.svg +9 -0
- package/assets/default-icon/default-icon.svg +10 -0
- package/assets/default-icon/icon_16.png +0 -0
- package/assets/default-icon/icon_24.png +0 -0
- package/assets/default-icon/icon_256.png +0 -0
- package/assets/default-icon/icon_32.png +0 -0
- package/assets/default-icon/icon_48.png +0 -0
- package/assets/default-icon/icon_64.png +0 -0
- package/assets/default-icon/icon_72.png +0 -0
- package/dist/add.d.ts +4 -1
- package/dist/add.js +14 -9
- package/dist/build-spk/dsm-icons.d.ts +31 -2
- package/dist/build-spk/dsm-icons.js +135 -56
- package/dist/build-spk/dsm-platforms.json +4 -1
- package/dist/build-spk/gateway.d.ts +1 -1
- package/dist/build-spk/gateway.js +8 -4
- package/dist/build-spk/generator.js +17 -3
- package/dist/build-spk/go-toolchain.d.ts +20 -0
- package/dist/build-spk/go-toolchain.js +44 -0
- package/dist/build-spk/png.d.ts +14 -0
- package/dist/build-spk/png.js +161 -0
- package/dist/build-spk/ui-texts.d.ts +20 -0
- package/dist/build-spk/ui-texts.js +42 -0
- package/dist/cli.js +39 -9
- package/dist/contract/i18n.d.ts +5 -0
- package/dist/contract/i18n.js +45 -0
- package/dist/contract/icon.d.ts +5 -0
- package/dist/contract/icon.js +27 -0
- package/dist/contract/validate.js +4 -0
- package/dist/contract/web-launch.js +6 -2
- package/dist/deploy/native-install-values.d.ts +17 -0
- package/dist/deploy/native-install-values.js +32 -0
- package/dist/deploy/plan.d.ts +6 -2
- package/dist/deploy/plan.js +57 -6
- package/dist/error-registry.js +15 -6
- package/dist/host/dsm-client.js +53 -17
- package/dist/host/dsm-http.js +24 -2
- package/dist/host/types.d.ts +1 -0
- package/dist/info.js +28 -9
- package/dist/init.js +1 -1
- package/dist/runtime/dsm-version.d.ts +6 -0
- package/dist/runtime/dsm-version.js +8 -0
- package/dist/runtime/executor-build.d.ts +1 -1
- package/dist/runtime/executor-build.js +8 -5
- package/dist/standalone/daemon.d.ts +2 -1
- package/dist/standalone/daemon.js +10 -6
- package/dist/standalone/dsm-window.d.ts +2 -0
- package/dist/standalone/dsm-window.js +26 -40
- package/dist/standalone/i18n.d.ts +12 -0
- package/dist/standalone/i18n.js +30 -0
- package/dist/standalone/inspect-spk.d.ts +5 -0
- package/dist/standalone/inspect-spk.js +41 -2
- package/dist/standalone/manifest.d.ts +5 -6
- package/dist/standalone/manifest.js +21 -34
- package/dist/standalone/native-authorization-window.d.ts +1 -1
- package/dist/standalone/native-authorization-window.js +7 -1
- package/dist/standalone/native-layout.d.ts +2 -0
- package/dist/standalone/native-layout.js +22 -1
- package/dist/standalone/native-root-window.d.ts +4 -2
- package/dist/standalone/native-root-window.js +33 -18
- package/dist/standalone/root-window.d.ts +4 -1
- package/dist/standalone/root-window.js +143 -33
- package/dist/standalone/spk.js +7 -4
- package/dist/templates/_daemon/go.mod +1 -1
- package/dist/templates/_daemon/main.go +6 -3
- package/dist/templates/_daemon/main_test.go +21 -1
- package/dist/templates/_daemon/testdata/child.go +3 -0
- package/dist/templates/_executor/README.md +4 -1
- package/dist/templates/_executor/anchor.go +10 -0
- package/dist/templates/_executor/anchor_test.go +40 -5
- package/dist/templates/_executor/digest_review_test.go +2 -2
- package/dist/templates/_executor/dsm.go +40 -3
- package/dist/templates/_executor/go.mod +2 -0
- package/dist/templates/_executor/main.go +11 -5
- package/dist/templates/_executor/managed.go +3 -3
- package/dist/templates/_executor/managed_test.go +3 -3
- package/dist/templates/_executor/path_test.go +18 -0
- package/dist/templates/_executor/pid.go +37 -0
- package/dist/templates/_executor/pid_test.go +113 -0
- package/dist/templates/_executor/platform_other.go +8 -0
- package/dist/templates/_executor/purge_log.go +18 -0
- package/dist/templates/_executor/tasks.go +45 -7
- package/dist/templates/_executor/tasks_dsm71_test.go +114 -0
- package/dist/templates/_executor/tasks_test.go +58 -1
- package/dist/templates/_executor/upgrade_watch_test.go +13 -5
- package/dist/templates/_gateway/go.mod +2 -0
- package/dist/templates/binary/dsm-package.yaml +3 -0
- package/dist/templates/compose/synology-app.yaml +3 -0
- package/dist/templates/native-go/backend/go.mod +1 -1
- package/dist/templates/native-go/backend/internal/filestation/client.go +39 -3
- package/dist/templates/native-go/backend/internal/filestation/download_dsm71_test.go +82 -0
- package/dist/templates/native-go/backend/internal/identity/README.md +5 -1
- package/dist/templates/native-go/backend/internal/identity/dsm.go +24 -5
- package/dist/templates/native-go/backend/internal/identity/http.go +1 -1
- package/dist/templates/native-go/backend/internal/identity/identity_test.go +48 -1
- package/dist/templates/native-go/backend/internal/identity/service.go +1 -1
- package/dist/templates/native-go/dsm-package.yaml +3 -0
- package/dist/templates/native-go-delegate/frontend/test/delegate.test.ts +15 -0
- package/dist/templates/native-go-root-ops/backend/internal/app/executor.go +30 -2
- package/dist/templates/native-go-root-ops/backend/internal/app/executor_test.go +64 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/ExecutorSettings.vue +3 -2
- package/dist/templates/native-go-vue/frontend/src/delegate/executor-settings.test.ts +23 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/executor-setup.ts +5 -1
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/de.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/en.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/es.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/fr.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/it.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/ja.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/ko.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/zh-CN.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/zh-TW.json +1 -0
- package/package.json +2 -1
- package/schema/synology-app.schema.json +22 -0
- package/templates/_daemon/go.mod +1 -1
- package/templates/_daemon/main.go +6 -3
- package/templates/_daemon/main_test.go +21 -1
- package/templates/_daemon/testdata/child.go +3 -0
- package/templates/_executor/README.md +4 -1
- package/templates/_executor/anchor.go +10 -0
- package/templates/_executor/anchor_test.go +40 -5
- package/templates/_executor/digest_review_test.go +2 -2
- package/templates/_executor/dsm.go +40 -3
- package/templates/_executor/go.mod +2 -0
- package/templates/_executor/main.go +11 -5
- package/templates/_executor/managed.go +3 -3
- package/templates/_executor/managed_test.go +3 -3
- package/templates/_executor/path_test.go +18 -0
- package/templates/_executor/pid.go +37 -0
- package/templates/_executor/pid_test.go +113 -0
- package/templates/_executor/platform_other.go +8 -0
- package/templates/_executor/purge_log.go +18 -0
- package/templates/_executor/tasks.go +45 -7
- package/templates/_executor/tasks_dsm71_test.go +114 -0
- package/templates/_executor/tasks_test.go +58 -1
- package/templates/_executor/upgrade_watch_test.go +13 -5
- package/templates/_gateway/go.mod +2 -0
- package/templates/binary/dsm-package.yaml +3 -0
- package/templates/compose/synology-app.yaml +3 -0
- package/templates/native-go/backend/go.mod +1 -1
- package/templates/native-go/backend/internal/filestation/client.go +39 -3
- package/templates/native-go/backend/internal/filestation/download_dsm71_test.go +82 -0
- package/templates/native-go/backend/internal/identity/README.md +5 -1
- package/templates/native-go/backend/internal/identity/dsm.go +24 -5
- package/templates/native-go/backend/internal/identity/http.go +1 -1
- package/templates/native-go/backend/internal/identity/identity_test.go +48 -1
- package/templates/native-go/backend/internal/identity/service.go +1 -1
- package/templates/native-go/dsm-package.yaml +3 -0
- package/templates/native-go-delegate/frontend/test/delegate.test.ts +15 -0
- package/templates/native-go-root-ops/backend/internal/app/executor.go +30 -2
- package/templates/native-go-root-ops/backend/internal/app/executor_test.go +64 -0
- package/templates/native-go-vue/frontend/src/delegate/ExecutorSettings.vue +3 -2
- package/templates/native-go-vue/frontend/src/delegate/executor-settings.test.ts +23 -0
- package/templates/native-go-vue/frontend/src/delegate/executor-setup.ts +5 -1
- package/templates/native-go-vue/frontend/src/delegate/i18n/de.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/en.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/es.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/fr.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/it.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/ja.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/ko.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/zh-CN.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/zh-TW.json +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
14
|
+
"assets",
|
|
14
15
|
"CHANGELOG.md",
|
|
15
16
|
"templates",
|
|
16
17
|
"schema",
|
|
@@ -43,6 +43,28 @@
|
|
|
43
43
|
"type": "string",
|
|
44
44
|
"minLength": 1
|
|
45
45
|
},
|
|
46
|
+
"icon": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"minLength": 1,
|
|
49
|
+
"description": "Package icon inside the project: a square PNG of at least 256x256, or a directory of icon_<size>.png (16, 24, 32, 48, 64, 72, 256; icon_256.png required). Omit to use the SDK default icon."
|
|
50
|
+
},
|
|
51
|
+
"i18n": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"description": "Translations; only info is allowed.",
|
|
54
|
+
"properties": {
|
|
55
|
+
"info": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"description": "Package Center INFO and DSM main menu and window title per locale (zh-TW, zh-CN, ja, ko, de, fr, es, it); English stays top level.",
|
|
58
|
+
"additionalProperties": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {
|
|
61
|
+
"displayname": { "type": "string", "minLength": 1 },
|
|
62
|
+
"description": { "type": "string", "minLength": 1 }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
46
68
|
"adminprotocol": {
|
|
47
69
|
"type": "string"
|
|
48
70
|
},
|
package/templates/_daemon/go.mod
CHANGED
|
@@ -28,6 +28,9 @@ type deadlines struct {
|
|
|
28
28
|
Start int `json:"start"`
|
|
29
29
|
Stop int `json:"stop"`
|
|
30
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"`
|
|
31
34
|
}
|
|
32
35
|
type config struct {
|
|
33
36
|
Package string `json:"package"`
|
|
@@ -88,7 +91,7 @@ func load(dest, data string) (*controller, error) {
|
|
|
88
91
|
if cfg.Exec == "" || filepath.IsAbs(cfg.Exec) || filepath.Clean(cfg.Exec) != cfg.Exec || strings.HasPrefix(cfg.Exec, "../") {
|
|
89
92
|
return nil, errors.New("invalid fixed executable")
|
|
90
93
|
}
|
|
91
|
-
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 {
|
|
94
|
+
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 {
|
|
92
95
|
return nil, errors.New("invalid daemon deadlines")
|
|
93
96
|
}
|
|
94
97
|
hash := sha256.Sum256(raw)
|
|
@@ -162,7 +165,7 @@ func (c *controller) run(action string) (int, error) {
|
|
|
162
165
|
budget += c.Timeouts.Start
|
|
163
166
|
}
|
|
164
167
|
if action == "uninstall" {
|
|
165
|
-
budget += c.Timeouts.
|
|
168
|
+
budget += c.Timeouts.Uninstall + 2
|
|
166
169
|
}
|
|
167
170
|
if action != "start" && action != "stop" && action != "status" && action != "restart" && action != "uninstall" {
|
|
168
171
|
return 1, errors.New("unsupported daemon action")
|
|
@@ -414,7 +417,7 @@ func (c *controller) uninstall() error {
|
|
|
414
417
|
select {
|
|
415
418
|
case err := <-done:
|
|
416
419
|
return err
|
|
417
|
-
case <-time.After(time.Duration(c.Timeouts.
|
|
420
|
+
case <-time.After(time.Duration(c.Timeouts.Uninstall) * time.Second):
|
|
418
421
|
cmd.Process.Signal(syscall.SIGTERM)
|
|
419
422
|
}
|
|
420
423
|
select {
|
|
@@ -50,7 +50,7 @@ func fixture(t *testing.T, mode string) (string, string, string) {
|
|
|
50
50
|
if raw, err := exec.Command(app, "cleanup").CombinedOutput(); err != nil {
|
|
51
51
|
t.Fatalf("payload warm-up: %v %s", err, raw)
|
|
52
52
|
}
|
|
53
|
-
cfg := config{Package: "DaemonFixture", Exec: "app", Args: []string{"${SYNOPKG_PKGVAR}/a space", "$(touch BAD)'", "${SYNOPKG_PKGDEST}"}, Env: map[string]string{"APP_MODE": mode}, Timeouts: deadlines{Start: 2, Stop: 2, Status: 1}}
|
|
53
|
+
cfg := config{Package: "DaemonFixture", Exec: "app", Args: []string{"${SYNOPKG_PKGVAR}/a space", "$(touch BAD)'", "${SYNOPKG_PKGDEST}"}, Env: map[string]string{"APP_MODE": mode}, Timeouts: deadlines{Start: 2, Stop: 2, Status: 1, Uninstall: 2}}
|
|
54
54
|
if os.Geteuid() == 0 {
|
|
55
55
|
cfg.Package = "nobody"
|
|
56
56
|
} else {
|
|
@@ -250,6 +250,26 @@ func TestUninstallCleanup(t *testing.T) {
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
// A revoke on uninstall can outlast the stop deadline on a loaded NAS; it has
|
|
254
|
+
// its own budget instead of Stop-1.
|
|
255
|
+
func TestUninstallCommandHasItsOwnBudget(t *testing.T) {
|
|
256
|
+
control, dest, data := fixture(t, "normal")
|
|
257
|
+
path := filepath.Join(dest, "daemon/config.json")
|
|
258
|
+
raw, _ := os.ReadFile(path)
|
|
259
|
+
var cfg config
|
|
260
|
+
json.Unmarshal(raw, &cfg)
|
|
261
|
+
cfg.UninstallArgs = []string{"cleanup-slow"}
|
|
262
|
+
cfg.Timeouts.Uninstall = 5
|
|
263
|
+
raw, _ = json.Marshal(cfg)
|
|
264
|
+
os.WriteFile(path, raw, 0644)
|
|
265
|
+
if rc, out := runControl(control, "start", dest, data); rc != 0 {
|
|
266
|
+
t.Fatalf("start: %d %s", rc, out)
|
|
267
|
+
}
|
|
268
|
+
if rc, out := runControl(control, "uninstall", dest, data); rc != 0 {
|
|
269
|
+
t.Fatalf("slow uninstall within its budget failed: %d %s", rc, out)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
253
273
|
func TestPackageAccountIdentity(t *testing.T) {
|
|
254
274
|
control, dest, data := fixture(t, "normal")
|
|
255
275
|
path := filepath.Join(dest, "daemon/config.json")
|
|
@@ -174,7 +174,10 @@ root-owned `/var/packages/<package>/INFO`, the root monitor copies it into
|
|
|
174
174
|
`runtime/`, verifies the copy and runs root `control start`, which replaces the
|
|
175
175
|
monitor without reapproval. Any other change stays paused for administrator
|
|
176
176
|
approval. Only root's pinned boot/control path or this anchored install can
|
|
177
|
-
replace a monitor or runtime.
|
|
177
|
+
replace a monitor or runtime. To exercise this path on a NAS without a real
|
|
178
|
+
executor change, the repository's `scripts/test/executor-bump.py` makes a
|
|
179
|
+
build+1 SPK whose executor differs by one byte and rewrites the INFO anchor,
|
|
180
|
+
`executor/setup.json` and the window script's embedded pins together.
|
|
178
181
|
|
|
179
182
|
At start, scheduler readback must exactly match the canonical lifecycle scripts
|
|
180
183
|
regenerated from the package identity and approved policy pin. Package
|
|
@@ -84,6 +84,11 @@ func installAnchored(ctx context.Context, source, info string, trusted func(os.F
|
|
|
84
84
|
if err != nil {
|
|
85
85
|
return err
|
|
86
86
|
}
|
|
87
|
+
// DSM links the package target to the volume; openRegular refuses links.
|
|
88
|
+
source, err = filepath.EvalSymlinks(source)
|
|
89
|
+
if err != nil {
|
|
90
|
+
return err
|
|
91
|
+
}
|
|
87
92
|
src, err := openRegular(filepath.Join(source, "executor"))
|
|
88
93
|
if err != nil {
|
|
89
94
|
return err
|
|
@@ -129,7 +134,12 @@ func startInstalled(runtime, pkg string) error {
|
|
|
129
134
|
return cmd.Process.Release()
|
|
130
135
|
}
|
|
131
136
|
|
|
137
|
+
// An anchored upgrade is handed to root start, which replaces this monitor:
|
|
138
|
+
// only differing pins are an approval request, and only they say so.
|
|
132
139
|
func approvedPins(state pinState, install, start func() error, log func(string)) bool {
|
|
140
|
+
if state == pinsDiffer {
|
|
141
|
+
log("executor paused: package pins require administrator approval")
|
|
142
|
+
}
|
|
133
143
|
if state != pinsAnchored {
|
|
134
144
|
return state == pinsMatch
|
|
135
145
|
}
|
|
@@ -5,6 +5,7 @@ import (
|
|
|
5
5
|
"errors"
|
|
6
6
|
"os"
|
|
7
7
|
"path/filepath"
|
|
8
|
+
"reflect"
|
|
8
9
|
"strings"
|
|
9
10
|
"testing"
|
|
10
11
|
)
|
|
@@ -101,6 +102,8 @@ func TestInstallAnchoredReplacesOnlyTheProtectedExecutorWithVerifiedBytes(t *tes
|
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
// The pause message names administrator approval only when the pins differ:
|
|
106
|
+
// an anchored handover is a finished upgrade, not a request for approval.
|
|
104
107
|
func TestApprovedPinsHandOverAnAnchoredUpgradeToRootStart(t *testing.T) {
|
|
105
108
|
for _, tc := range []struct {
|
|
106
109
|
name string
|
|
@@ -109,18 +112,50 @@ func TestApprovedPinsHandOverAnAnchoredUpgradeToRootStart(t *testing.T) {
|
|
|
109
112
|
open bool
|
|
110
113
|
installs int
|
|
111
114
|
starts int
|
|
115
|
+
logged string
|
|
112
116
|
}{
|
|
113
|
-
{"match", pinsMatch, nil, true, 0, 0},
|
|
114
|
-
{"differ", pinsDiffer, nil, false, 0, 0},
|
|
115
|
-
{"anchored", pinsAnchored, nil, false, 1, 1},
|
|
116
|
-
{"install failed", pinsAnchored, errors.New("digest"), false, 1, 0},
|
|
117
|
+
{"match", pinsMatch, nil, true, 0, 0, ""},
|
|
118
|
+
{"differ", pinsDiffer, nil, false, 0, 0, "executor paused: package pins require administrator approval"},
|
|
119
|
+
{"anchored", pinsAnchored, nil, false, 1, 1, "executor upgrade: INFO-anchored executor installed under the approved policy"},
|
|
120
|
+
{"install failed", pinsAnchored, errors.New("digest"), false, 1, 0, "executor upgrade refused: INFO-anchored executor could not be installed"},
|
|
117
121
|
} {
|
|
118
122
|
t.Run(tc.name, func(t *testing.T) {
|
|
119
123
|
installs, starts := 0, 0
|
|
120
|
-
|
|
124
|
+
var logged []string
|
|
125
|
+
open := approvedPins(tc.state, func() error { installs++; return tc.installErr }, func() error { starts++; return nil }, func(s string) { logged = append(logged, s) })
|
|
121
126
|
if open != tc.open || installs != tc.installs || starts != tc.starts {
|
|
122
127
|
t.Fatal("handover", open, installs, starts)
|
|
123
128
|
}
|
|
129
|
+
if want := []string{tc.logged}; tc.logged == "" {
|
|
130
|
+
if len(logged) != 0 {
|
|
131
|
+
t.Fatal("matching pins logged", logged)
|
|
132
|
+
}
|
|
133
|
+
} else if !reflect.DeepEqual(logged, want) {
|
|
134
|
+
t.Fatal("log", logged, want)
|
|
135
|
+
}
|
|
124
136
|
})
|
|
125
137
|
}
|
|
126
138
|
}
|
|
139
|
+
|
|
140
|
+
// DSM: /var/packages/<pkg>/target links to /volume1/@appstore/<pkg>.
|
|
141
|
+
func TestInstallAnchoredFollowsTheDSMTargetLink(t *testing.T) {
|
|
142
|
+
ctx := context.Background()
|
|
143
|
+
anchor := hashBytes([]byte("new executor"))
|
|
144
|
+
appstore, info := anchoredPackage(t, "new executor", "approved policy", anchor)
|
|
145
|
+
link := filepath.Join(filepath.Dir(info), "linked-target")
|
|
146
|
+
if err := os.Symlink(filepath.Dir(appstore), link); err != nil {
|
|
147
|
+
t.Fatal(err)
|
|
148
|
+
}
|
|
149
|
+
runtime := realTestPath(t, t.TempDir())
|
|
150
|
+
os.WriteFile(filepath.Join(runtime, "executor"), []byte("approved executor"), 0700)
|
|
151
|
+
target := filepath.Join(link, "executor")
|
|
152
|
+
if state := packagePins(ctx, target, info, trustAll, hashBytes([]byte("approved executor")), hashBytes([]byte("approved policy"))); state != pinsAnchored {
|
|
153
|
+
t.Fatal("pin state", state)
|
|
154
|
+
}
|
|
155
|
+
if err := installAnchored(ctx, target, info, trustAll, runtime); err != nil {
|
|
156
|
+
t.Fatal("anchored executor behind the target link refused", err)
|
|
157
|
+
}
|
|
158
|
+
if b, _ := os.ReadFile(filepath.Join(runtime, "executor")); string(b) != "new executor" {
|
|
159
|
+
t.Fatal("installed", string(b))
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -32,7 +32,7 @@ func TestReviewLargePackageHashDoesNotBlockShutdown(t *testing.T) {
|
|
|
32
32
|
done <- manageService(ctx, make(chan string), func() (func(), error) { return func() {}, nil }, func(ctx context.Context) bool {
|
|
33
33
|
close(entered)
|
|
34
34
|
return samePackagePins(ctx, dir, strings.Repeat("a", 64), strings.Repeat("b", 64))
|
|
35
|
-
}, func() error { return nil }
|
|
35
|
+
}, func() error { return nil })
|
|
36
36
|
}()
|
|
37
37
|
<-entered
|
|
38
38
|
time.Sleep(20 * time.Millisecond)
|
|
@@ -93,7 +93,7 @@ func TestLifecycleCancelsPendingPinCheck(t *testing.T) {
|
|
|
93
93
|
actions := make(chan string)
|
|
94
94
|
done := make(chan error, 1)
|
|
95
95
|
go func() {
|
|
96
|
-
done <- manageService(ctx, actions, func() (func(), error) { t.Error("opened during canceled check"); return func() {}, nil }, func(ctx context.Context) bool { close(entered); <-ctx.Done(); close(canceled); return false }, func() error { return nil }
|
|
96
|
+
done <- manageService(ctx, actions, func() (func(), error) { t.Error("opened during canceled check"); return func() {}, nil }, func(ctx context.Context) bool { close(entered); <-ctx.Done(); close(canceled); return false }, func() error { return nil })
|
|
97
97
|
}()
|
|
98
98
|
<-entered
|
|
99
99
|
if action == "shutdown" {
|
|
@@ -78,11 +78,32 @@ type envelope struct {
|
|
|
78
78
|
Data json.RawMessage `json:"data"`
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// dsmError keeps the DSM error code of a refused call; the message stays generic.
|
|
82
|
+
type dsmError struct{ Code int }
|
|
83
|
+
|
|
84
|
+
func (*dsmError) Error() string { return "DSM refused" }
|
|
85
|
+
|
|
81
86
|
func decodeEnvelope(b []byte) (json.RawMessage, error) {
|
|
87
|
+
data, err := decodeSuccess(b)
|
|
88
|
+
if err == nil && (len(data) == 0 || string(data) == "null") {
|
|
89
|
+
return nil, &dsmError{}
|
|
90
|
+
}
|
|
91
|
+
return data, err
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// decodeSuccess accepts a success without data: scheduler deletes answer
|
|
95
|
+
// {"success":true}, sometimes with httpd_restart, and that confirms the delete.
|
|
96
|
+
func decodeSuccess(b []byte) (json.RawMessage, error) {
|
|
82
97
|
var r envelope
|
|
83
98
|
d := json.NewDecoder(bytes.NewReader(b))
|
|
84
|
-
if uniqueValue(d, false) != nil || hasDSMError(b) || json.Unmarshal(b, &r) != nil || !r.Success
|
|
85
|
-
|
|
99
|
+
if uniqueValue(d, false) != nil || hasDSMError(b) || json.Unmarshal(b, &r) != nil || !r.Success {
|
|
100
|
+
var refused struct {
|
|
101
|
+
Error struct {
|
|
102
|
+
Code int `json:"code"`
|
|
103
|
+
} `json:"error"`
|
|
104
|
+
}
|
|
105
|
+
json.Unmarshal(b, &refused)
|
|
106
|
+
return nil, &dsmError{Code: refused.Error.Code}
|
|
86
107
|
}
|
|
87
108
|
return r.Data, nil
|
|
88
109
|
}
|
|
@@ -117,6 +138,22 @@ func (d *liveDSM) Verify(ctx context.Context, cookie, token string) (string, err
|
|
|
117
138
|
return out.Username, nil
|
|
118
139
|
}
|
|
119
140
|
func (d *liveDSM) call(ctx context.Context, api, method, version, runner string, params map[string]any) (json.RawMessage, error) {
|
|
141
|
+
b, err := d.exec(ctx, api, method, version, runner, params)
|
|
142
|
+
if err != nil {
|
|
143
|
+
return nil, err
|
|
144
|
+
}
|
|
145
|
+
return decodeEnvelope(b)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// mutate runs a call whose success carries no data, such as a scheduler delete.
|
|
149
|
+
func (d *liveDSM) mutate(ctx context.Context, api, method, version string, params map[string]any) error {
|
|
150
|
+
b, err := d.exec(ctx, api, method, version, "", params)
|
|
151
|
+
if err == nil {
|
|
152
|
+
_, err = decodeSuccess(b)
|
|
153
|
+
}
|
|
154
|
+
return err
|
|
155
|
+
}
|
|
156
|
+
func (d *liveDSM) exec(ctx context.Context, api, method, version, runner string, params map[string]any) ([]byte, error) {
|
|
120
157
|
args := []string{"--exec", "api=" + api, "method=" + method, "version=" + version}
|
|
121
158
|
if runner != "" {
|
|
122
159
|
args = append(args, "runner="+runner)
|
|
@@ -140,7 +177,7 @@ func (d *liveDSM) call(ctx context.Context, api, method, version, runner string,
|
|
|
140
177
|
}
|
|
141
178
|
return nil, errors.New("DSM command")
|
|
142
179
|
}
|
|
143
|
-
return
|
|
180
|
+
return b, nil
|
|
144
181
|
}
|
|
145
182
|
func (d *liveDSM) Account(ctx context.Context, name string) (Subject, error) {
|
|
146
183
|
s, err := d.Accounts.Lookup(name)
|
|
@@ -360,7 +360,7 @@ func serve(e *Executor, uid, gid int, revived bool) error {
|
|
|
360
360
|
purge := func() error {
|
|
361
361
|
return purgeProtected(ctx, d, e.Policy.Package, filepath.Join(stateBase, e.Policy.Package), runtime, sockDir)
|
|
362
362
|
}
|
|
363
|
-
return manageService(ctx, actions, open, samePins, purge
|
|
363
|
+
return manageService(ctx, actions, open, samePins, purge)
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
func openService(parent context.Context, e *Executor, uid, gid int, sock string) (func(), error) {
|
|
@@ -438,11 +438,12 @@ func openService(parent context.Context, e *Executor, uid, gid int, sock string)
|
|
|
438
438
|
}, nil
|
|
439
439
|
}
|
|
440
440
|
func purgeProtected(ctx context.Context, d *liveDSM, pkg, state, runtime, socket string) error {
|
|
441
|
+
logf := purgeLogger()
|
|
441
442
|
inv, err := readTaskInventory(runtime)
|
|
442
443
|
if err != nil {
|
|
443
|
-
|
|
444
|
+
logf("task cleanup skipped: protected inventory unavailable: package=" + pkg)
|
|
444
445
|
} else {
|
|
445
|
-
return purgeWithInventory(ctx, d, pkg, inv, state, runtime, socket,
|
|
446
|
+
return purgeWithInventory(ctx, d, pkg, inv, state, runtime, socket, logf)
|
|
446
447
|
}
|
|
447
448
|
return finishControl(state, runtime, socket, true)
|
|
448
449
|
}
|
|
@@ -515,8 +516,13 @@ func verifiedPID(f *os.File, runtime string) (int, error) {
|
|
|
515
516
|
if err != nil || pid <= 1 {
|
|
516
517
|
return 0, errors.New("pid")
|
|
517
518
|
}
|
|
518
|
-
|
|
519
|
-
|
|
519
|
+
exe := fmt.Sprintf("/proc/%d/exe", pid)
|
|
520
|
+
link, err := os.Readlink(exe)
|
|
521
|
+
if err != nil {
|
|
522
|
+
return 0, errors.New("pid executable mismatch")
|
|
523
|
+
}
|
|
524
|
+
running, err := os.Stat(exe)
|
|
525
|
+
if err != nil || !approvedExecutable(link, running, runtime, rootOwnedReadOnly) {
|
|
520
526
|
return 0, errors.New("pid executable mismatch")
|
|
521
527
|
}
|
|
522
528
|
return pid, nil
|
|
@@ -22,8 +22,9 @@ func samePackagePins(ctx context.Context, dir, executorDigest, policyDigest stri
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// The process and monitor outlive its listener. A package start can only expose
|
|
25
|
-
// the already-approved runtime when the shipped bytes still match both pins
|
|
26
|
-
|
|
25
|
+
// the already-approved runtime when the shipped bytes still match both pins;
|
|
26
|
+
// samePins reports why it refused.
|
|
27
|
+
func manageService(ctx context.Context, actions <-chan string, open func() (func(), error), samePins func(context.Context) bool, purge func() error) error {
|
|
27
28
|
var closeService func()
|
|
28
29
|
var checking <-chan bool
|
|
29
30
|
var cancelCheck context.CancelFunc
|
|
@@ -62,7 +63,6 @@ func manageService(ctx context.Context, actions <-chan string, open func() (func
|
|
|
62
63
|
return nil
|
|
63
64
|
}
|
|
64
65
|
if !matches {
|
|
65
|
-
log("executor paused: package pins require administrator approval")
|
|
66
66
|
continue
|
|
67
67
|
}
|
|
68
68
|
var err error
|
|
@@ -15,7 +15,7 @@ import (
|
|
|
15
15
|
func TestPauseResumeOnlyApprovedPinsAndPurgeWhilePaused(t *testing.T) {
|
|
16
16
|
for _, changed := range []bool{false, true} {
|
|
17
17
|
t.Run(map[bool]string{false: "same pins", true: "different policy"}[changed], func(t *testing.T) {
|
|
18
|
-
dir := t.TempDir()
|
|
18
|
+
dir := realTestPath(t, t.TempDir())
|
|
19
19
|
os.WriteFile(filepath.Join(dir, "executor"), []byte("approved executable"), 0600)
|
|
20
20
|
os.WriteFile(filepath.Join(dir, "policy.json"), []byte("approved policy"), 0600)
|
|
21
21
|
ed, _ := digestFile(context.Background(), filepath.Join(dir, "executor"), maxExecutorBytes)
|
|
@@ -34,7 +34,7 @@ func TestPauseResumeOnlyApprovedPinsAndPurgeWhilePaused(t *testing.T) {
|
|
|
34
34
|
}
|
|
35
35
|
opened.Add(1)
|
|
36
36
|
return func() { listener.Close(); closed.Add(1) }, nil
|
|
37
|
-
}, func(ctx context.Context) bool { return samePackagePins(ctx, dir, ed, pd) }, func() error { purged.Add(1); return nil }
|
|
37
|
+
}, func(ctx context.Context) bool { return samePackagePins(ctx, dir, ed, pd) }, func() error { purged.Add(1); return nil })
|
|
38
38
|
}()
|
|
39
39
|
await := func(v *atomic.Int32, want int32) {
|
|
40
40
|
t.Helper()
|
|
@@ -105,7 +105,7 @@ func TestRootControlSignalsPauseThenPurgeWithoutMailbox(t *testing.T) {
|
|
|
105
105
|
var opened, closed, purged atomic.Int32
|
|
106
106
|
done := make(chan error, 1)
|
|
107
107
|
go func() {
|
|
108
|
-
done <- manageService(ctx, actions, func() (func(), error) { opened.Add(1); return func() { closed.Add(1) }, nil }, func(context.Context) bool { return true }, func() error { purged.Add(1); return nil }
|
|
108
|
+
done <- manageService(ctx, actions, func() (func(), error) { opened.Add(1); return func() { closed.Add(1) }, nil }, func(context.Context) bool { return true }, func() error { purged.Add(1); return nil })
|
|
109
109
|
}()
|
|
110
110
|
wait := func(value *atomic.Int32) {
|
|
111
111
|
t.Helper()
|
|
@@ -15,3 +15,21 @@ func realTestPath(t *testing.T, p string) string {
|
|
|
15
15
|
return p
|
|
16
16
|
}
|
|
17
17
|
func makeTestDir(p string) error { return os.Mkdir(p, 0700) }
|
|
18
|
+
|
|
19
|
+
// DSM links /var/packages/<pkg>/target to the volume. Every platform must
|
|
20
|
+
// refuse a symlinked ancestor so tests catch callers that skip EvalSymlinks.
|
|
21
|
+
func TestOpenRegularRefusesASymlinkedAncestor(t *testing.T) {
|
|
22
|
+
dir := realTestPath(t, t.TempDir())
|
|
23
|
+
os.MkdirAll(filepath.Join(dir, "appstore"), 0755)
|
|
24
|
+
os.WriteFile(filepath.Join(dir, "appstore", "executor"), []byte("x"), 0755)
|
|
25
|
+
os.Symlink(filepath.Join(dir, "appstore"), filepath.Join(dir, "target"))
|
|
26
|
+
if f, err := openRegular(filepath.Join(dir, "target", "executor")); err == nil {
|
|
27
|
+
f.Close()
|
|
28
|
+
t.Fatal("symlinked ancestor accepted")
|
|
29
|
+
}
|
|
30
|
+
f, err := openRegular(filepath.Join(dir, "appstore", "executor"))
|
|
31
|
+
if err != nil {
|
|
32
|
+
t.Fatal(err)
|
|
33
|
+
}
|
|
34
|
+
f.Close()
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"syscall"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// approvedExecutable reports whether a /proc/<pid>/exe link and the inode it
|
|
11
|
+
// runs (stat of that link) belong to an executor root installed in runtime.
|
|
12
|
+
// The inode decides, not the name: kernel 3.10 (DSM 7.1) names a
|
|
13
|
+
// rename-replaced inode after the rename source, so a monitor whose executor
|
|
14
|
+
// was replaced reads as .executor (control.sh) or .executor-* (installAnchored)
|
|
15
|
+
// "(deleted)". Only root writes runtime (root:root 0700), and a replaced inode
|
|
16
|
+
// must be unlinked on the runtime's device.
|
|
17
|
+
func approvedExecutable(link string, running os.FileInfo, runtime string, trusted func(os.FileInfo) bool) bool {
|
|
18
|
+
name, deleted := strings.CutSuffix(link, " (deleted)")
|
|
19
|
+
base := filepath.Base(name)
|
|
20
|
+
if filepath.Dir(name) != runtime || (base != "executor" && !strings.HasPrefix(base, ".executor")) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
if !running.Mode().IsRegular() || !trusted(running) {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
if !deleted {
|
|
27
|
+
current, err := os.Lstat(filepath.Join(runtime, "executor"))
|
|
28
|
+
return err == nil && os.SameFile(running, current)
|
|
29
|
+
}
|
|
30
|
+
dir, err := os.Stat(runtime)
|
|
31
|
+
if err != nil {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
st, ok := running.Sys().(*syscall.Stat_t)
|
|
35
|
+
dt, dirOK := dir.Sys().(*syscall.Stat_t)
|
|
36
|
+
return ok && dirOK && st.Nlink == 0 && st.Dev == dt.Dev
|
|
37
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// runningExecutor installs runtime/executor and keeps it open, as a live monitor.
|
|
10
|
+
func runningExecutor(t *testing.T) (string, *os.File) {
|
|
11
|
+
t.Helper()
|
|
12
|
+
runtime := t.TempDir()
|
|
13
|
+
path := filepath.Join(runtime, "executor")
|
|
14
|
+
if err := os.WriteFile(path, []byte("old"), 0700); err != nil {
|
|
15
|
+
t.Fatal(err)
|
|
16
|
+
}
|
|
17
|
+
f, err := os.Open(path)
|
|
18
|
+
if err != nil {
|
|
19
|
+
t.Fatal(err)
|
|
20
|
+
}
|
|
21
|
+
t.Cleanup(func() { f.Close() })
|
|
22
|
+
return runtime, f
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// replaceExecutor renames a new copy over runtime/executor, as control.sh and
|
|
26
|
+
// installAnchored do; the running inode is unlinked.
|
|
27
|
+
func replaceExecutor(t *testing.T, runtime, staged string) {
|
|
28
|
+
t.Helper()
|
|
29
|
+
if err := os.WriteFile(filepath.Join(runtime, staged), []byte("new"), 0700); err != nil {
|
|
30
|
+
t.Fatal(err)
|
|
31
|
+
}
|
|
32
|
+
if err := os.Rename(filepath.Join(runtime, staged), filepath.Join(runtime, "executor")); err != nil {
|
|
33
|
+
t.Fatal(err)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func stat(t *testing.T, f *os.File) os.FileInfo {
|
|
38
|
+
t.Helper()
|
|
39
|
+
info, err := f.Stat()
|
|
40
|
+
if err != nil {
|
|
41
|
+
t.Fatal(err)
|
|
42
|
+
}
|
|
43
|
+
return info
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func TestApprovedExecutableAcceptsTheLiveRuntimeExecutor(t *testing.T) {
|
|
47
|
+
runtime, f := runningExecutor(t)
|
|
48
|
+
if !approvedExecutable(filepath.Join(runtime, "executor"), stat(t, f), runtime, trustAll) {
|
|
49
|
+
t.Fatal("live runtime executor refused")
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Kernel 3.10 (DSM 7.1 armv7) names a rename-replaced inode after the rename
|
|
54
|
+
// source: control.sh stages .executor, installAnchored stages .executor-*.
|
|
55
|
+
func TestApprovedExecutableAcceptsAReplacedExecutorUnderItsSwappedName(t *testing.T) {
|
|
56
|
+
for _, staged := range []string{".executor", ".executor-123456"} {
|
|
57
|
+
runtime, f := runningExecutor(t)
|
|
58
|
+
replaceExecutor(t, runtime, staged)
|
|
59
|
+
for _, name := range []string{staged, "executor"} {
|
|
60
|
+
link := filepath.Join(runtime, name) + " (deleted)"
|
|
61
|
+
if !approvedExecutable(link, stat(t, f), runtime, trustAll) {
|
|
62
|
+
t.Fatalf("replaced executor refused: %s", link)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func TestApprovedExecutableRefusesAnotherInode(t *testing.T) {
|
|
69
|
+
runtime, _ := runningExecutor(t)
|
|
70
|
+
other := filepath.Join(runtime, ".executor")
|
|
71
|
+
if err := os.WriteFile(other, []byte("other"), 0700); err != nil {
|
|
72
|
+
t.Fatal(err)
|
|
73
|
+
}
|
|
74
|
+
f, err := os.Open(other)
|
|
75
|
+
if err != nil {
|
|
76
|
+
t.Fatal(err)
|
|
77
|
+
}
|
|
78
|
+
defer f.Close()
|
|
79
|
+
running := stat(t, f)
|
|
80
|
+
// Still linked: not the installed executor, whatever the link claims.
|
|
81
|
+
for _, link := range []string{other, other + " (deleted)", filepath.Join(runtime, "executor"), filepath.Join(runtime, "executor") + " (deleted)"} {
|
|
82
|
+
if approvedExecutable(link, running, runtime, trustAll) {
|
|
83
|
+
t.Fatalf("linked non-executor accepted as %s", link)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func TestApprovedExecutableRefusesAnUnlinkedInodeOutsideTheRuntime(t *testing.T) {
|
|
89
|
+
runtime, f := runningExecutor(t)
|
|
90
|
+
replaceExecutor(t, runtime, ".executor")
|
|
91
|
+
running := stat(t, f)
|
|
92
|
+
for _, link := range []string{
|
|
93
|
+
filepath.Join(t.TempDir(), "executor") + " (deleted)",
|
|
94
|
+
filepath.Join(runtime, "sh") + " (deleted)",
|
|
95
|
+
filepath.Join(runtime, ".executor"), // unlinked yet not reported deleted
|
|
96
|
+
} {
|
|
97
|
+
if approvedExecutable(link, running, runtime, trustAll) {
|
|
98
|
+
t.Fatalf("unlinked inode accepted as %s", link)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func TestApprovedExecutableRefusesAnUntrustedInode(t *testing.T) {
|
|
104
|
+
runtime, f := runningExecutor(t)
|
|
105
|
+
refuse := func(os.FileInfo) bool { return false }
|
|
106
|
+
if approvedExecutable(filepath.Join(runtime, "executor"), stat(t, f), runtime, refuse) {
|
|
107
|
+
t.Fatal("untrusted live executor accepted")
|
|
108
|
+
}
|
|
109
|
+
replaceExecutor(t, runtime, ".executor")
|
|
110
|
+
if approvedExecutable(filepath.Join(runtime, ".executor")+" (deleted)", stat(t, f), runtime, refuse) {
|
|
111
|
+
t.Fatal("untrusted replaced executor accepted")
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -8,6 +8,7 @@ import (
|
|
|
8
8
|
"net"
|
|
9
9
|
"os"
|
|
10
10
|
"os/exec"
|
|
11
|
+
"path/filepath"
|
|
11
12
|
"strings"
|
|
12
13
|
"syscall"
|
|
13
14
|
)
|
|
@@ -19,7 +20,14 @@ var errLinux = errors.New("executor requires Linux")
|
|
|
19
20
|
func ownedByRoot(os.FileInfo) bool { return false }
|
|
20
21
|
func configureProcess(*exec.Cmd) {}
|
|
21
22
|
func peerUID(net.Conn) (int, error) { return -1, errLinux }
|
|
23
|
+
|
|
24
|
+
// Like Linux openBeneath, refuse a symlink in any component, not only the last.
|
|
22
25
|
func openRegular(path string) (*os.File, error) {
|
|
26
|
+
for dir := filepath.Dir(path); dir != filepath.Dir(dir); dir = filepath.Dir(dir) {
|
|
27
|
+
if info, err := os.Lstat(dir); err != nil || info.Mode()&os.ModeSymlink != 0 {
|
|
28
|
+
return nil, errors.New("path")
|
|
29
|
+
}
|
|
30
|
+
}
|
|
23
31
|
info, err := os.Lstat(path)
|
|
24
32
|
if err != nil {
|
|
25
33
|
return nil, err
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"log"
|
|
5
|
+
"log/syslog"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// Purge removes executor.log with the protected state, so its diagnostics
|
|
9
|
+
// also go to the system log, where they survive the uninstall.
|
|
10
|
+
func purgeLogger() func(string) {
|
|
11
|
+
system, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_DAEMON, "synopkgland-executor")
|
|
12
|
+
return func(s string) {
|
|
13
|
+
log.Print(s)
|
|
14
|
+
if err == nil {
|
|
15
|
+
_ = system.Notice(s)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|