@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
|
@@ -1,23 +1,47 @@
|
|
|
1
1
|
import { bootDefinition, controlDefinition } from '../runtime/executor-tasks.js';
|
|
2
2
|
import { executorScopeTask } from '../runtime/executor-scope-task.js';
|
|
3
|
+
import { versionUnsupported } from '../runtime/dsm-version.js';
|
|
3
4
|
export { executorTasks as rootTasks } from '../runtime/executor-tasks.js';
|
|
4
5
|
/** Exact readback is authoritative. PasswordConfirm tokens are reused immediately,
|
|
5
6
|
* across both Root APIs, and never persisted (DSM validity is about 300 seconds). */
|
|
6
7
|
export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
7
8
|
var eventAPI = "SYNO.Core.EventScheduler", taskAPI = "SYNO.Core.TaskScheduler";
|
|
8
|
-
var busy = null;
|
|
9
|
-
|
|
9
|
+
var busy = null, legacy = false;
|
|
10
|
+
var versionUnsupported = ${versionUnsupported.toString()};
|
|
11
|
+
function fail(code, detail) { var e = new Error(code); e.code = code; if (detail) e.detail = detail; return e; }
|
|
10
12
|
function reason(e) {
|
|
11
13
|
var code = String(e && e.code || "executor_bad_response");
|
|
14
|
+
if (e && e.passwordConfirm) return code + (code === "dsm_3121" ? ": DSM rejected the administrator password." : ": administrator password confirmation failed.") + " Nothing was created.";
|
|
15
|
+
if (e && e.detail) return code + ": " + e.detail;
|
|
12
16
|
return code + (code === "105" || code === "dsm_105" ? ": DSM denied administrator scheduler access. Check the DSM session and scheduler; a NAS restart resolved this on the measured DSM. No automatic retry was made." : ": scheduler readback or operation failed.");
|
|
13
17
|
}
|
|
14
18
|
// AppWindow.sendWebAPI encodes JSON parameters. Preserve their native types;
|
|
15
19
|
// pre-encoding turns objects/booleans into strings and DSM rejects create.
|
|
16
20
|
function send(api, method, version, params) { return options.send(api, method, version, params || {}); }
|
|
21
|
+
// DSM 7.1 has no Root APIs: TaskScheduler stops at list v2 (offset) and get v3.
|
|
22
|
+
// Only an unsupported method version on a read selects that profile, which
|
|
23
|
+
// then creates through the plain scheduler APIs. Writes never fall back.
|
|
24
|
+
function taskRead(method, version, params, legacyVersion, legacyParams) {
|
|
25
|
+
if (legacy) return send(taskAPI, method, legacyVersion, legacyParams);
|
|
26
|
+
return send(taskAPI, method, version, params).catch(function (e) {
|
|
27
|
+
if (!versionUnsupported(e && e.code)) throw e;
|
|
28
|
+
legacy = true;
|
|
29
|
+
return send(taskAPI, method, legacyVersion, legacyParams);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function create(boot, params, confirmation) {
|
|
33
|
+
if (legacy) return send(boot ? eventAPI : taskAPI, "create", boot ? 1 : 3, params);
|
|
34
|
+
params.SynoConfirmPWToken = confirmation.token;
|
|
35
|
+
return send(boot ? eventAPI + ".Root" : taskAPI + ".Root", "create", boot ? 1 : 4, params);
|
|
36
|
+
}
|
|
37
|
+
function remove(task, ref) {
|
|
38
|
+
var boot = task.kind === "boot";
|
|
39
|
+
return send(boot ? eventAPI : taskAPI, "delete", boot ? 1 : 2, boot ? ref : {tasks: [ref]});
|
|
40
|
+
}
|
|
17
41
|
function admin() { if (!options.user() || options.user().admin !== true) throw fail("admin_required"); }
|
|
18
42
|
function find(task) {
|
|
19
43
|
var boot = task.kind === "boot";
|
|
20
|
-
return
|
|
44
|
+
return (boot ? send(eventAPI, "list", 1, {}) : taskRead("list", 3, {start: 0, limit: 500}, 2, {offset: 0, limit: 500})).then(function (data) {
|
|
21
45
|
var records = boot ? data : data && data.tasks;
|
|
22
46
|
if (!Array.isArray(records) || records.length > 500 || (!boot && (!Number.isInteger(data.total) || data.total !== records.length))) throw fail("executor_bad_response");
|
|
23
47
|
var named = records.filter(function (r) { return r && (boot ? r.task_name : r.name) === task.name; });
|
|
@@ -26,7 +50,7 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
26
50
|
var summary = named[0];
|
|
27
51
|
if (!boot && !/^[0-9]+$/.test(String(summary.id))) throw fail("executor_bad_response");
|
|
28
52
|
var ref = boot ? {task_name: task.name} : {id: summary.id, real_owner: "root"};
|
|
29
|
-
var detail =
|
|
53
|
+
var detail = boot ? send(eventAPI, "get", 1, ref) : taskRead("get", 4, ref, 3, ref);
|
|
30
54
|
return detail.then(function (r) {
|
|
31
55
|
var owner = r && r.owner;
|
|
32
56
|
var root = owner === "root" || (boot && owner && owner["0"] === "root");
|
|
@@ -49,6 +73,16 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
49
73
|
return result;
|
|
50
74
|
});
|
|
51
75
|
}
|
|
76
|
+
// Removes, newest first, the tasks one approval created and still reads as
|
|
77
|
+
// exactly ours. Resolves with the last refusal's reason, if any.
|
|
78
|
+
function withdraw(created) {
|
|
79
|
+
var failed;
|
|
80
|
+
return created.reduceRight(function (withdrawn, task) {
|
|
81
|
+
return withdrawn.then(function () { return find(task); }).then(function (current) {
|
|
82
|
+
if (current.state === "ready") return remove(task, current.ref);
|
|
83
|
+
}).catch(function (e) { failed = reason(e); });
|
|
84
|
+
}, Promise.resolve()).then(function () { return failed; });
|
|
85
|
+
}
|
|
52
86
|
function definition(task) { return task.kind === "boot" ? (${bootDefinition.toString()})(task) : (${controlDefinition.toString()})(task); }
|
|
53
87
|
function approve(password, selectedScope) {
|
|
54
88
|
var selectionTask;
|
|
@@ -56,10 +90,13 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
56
90
|
return read().then(function (before) {
|
|
57
91
|
if (before.state === "blocked" || before.state === "unsupported") return before;
|
|
58
92
|
if (before.state === "ready" && !selectionTask) return start(before);
|
|
59
|
-
return options.confirmPassword(password).
|
|
93
|
+
return options.confirmPassword(password).catch(function (e) {
|
|
94
|
+
throw {code: e && e.code, passwordConfirm: true};
|
|
95
|
+
}).then(function (confirmation) {
|
|
60
96
|
if (!confirmation || confirmation.confirmed !== true) return {state: "cancelled", tasks: before.tasks};
|
|
61
|
-
|
|
62
|
-
|
|
97
|
+
// DSM 7.1 confirms the password server-side but issues no create token.
|
|
98
|
+
if (!legacy && (typeof confirmation.token !== "string" || !confirmation.token)) throw fail("executor_confirmation_missing");
|
|
99
|
+
var errors = {}, created = [], withdrawFailed;
|
|
63
100
|
// Delete only a verified, same-header outdated task, then create the pinned
|
|
64
101
|
// replacement. set has no measured wire contract. Re-read before each write.
|
|
65
102
|
var chain = Promise.resolve();
|
|
@@ -68,22 +105,35 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
68
105
|
return find(task).then(function (current) {
|
|
69
106
|
if (current.state === "ready") return;
|
|
70
107
|
if (current.state === "blocked") throw fail(current.reason);
|
|
71
|
-
var
|
|
72
|
-
|
|
73
|
-
return removed.then(function () {
|
|
74
|
-
var params = definition(task); params.SynoConfirmPWToken = confirmation.token;
|
|
75
|
-
return send(boot ? eventAPI + ".Root" : taskAPI + ".Root", "create", boot ? 1 : 4, params);
|
|
76
|
-
});
|
|
108
|
+
var removed = current.ref ? remove(task, current.ref) : Promise.resolve();
|
|
109
|
+
return removed.then(function () { return create(task.kind === "boot", definition(task), confirmation); }).then(function () { created.push(task); });
|
|
77
110
|
}).catch(function (e) { errors[task.kind] = reason(e); });
|
|
78
111
|
});
|
|
79
112
|
});
|
|
80
|
-
|
|
113
|
+
// A partial approval must not leave an enabled root bootup task that no
|
|
114
|
+
// control task or uninstall inventory owns: withdraw what it created.
|
|
115
|
+
return chain.then(function () {
|
|
116
|
+
if (Object.keys(errors).length) return withdraw(created).then(function (failed) { withdrawFailed = failed; });
|
|
117
|
+
}).then(read).then(function (after) {
|
|
81
118
|
Object.keys(errors).forEach(function (kind) {
|
|
82
119
|
if (after.tasks[kind].state !== "ready") { after.tasks[kind].state = "blocked"; after.tasks[kind].reason = errors[kind]; after.state = "blocked"; after.reason = errors[kind]; }
|
|
83
120
|
});
|
|
121
|
+
if (withdrawFailed) { after.state = "blocked"; after.reason = (after.reason ? after.reason + " " : "") + "Withdrawing the tasks this approval created failed: " + withdrawFailed; }
|
|
84
122
|
// A scope change must not restart a running executor: the boot task
|
|
85
123
|
// stops it, which interrupts in-flight copies instead of canceling them.
|
|
86
124
|
return after.state === "ready" ? (selectionTask ? running(after) : Promise.resolve(null)).then(function (live) { return live || start(after); }).then(function (started) {
|
|
125
|
+
// Without a running executor there is no task inventory, so uninstall
|
|
126
|
+
// cannot find these tasks: withdraw what this approval created.
|
|
127
|
+
if (started.state === "ready" || !created.length) return started;
|
|
128
|
+
return withdraw(created).then(function (failed) {
|
|
129
|
+
return read().then(function (current) {
|
|
130
|
+
current.state = "blocked";
|
|
131
|
+
current.reason = started.reason + (failed ? " Withdrawing the tasks this approval created failed: " + failed : " The tasks this approval created were withdrawn.");
|
|
132
|
+
current.retry = !failed;
|
|
133
|
+
return current;
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}).then(function (started) {
|
|
87
137
|
if (!selectionTask || started.state !== "ready") return started;
|
|
88
138
|
var generation = started.executor.selection ? started.executor.selection.generation : 0;
|
|
89
139
|
if (started.executor.selection && (!Number.isSafeInteger(generation) || generation <= 0)) throw fail("executor_selection_invalid");
|
|
@@ -102,23 +152,20 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
102
152
|
function selectScope(task, confirmation, verified, selectedScope) {
|
|
103
153
|
function removeExact() {
|
|
104
154
|
return find(task).then(function (current) {
|
|
105
|
-
if (current.state === "ready") return
|
|
155
|
+
if (current.state === "ready") return remove(task, current.ref);
|
|
106
156
|
if (current.ref) throw fail("executor_selection_cleanup_conflict");
|
|
107
157
|
});
|
|
108
158
|
}
|
|
109
159
|
return find(task).then(function (current) {
|
|
110
160
|
if (current.state === "blocked" || current.state === "unsupported") throw fail("executor_selection_conflict");
|
|
111
|
-
var removed = current.ref ?
|
|
112
|
-
return removed.then(function () {
|
|
113
|
-
var params = definition(task); params.SynoConfirmPWToken = confirmation.token;
|
|
114
|
-
return send(taskAPI + ".Root", "create", 4, params);
|
|
115
|
-
});
|
|
161
|
+
var removed = current.ref ? remove(task, current.ref) : Promise.resolve();
|
|
162
|
+
return removed.then(function () { return create(false, definition(task), confirmation); });
|
|
116
163
|
}).then(function () { return find(task); }).then(function (current) {
|
|
117
164
|
if (current.state !== "ready") throw fail("executor_selection_readback_mismatch");
|
|
118
165
|
return send(taskAPI, "run", 2, {tasks: [current.ref]});
|
|
119
166
|
}).then(function () {
|
|
120
167
|
function readSelection(attempts) {
|
|
121
|
-
return health(verified,
|
|
168
|
+
return health(verified, 0).then(function (current) {
|
|
122
169
|
if (current.state === "ready" && current.executor.selection && current.executor.selection.scope === selectedScope && Number.isSafeInteger(current.executor.selection.generation) && current.executor.selection.generation > 0) return current;
|
|
123
170
|
if (attempts > 1 && options.wait) return options.wait(500).then(function () { verified.state = "ready"; return readSelection(attempts - 1); });
|
|
124
171
|
throw fail("executor_selection_unconfirmed");
|
|
@@ -129,21 +176,35 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
129
176
|
return removeExact().then(function () { throw error; });
|
|
130
177
|
});
|
|
131
178
|
}
|
|
132
|
-
|
|
179
|
+
// Why a status reply is not the approved, running executor; null when it is.
|
|
180
|
+
function mismatch(status) {
|
|
181
|
+
if (!status || status.ready !== true) return "the approved executor is not answering" + (status && (status.reason || status.state) ? " (" + (status.reason || status.state) + ")" : "") + ".";
|
|
182
|
+
if (!options.pins || status.executor_digest !== options.pins.executorDigest || status.policy_digest !== options.pins.policyDigest) return "the running executor reports other digests than this package pins.";
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
function pinned(status) { return mismatch(status) === null; }
|
|
133
186
|
function running(verified) {
|
|
134
187
|
return options.call("executor.status", {}).then(function (status) {
|
|
135
188
|
if (!pinned(status)) return null;
|
|
136
189
|
verified.executor = status; return verified;
|
|
137
190
|
}, function () { return null; });
|
|
138
191
|
}
|
|
139
|
-
|
|
192
|
+
// budget: how long to keep asking after a start (ms). The wait doubles from
|
|
193
|
+
// 500 ms to 2 s so an open window does not poll the executor twice a second.
|
|
194
|
+
function health(verified, budget, delay) {
|
|
140
195
|
if (verified.state !== "ready") return Promise.resolve(verified);
|
|
196
|
+
delay = Math.min(delay || 500, 2000);
|
|
141
197
|
return options.call("executor.status", {}).then(function (status) {
|
|
142
|
-
|
|
198
|
+
var why = mismatch(status);
|
|
199
|
+
if (why) throw fail("executor_status_mismatch", why);
|
|
143
200
|
verified.executor = status; return verified;
|
|
201
|
+
}, function (e) {
|
|
202
|
+
throw fail(e && e.code, "the executor status request failed.");
|
|
144
203
|
}).catch(function (e) {
|
|
145
|
-
if (
|
|
146
|
-
|
|
204
|
+
if (budget >= delay && options.wait) return options.wait(delay).then(function () { return health(verified, budget - delay, delay * 2); });
|
|
205
|
+
// The tasks are approved: approving again runs start, which restarts the
|
|
206
|
+
// executor without a password, so the panel offers it.
|
|
207
|
+
verified.state = "blocked"; verified.reason = reason(e); verified.retry = true; return verified;
|
|
147
208
|
});
|
|
148
209
|
}
|
|
149
210
|
function start(verified) {
|
|
@@ -151,13 +212,17 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
151
212
|
return send(eventAPI, "run", 1, verified.tasks.boot.ref);
|
|
152
213
|
}).then(function () {
|
|
153
214
|
// run can succeed for a nonexistent task. Read both definitions again.
|
|
154
|
-
return read().then(function (after) { return health(after,
|
|
215
|
+
return read().then(function (after) { return health(after, 10000); });
|
|
155
216
|
}).catch(function (e) { verified.state = "blocked"; verified.reason = reason(e); return verified; });
|
|
156
217
|
}
|
|
218
|
+
// The window panel and the app's own settings share this access; each
|
|
219
|
+
// result reaches every view so neither shows a stale state.
|
|
220
|
+
var listeners = [];
|
|
221
|
+
function publish(result) { listeners.forEach(function (listener) { listener(result); }); return result; }
|
|
157
222
|
return {
|
|
158
|
-
status: function () { return read().then(function (verified) { return health(verified,
|
|
223
|
+
status: function () { return read().then(function (verified) { return health(verified, 0); }).then(publish); },
|
|
159
224
|
authorize: function (password, selectedScope) {
|
|
160
|
-
if (!busy) busy = Promise.resolve().then(function () { return approve(password, selectedScope); }).catch(function (e) { return {state: "blocked", reason: reason(e), tasks: {}}; }).then(function (r) { busy = null; return r; });
|
|
225
|
+
if (!busy) busy = Promise.resolve().then(function () { return approve(password, selectedScope); }).catch(function (e) { return {state: "blocked", reason: reason(e), tasks: {}, retry: !!(e && e.passwordConfirm)}; }).then(function (r) { busy = null; return publish(r); });
|
|
161
226
|
return busy;
|
|
162
227
|
},
|
|
163
228
|
control: function (action) {
|
|
@@ -166,9 +231,10 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
166
231
|
if (verified.state !== "ready") throw fail("executor_not_ready");
|
|
167
232
|
return options.call("executor.control", {action: action}).then(function () {
|
|
168
233
|
return send(taskAPI, "run", 2, {tasks: [verified.tasks.control.ref]});
|
|
169
|
-
}).then(read).then(function (after) { return action === "start" ? health(after,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
234
|
+
}).then(read).then(function (after) { return action === "start" ? health(after, 10000) : after; });
|
|
235
|
+
}).then(publish);
|
|
236
|
+
},
|
|
237
|
+
subscribe: function (listener) { listeners.push(listener); }
|
|
172
238
|
};
|
|
173
239
|
}`;
|
|
174
240
|
export const ROOT_PANEL = `function mountRootPanel(host, access, i18n, tasks, admin) {
|
|
@@ -176,9 +242,53 @@ export const ROOT_PANEL = `function mountRootPanel(host, access, i18n, tasks, ad
|
|
|
176
242
|
var status = document.createElement("p"), approve = document.createElement("button"), retry = document.createElement("button");
|
|
177
243
|
approve.textContent = "Approve background access"; retry.textContent = "Check status";
|
|
178
244
|
host.appendChild(status); host.appendChild(approve); host.appendChild(retry);
|
|
179
|
-
function show(result) { status.textContent = result.state + (result.reason ? ": " + result.reason : ""); approve.hidden = result.state !== "needs-admin-approval"; }
|
|
245
|
+
function show(result) { status.textContent = result.state + (result.reason ? ": " + result.reason : ""); approve.hidden = result.state !== "needs-admin-approval" && result.retry !== true; }
|
|
180
246
|
function run(fn) { approve.disabled = retry.disabled = true; return fn().then(show, function(e) { show({state: "blocked", reason: e.message || e.code}); }).then(function() { approve.disabled = retry.disabled = false; }); }
|
|
247
|
+
access.subscribe(show);
|
|
181
248
|
approve.onclick = function() { return run(function() { return access.authorize(); }); };
|
|
182
249
|
retry.onclick = function() { return run(function() { return access.status(); }); };
|
|
183
250
|
run(function() { return access.status().then(function(result) { return result.tasks && result.tasks.boot && result.tasks.control && result.tasks.boot.state === "ready" && result.tasks.control.state === "ready" ? access.control("start") : result; }); });
|
|
184
251
|
}`;
|
|
252
|
+
/** Administrator password confirmation in the window's own session: a supplied
|
|
253
|
+
* password, or a masked form when none is given. */
|
|
254
|
+
export const PASSWORD_CONFIRM = `function createPasswordConfirm(win) {
|
|
255
|
+
var versionUnsupported = ${versionUnsupported.toString()};
|
|
256
|
+
function send(password, version) {
|
|
257
|
+
return new Promise(function (resolve, reject) {
|
|
258
|
+
win.sendWebAPI({api: "SYNO.Core.User.PasswordConfirm", method: "auth", version: version, params: {password: password}, scope: win,
|
|
259
|
+
callback: function (success, response) {
|
|
260
|
+
if (!success) reject({code: "dsm_" + ((response && response.code) || "error")});
|
|
261
|
+
else resolve({confirmed: true, token: response && response.SynoConfirmPWToken});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
// DSM 7.1 has PasswordConfirm v1 only, which checks the password but issues no
|
|
267
|
+
// token. Only an unsupported version is retried; a wrong password is not.
|
|
268
|
+
function auth(password) {
|
|
269
|
+
return send(password, 2).catch(function (error) {
|
|
270
|
+
if (!versionUnsupported(error.code)) throw error;
|
|
271
|
+
return send(password, 1);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return function (suppliedPassword) {
|
|
275
|
+
if (typeof suppliedPassword === "string" && suppliedPassword) return auth(suppliedPassword);
|
|
276
|
+
return new Promise(function (resolve, reject) {
|
|
277
|
+
var form = document.createElement("form"), label = document.createElement("label"), input = document.createElement("input");
|
|
278
|
+
var submit = document.createElement("button"), cancel = document.createElement("button");
|
|
279
|
+
label.textContent = "Confirm your DSM administrator password";
|
|
280
|
+
input.type = "password"; input.autocomplete = "current-password"; input.required = true;
|
|
281
|
+
label.appendChild(input); form.appendChild(label);
|
|
282
|
+
submit.type = "submit"; submit.textContent = "Approve"; form.appendChild(submit);
|
|
283
|
+
cancel.type = "button"; cancel.textContent = "Cancel"; form.appendChild(cancel);
|
|
284
|
+
win.body.dom.appendChild(form); input.focus();
|
|
285
|
+
function close() { input.value = ""; if (form.parentNode) form.parentNode.removeChild(form); }
|
|
286
|
+
cancel.onclick = function () { close(); resolve({confirmed: false}); };
|
|
287
|
+
form.onsubmit = function (event) {
|
|
288
|
+
event.preventDefault(); submit.disabled = cancel.disabled = true;
|
|
289
|
+
var password = input.value; input.value = "";
|
|
290
|
+
auth(password).then(function (result) { close(); resolve(result); }, function (error) { close(); reject(error); });
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
}`;
|
package/dist/standalone/spk.js
CHANGED
|
@@ -4,7 +4,7 @@ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync,
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { infoArchValue } from "../build-spk/dsm-arch.js";
|
|
7
|
-
import { writeDsmWindowIcons, writePackageCenterIcons } from "../build-spk/dsm-icons.js";
|
|
7
|
+
import { packageIcons, writeDsmWindowIcons, writePackageCenterIcons } from "../build-spk/dsm-icons.js";
|
|
8
8
|
import { renderNativePrivilege } from "../build-spk/native-lifecycle.js";
|
|
9
9
|
import { renderUninstallWizardFiles } from "../build-spk/uninstall-wizard.js";
|
|
10
10
|
import { renderInstallWizardFiles, renderNativeInstallWizard } from "../build-spk/install-wizard.js";
|
|
@@ -13,7 +13,8 @@ import { renderDelegateScopeSteps } from '../build-spk/delegate-scope.js';
|
|
|
13
13
|
import { createReproducibleTar } from "../build-spk/reproducible-archive.js";
|
|
14
14
|
import { renderStandaloneScript } from "./lifecycle.js";
|
|
15
15
|
import { DSM_LANGS, LOCALES } from "./i18n.js";
|
|
16
|
-
import { CGI_PAYLOAD_PATH, renderUiConfig, renderWindowScript, windowScriptName } from "./dsm-window.js";
|
|
16
|
+
import { appTitle, CGI_PAYLOAD_PATH, renderUiConfig, renderWindowScript, windowScriptName } from "./dsm-window.js";
|
|
17
|
+
import { writeUiTexts } from "../build-spk/ui-texts.js";
|
|
17
18
|
import { copyStandaloneNotices } from "../build-spk/notices.js";
|
|
18
19
|
import { hostScriptName, renderHostAdapterScript } from "./host-adapter.js";
|
|
19
20
|
import { copyDaemonPayload } from "./daemon.js";
|
|
@@ -54,6 +55,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
54
55
|
try {
|
|
55
56
|
if ((manifest.capabilities.rootOps || manifest.capabilities.delegate) && !options.executor)
|
|
56
57
|
throw new StandaloneBuildError("EXECUTOR_PIN_REQUIRED", "Executor artifact required.");
|
|
58
|
+
const icons = packageIcons(manifest.icon);
|
|
57
59
|
const uiRoot = join(staging, "package", "ui");
|
|
58
60
|
const appDir = join(uiRoot, "app");
|
|
59
61
|
copyUiTree(manifest.ui.dir, appDir);
|
|
@@ -61,7 +63,8 @@ export async function assembleStandaloneSpk(options) {
|
|
|
61
63
|
writeFile(join(uiRoot, "config"), renderUiConfig(manifest));
|
|
62
64
|
writeFile(join(uiRoot, windowScriptName(manifest)), renderWindowScript(manifest, assetVersion));
|
|
63
65
|
writeFile(join(uiRoot, hostScriptName(manifest.build)), renderHostAdapterScript(manifest.package));
|
|
64
|
-
writeDsmWindowIcons(uiRoot);
|
|
66
|
+
writeDsmWindowIcons(uiRoot, icons);
|
|
67
|
+
writeUiTexts(uiRoot, appTitle(manifest));
|
|
65
68
|
const cgiTarget = join(staging, "package", CGI_PAYLOAD_PATH);
|
|
66
69
|
mkdirSync(dirname(cgiTarget), { recursive: true });
|
|
67
70
|
copyFileSync(options.cgiPath, cgiTarget);
|
|
@@ -129,7 +132,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
129
132
|
...(folder ? renderSharedFolderUpgradeWizardFiles(manifest.package, folder) : {}),
|
|
130
133
|
...renderUninstallWizardFiles(manifest.package, "native", { executor: Boolean(options.executor) }),
|
|
131
134
|
});
|
|
132
|
-
writePackageCenterIcons(staging);
|
|
135
|
+
writePackageCenterIcons(staging, icons);
|
|
133
136
|
mkdirSync(dirname(options.outputPath), { recursive: true });
|
|
134
137
|
await createReproducibleTar(staging, options.outputPath);
|
|
135
138
|
}
|
|
@@ -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)
|