@synopackageland/cli 0.10.2 → 0.10.3
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 +215 -1696
- package/README.md +21 -2
- package/assets/login-style/mask.png +0 -0
- package/assets/login-style/wallpaper-thumbnail.jpg +0 -0
- package/assets/login-style/wallpaper.jpg +0 -0
- package/dist/build-spk/generator.js +10 -1
- package/dist/build-spk/login-style.d.ts +55 -0
- package/dist/build-spk/login-style.js +115 -0
- package/dist/cli.js +22 -1
- package/dist/error-registry.js +37 -1
- package/dist/host/dsm-client.js +6 -9
- package/dist/init.js +9 -5
- package/dist/runtime/delegate-contract.d.ts +2 -92
- package/dist/runtime/delegate-contract.js +2 -24
- package/dist/runtime/executor-digests.json +11 -0
- package/dist/runtime/executor-policy-check.d.ts +1 -1
- package/dist/runtime/executor-policy-check.js +5 -7
- package/dist/runtime/executor-policy.d.ts +2 -0
- package/dist/runtime/executor-policy.js +5 -1
- package/dist/runtime/root-programs.d.ts +2 -0
- package/dist/runtime/sdk-source.d.ts +4 -0
- package/dist/runtime/sdk-source.js +7 -0
- package/dist/runtime/sdk-timeouts.d.ts +23 -0
- package/dist/runtime/sdk-timeouts.js +44 -0
- package/dist/skill.js +1 -0
- package/dist/standalone/app-scripts.d.ts +26 -0
- package/dist/standalone/app-scripts.js +141 -0
- package/dist/standalone/bridge-app.d.ts +2 -0
- package/dist/standalone/bridge-app.js +9 -1
- package/dist/standalone/bridge-backend.d.ts +1 -1
- package/dist/standalone/build.js +5 -2
- package/dist/standalone/dsm-window.d.ts +14 -3
- package/dist/standalone/dsm-window.js +24 -11
- package/dist/standalone/host-adapter.d.ts +1 -1
- package/dist/standalone/host-adapter.js +1 -1
- package/dist/standalone/host-file-navigation.d.ts +6 -2
- package/dist/standalone/host-file-navigation.js +8 -2
- package/dist/standalone/inspect-spk.d.ts +9 -0
- package/dist/standalone/inspect-spk.js +60 -6
- package/dist/standalone/lifecycle.js +3 -1
- package/dist/standalone/manifest.d.ts +6 -0
- package/dist/standalone/manifest.js +51 -3
- package/dist/standalone/portal-window.d.ts +6 -6
- package/dist/standalone/portal-window.js +12 -32
- package/dist/standalone/request-timeout.d.ts +6 -3
- package/dist/standalone/request-timeout.js +10 -4
- package/dist/standalone/root-ops.d.ts +3 -2
- package/dist/standalone/root-ops.js +9 -3
- package/dist/standalone/root-window.d.ts +2 -2
- package/dist/standalone/root-window.js +173 -49
- package/dist/standalone/sdk-check.js +4 -1
- package/dist/standalone/spk.d.ts +3 -0
- package/dist/standalone/spk.js +10 -3
- package/dist/validate.js +2 -0
- package/package.json +2 -3
- package/skills/SKILL.md +3 -1
- package/skills/reference/create.md +5 -1
- package/skills/reference/identity.md +1 -1
- package/skills/reference/lifecycle.md +110 -0
- package/skills/reference/portal.md +12 -0
- package/dist/authorization-sdk.d.ts +0 -6
- package/dist/authorization-sdk.js +0 -6
- package/dist/standalone/native-authorization-window.d.ts +0 -2
- package/dist/standalone/native-authorization-window.js +0 -36
- package/dist/standalone/native-root-window.d.ts +0 -29
- package/dist/standalone/native-root-window.js +0 -307
|
@@ -27,8 +27,9 @@ export declare function loadRootOps(dir: string): RootOpsResult;
|
|
|
27
27
|
export declare function rootBinaryProblem(ops: readonly RootOp[], settings: RootSettings | undefined, arches: readonly NativeArchFamily[]): string | undefined;
|
|
28
28
|
/**
|
|
29
29
|
* dsm-package.yaml `root`: `programs.<name>` gives a root/ program its kind,
|
|
30
|
-
* params schema and run or job limits; `
|
|
31
|
-
* runs
|
|
30
|
+
* params schema and run or job limits; `start` names the program the executor
|
|
31
|
+
* runs once after each install or upgrade, `stop` the one it runs at package
|
|
32
|
+
* stop and upgrade; `state: true` gives root programs a
|
|
32
33
|
* directory only root can read. Returns why the settings are refused.
|
|
33
34
|
*/
|
|
34
35
|
export declare function readRootSettings(value: unknown, ops: readonly RootOp[] | undefined): {
|
|
@@ -91,12 +91,13 @@ function readRootOp(path, file) {
|
|
|
91
91
|
}
|
|
92
92
|
return { name, body };
|
|
93
93
|
}
|
|
94
|
-
const ROOT_SETTINGS_FIELDS = ["programs", "stop", "state"];
|
|
94
|
+
const ROOT_SETTINGS_FIELDS = ["programs", "start", "stop", "state"];
|
|
95
95
|
const PROGRAM_FIELDS = ["kind", "schema", "run", "job"];
|
|
96
96
|
/**
|
|
97
97
|
* dsm-package.yaml `root`: `programs.<name>` gives a root/ program its kind,
|
|
98
|
-
* params schema and run or job limits; `
|
|
99
|
-
* runs
|
|
98
|
+
* params schema and run or job limits; `start` names the program the executor
|
|
99
|
+
* runs once after each install or upgrade, `stop` the one it runs at package
|
|
100
|
+
* stop and upgrade; `state: true` gives root programs a
|
|
100
101
|
* directory only root can read. Returns why the settings are refused.
|
|
101
102
|
*/
|
|
102
103
|
export function readRootSettings(value, ops) {
|
|
@@ -118,6 +119,11 @@ export function readRootSettings(value, ops) {
|
|
|
118
119
|
if (problem)
|
|
119
120
|
return fail(`programs.${name}: ${problem}`);
|
|
120
121
|
}
|
|
122
|
+
if (raw.start !== undefined && (typeof raw.start !== "string" || !names.has(raw.start)))
|
|
123
|
+
return fail(`start must be a program name in root/, not ${JSON.stringify(raw.start)}.`);
|
|
124
|
+
const start = typeof raw.start === "string" ? programs[raw.start] : undefined;
|
|
125
|
+
if (start && (start.run !== undefined || start.job !== undefined))
|
|
126
|
+
return fail(`start: root program ${raw.start} runs once after each install or upgrade and cannot also declare run or job; give the session work another program name.`);
|
|
121
127
|
if (raw.stop !== undefined && (typeof raw.stop !== "string" || !names.has(raw.stop)))
|
|
122
128
|
return fail(`stop must be a program name in root/, not ${JSON.stringify(raw.stop)}.`);
|
|
123
129
|
if (raw.state !== undefined && raw.state !== true)
|
|
@@ -3,13 +3,13 @@ export { type ExecutorTask as RootTask } from '../runtime/executor-tasks.js';
|
|
|
3
3
|
* across both Root APIs, and never persisted (DSM validity is about 300 seconds). */
|
|
4
4
|
export declare const ROOT_RUNTIME: string;
|
|
5
5
|
type PanelLocale = "en" | "zh-TW" | "zh-CN" | "ja" | "ko" | "de" | "fr" | "es" | "it";
|
|
6
|
-
/** The plain-UI window panel's text
|
|
6
|
+
/** The plain-UI window panel's text: a state, and a result's code and outcome; reason stays the runtime's English detail. */
|
|
7
7
|
export declare const ROOT_PANEL_MESSAGES: Record<string, Record<PanelLocale, string>>;
|
|
8
8
|
/** ROOT_PANEL_MESSAGES as createI18n catalogs, one per locale. */
|
|
9
9
|
export declare function rootPanelCatalogs(): Record<string, Record<string, string>>;
|
|
10
10
|
/** The plain-UI approval panel: it asks for the password itself and passes it
|
|
11
11
|
* to `access.authorize`, the same PasswordConfirm path as ExecutorSettings. */
|
|
12
|
-
export declare const ROOT_PANEL = "function mountRootPanel(host, access, t, admin) {\n if (!admin) return;\n function el(tag, className) { var e = document.createElement(tag); if (className) e.className = className; return e; }\n var panel = el(\"div\", \"dsmapp-rootpanel\"), title = el(\"h2\"), status = el(\"p\"), reason = el(\"p\", \"dsmapp-rootpanel-hint\");\n var form = el(\"form\"), label = el(\"label\"), password = el(\"input\"), actions = el(\"div\", \"dsmapp-rootpanel-actions\");\n var approve = el(\"button\", \"dsmapp-rootpanel-primary\"), check = el(\"button\");\n title.textContent = t(\"title\"); label.textContent = t(\"password\");\n password.type = \"password\"; password.autocomplete = \"current-password\";\n approve.type = \"submit\"; approve.textContent = t(\"approve\");\n check.type = \"button\"; check.textContent = t(\"check\");\n label.appendChild(password); actions.appendChild(approve); actions.appendChild(check);\n form.appendChild(label); form.appendChild(actions);\n panel.appendChild(title); panel.appendChild(status); panel.appendChild(reason); panel.appendChild(form);\n host.appendChild(panel);\n function
|
|
12
|
+
export declare const ROOT_PANEL = "function mountRootPanel(host, access, t, admin) {\n if (!admin) return;\n function el(tag, className) { var e = document.createElement(tag); if (className) e.className = className; return e; }\n var panel = el(\"div\", \"dsmapp-rootpanel\"), title = el(\"h2\"), status = el(\"p\"), reason = el(\"p\", \"dsmapp-rootpanel-hint\");\n var form = el(\"form\"), label = el(\"label\"), password = el(\"input\"), actions = el(\"div\", \"dsmapp-rootpanel-actions\");\n var approve = el(\"button\", \"dsmapp-rootpanel-primary\"), check = el(\"button\");\n title.textContent = t(\"title\"); label.textContent = t(\"password\");\n password.type = \"password\"; password.autocomplete = \"current-password\";\n approve.type = \"submit\"; approve.textContent = t(\"approve\");\n check.type = \"button\"; check.textContent = t(\"check\");\n label.appendChild(password); actions.appendChild(approve); actions.appendChild(check);\n form.appendChild(label); form.appendChild(actions);\n panel.appendChild(title); panel.appendChild(status); panel.appendChild(reason); panel.appendChild(form);\n host.appendChild(panel);\n // The code and outcome in the DSM language, then the DSM code the English reason starts with.\n function explain(result) {\n if (!result.reason && !result.code) return \"\";\n var parts = [t(\"reason.\" + (result.code || \"failed\"))];\n if (result.outcome) parts.push(t(\"outcome.\" + result.outcome));\n var detail = /^([A-Za-z0-9_]+):/.exec(result.reason || \"\");\n if (detail) parts.push(\"(\" + detail[1] + \")\");\n return parts.reduce(function (text, part) { return text + (/[\u3000-\u30FF\u4E00-\u9FFF\uAC00-\uD7AF\uFF00-\uFFEF]$/.test(text) ? \"\" : \" \") + part; });\n }\n function show(result) {\n status.textContent = t(\"state.\" + result.state);\n reason.textContent = explain(result); reason.hidden = !reason.textContent;\n label.hidden = approve.hidden = result.state !== \"needs-admin-approval\" && result.retry !== true;\n }\n // A start step can hold the background service for up to a minute.\n function run(fn) { approve.disabled = check.disabled = true; reason.textContent = t(\"working\"); reason.hidden = false; return fn().then(show, function(e) { show({state: \"blocked\", reason: e && e.code ? e.code + \":\" : \"\", code: \"failed\"}); }).then(function() { approve.disabled = check.disabled = false; }); }\n access.subscribe(show);\n form.onsubmit = function(event) {\n if (event) event.preventDefault();\n var entered = password.value; password.value = \"\";\n if (!entered) return Promise.resolve();\n return run(function() { return access.authorize(entered); });\n };\n check.onclick = function() { return run(function() { return access.status(); }); };\n 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; }); });\n}";
|
|
13
13
|
/** Administrator password confirmation in the window's own session, for the
|
|
14
14
|
* password an approval panel collected. */
|
|
15
15
|
export declare const PASSWORD_CONFIRM: string;
|
|
@@ -12,8 +12,29 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
12
12
|
var code = String(e && e.code || "executor_bad_response");
|
|
13
13
|
if (e && e.passwordConfirm) return code + (code === "dsm_3121" ? ": DSM rejected the administrator password." : ": administrator password confirmation failed.") + " Nothing was created.";
|
|
14
14
|
if (e && e.detail) return code + ": " + e.detail;
|
|
15
|
+
if (["dsm_106","dsm_107","dsm_119"].indexOf(code) >= 0) return code + ": the DSM session ended. Sign in to DSM again, then approve again.";
|
|
15
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.");
|
|
16
17
|
}
|
|
18
|
+
// What a result's code says, for the window panel and the SDK to translate;
|
|
19
|
+
// reason keeps the English detail. fallback names a step's own failure.
|
|
20
|
+
function codeOf(e, fallback) {
|
|
21
|
+
var code = String(e && e.code || "");
|
|
22
|
+
if (e && e.passwordConfirm) return code === "dsm_3121" ? "password_rejected" : code === "password_required" ? "password_required" : ["dsm_106","dsm_107","dsm_119"].indexOf(code) >= 0 ? "session_ended" : "password_confirm_failed";
|
|
23
|
+
if (e && e.task) return e.task;
|
|
24
|
+
if (["dsm_106","dsm_107","dsm_119"].indexOf(code) >= 0) return "session_ended";
|
|
25
|
+
if (code === "admin_required") return code;
|
|
26
|
+
if (code === "executor_confirmation_missing") return "password_confirm_failed";
|
|
27
|
+
if (code === "executor_starting") return code;
|
|
28
|
+
if (e && e.detail) return /other digests/.test(e.detail) ? "executor_mismatch" : "executor_unreachable";
|
|
29
|
+
if (code.indexOf("executor_selection") === 0) return "selection_failed";
|
|
30
|
+
if (fallback) return fallback;
|
|
31
|
+
if (code === "105" || code === "dsm_105") return "scheduler_denied";
|
|
32
|
+
if (["102","103","104","dsm_102","dsm_103","dsm_104"].indexOf(code) >= 0) return "scheduler_unsupported";
|
|
33
|
+
return "scheduler_failed";
|
|
34
|
+
}
|
|
35
|
+
function block(result, e, fallback) { result.state = "blocked"; result.reason = reason(e); result.code = codeOf(e, fallback); return result; }
|
|
36
|
+
// A find result that stops an approval, with its code.
|
|
37
|
+
function refused(current) { var e = fail(current.reason); e.task = current.code; return e; }
|
|
17
38
|
// AppWindow.sendWebAPI encodes JSON parameters. Preserve their native types;
|
|
18
39
|
// pre-encoding turns objects/booleans into strings and DSM rejects create.
|
|
19
40
|
function send(api, method, version, params) { return options.send(api, method, version, params || {}); }
|
|
@@ -38,36 +59,60 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
38
59
|
return send(boot ? eventAPI : taskAPI, "delete", boot ? 1 : 2, boot ? ref : {tasks: [ref]});
|
|
39
60
|
}
|
|
40
61
|
function admin() { if (!options.user() || options.user().admin !== true) throw fail("admin_required"); }
|
|
41
|
-
function
|
|
62
|
+
function missing() { return {state: "needs-admin-approval", reason: "Task is missing.", code: "task_missing"}; }
|
|
63
|
+
// EventScheduler keys a task by its name (DSM refuses a second one with 117
|
|
64
|
+
// DUPLICATE_PRIMARY_KEY) and get answers no data for a missing name, so get
|
|
65
|
+
// alone finds it. Task Scheduler names repeat: a list finds the id and
|
|
66
|
+
// refuses duplicates, and later cached reads get that id until it fails.
|
|
67
|
+
var listed = {};
|
|
68
|
+
function locate(task, cached) {
|
|
69
|
+
if (task.kind === "boot") return Promise.resolve({task_name: task.name});
|
|
70
|
+
if (cached && listed[task.name] !== undefined) return Promise.resolve({id: listed[task.name], real_owner: "root", cached: true});
|
|
71
|
+
return taskRead("list", 3, {start: 0, limit: 500}, 2, {offset: 0, limit: 500}).then(function (data) {
|
|
72
|
+
var records = data && data.tasks;
|
|
73
|
+
if (!Array.isArray(records) || records.length > 500 || !Number.isInteger(data.total) || data.total !== records.length) throw fail("executor_bad_response");
|
|
74
|
+
var named = records.filter(function (r) { return r && r.name === task.name; });
|
|
75
|
+
delete listed[task.name];
|
|
76
|
+
if (!named.length) return null;
|
|
77
|
+
if (named.length !== 1) return {duplicate: true};
|
|
78
|
+
if (!/^[0-9]+$/.test(String(named[0].id))) throw fail("executor_bad_response");
|
|
79
|
+
listed[task.name] = named[0].id;
|
|
80
|
+
return {id: named[0].id, real_owner: "root"};
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// cached: get the control task by the id an earlier list in this window showed.
|
|
84
|
+
function find(task, cached) {
|
|
42
85
|
var boot = task.kind === "boot";
|
|
43
|
-
return (
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
var
|
|
47
|
-
if (!named.length) return {state: "needs-admin-approval", reason: "Task is missing."};
|
|
48
|
-
if (named.length !== 1) return {state: "blocked", reason: "Duplicate task names."};
|
|
49
|
-
var summary = named[0];
|
|
50
|
-
if (!boot && !/^[0-9]+$/.test(String(summary.id))) throw fail("executor_bad_response");
|
|
51
|
-
var ref = boot ? {task_name: task.name} : {id: summary.id, real_owner: "root"};
|
|
86
|
+
return locate(task, cached).then(function (located) {
|
|
87
|
+
if (!located) return missing();
|
|
88
|
+
if (located.duplicate) return {state: "blocked", reason: "Duplicate task names.", code: "task_duplicate"};
|
|
89
|
+
var ref = boot ? located : {id: located.id, real_owner: "root"};
|
|
52
90
|
var detail = boot ? send(eventAPI, "get", 1, ref) : taskRead("get", 4, ref, 3, ref);
|
|
91
|
+
// A cached id that is gone or now names another task is listed again.
|
|
92
|
+
if (located.cached) detail = detail.then(function (r) { if (!r || r.name !== task.name || String(r.id) !== String(ref.id)) throw {relist: true}; return r; });
|
|
53
93
|
return detail.then(function (r) {
|
|
94
|
+
if (boot && (r === undefined || r === null || (typeof r === "object" && !Object.keys(r).length))) return missing();
|
|
54
95
|
var owner = r && r.owner;
|
|
55
96
|
var root = owner === "root" || (boot && owner && owner["0"] === "root");
|
|
56
97
|
var script = boot ? r && r.operation : r && r.extra && r.extra.script;
|
|
57
|
-
if (!r || (boot ? r.task_name !== task.name : String(r.id) !== String(ref.id) || r.name !== task.name) || !root || (!boot && (r.real_owner !== "root" || r.enable !== false || r.type !== "script")) || (boot && (r.enable !== true || r.event !== "bootup" || r.operation_type !== "script")) || typeof script !== "string") return {state: "blocked", reason: "Task ownership or definition cannot be verified."};
|
|
98
|
+
if (!r || (boot ? r.task_name !== task.name : String(r.id) !== String(ref.id) || r.name !== task.name) || !root || (!boot && (r.real_owner !== "root" || r.enable !== false || r.type !== "script")) || (boot && (r.enable !== true || r.event !== "bootup" || r.operation_type !== "script")) || typeof script !== "string") return {state: "blocked", reason: "Task ownership or definition cannot be verified.", code: "task_unverified"};
|
|
58
99
|
if (script === task.script) return {state: "ready", ref: ref};
|
|
59
|
-
if (script.indexOf(task.header) === 0) return {state: "needs-admin-approval", ref: ref, reason: "Approved digests differ."};
|
|
60
|
-
return {state: "blocked", reason: "Same-named task has an unrelated script."};
|
|
100
|
+
if (script.indexOf(task.header) === 0) return {state: "needs-admin-approval", ref: ref, script: script, reason: "Approved digests differ.", code: "task_outdated"};
|
|
101
|
+
return {state: "blocked", reason: "Same-named task has an unrelated script.", code: "task_conflict"};
|
|
102
|
+
}, function (e) {
|
|
103
|
+
if (!located.cached || !(e && (e.relist || e.code === "dsm_4801"))) throw e;
|
|
104
|
+
delete listed[task.name];
|
|
105
|
+
return find(task, false);
|
|
61
106
|
});
|
|
62
|
-
}).catch(function (e) { return {state: ["102","103","104","dsm_102","dsm_103","dsm_104"].indexOf(String(e && e.code)) >= 0 ? "unsupported" : "blocked", reason: reason(e)}; });
|
|
107
|
+
}).catch(function (e) { return {state: ["102","103","104","dsm_102","dsm_103","dsm_104"].indexOf(String(e && e.code)) >= 0 ? "unsupported" : "blocked", reason: reason(e), code: codeOf(e)}; });
|
|
63
108
|
}
|
|
64
|
-
function read() {
|
|
65
|
-
try { admin(); } catch (e) { return Promise.resolve({
|
|
66
|
-
return Promise.all(options.tasks.map(find)).then(function (found) {
|
|
109
|
+
function read(cached) {
|
|
110
|
+
try { admin(); } catch (e) { return Promise.resolve(block({tasks: {}}, e)); }
|
|
111
|
+
return Promise.all(options.tasks.map(function (task) { return find(task, cached === true); })).then(function (found) {
|
|
67
112
|
var result = {state: "ready", tasks: {}};
|
|
68
113
|
options.tasks.forEach(function (t, i) {
|
|
69
114
|
result.tasks[t.kind] = found[i];
|
|
70
|
-
if (found[i].state === "blocked" || (found[i].state !== "ready" && result.state === "ready")) { result.state = found[i].state; result.reason = found[i].reason; }
|
|
115
|
+
if (found[i].state === "blocked" || (found[i].state !== "ready" && result.state === "ready")) { result.state = found[i].state; result.reason = found[i].reason; result.code = found[i].code; }
|
|
71
116
|
});
|
|
72
117
|
return result;
|
|
73
118
|
});
|
|
@@ -82,6 +127,18 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
82
127
|
}).catch(function (e) { failed = reason(e); });
|
|
83
128
|
}, Promise.resolve()).then(function () { return failed; });
|
|
84
129
|
}
|
|
130
|
+
// Undoes one approval: withdraws what it created, then recreates, newest
|
|
131
|
+
// first, the earlier approval's tasks it removed. Resolves like withdraw.
|
|
132
|
+
function rollback(created, removed, confirmation) {
|
|
133
|
+
return withdraw(created).then(function (failed) {
|
|
134
|
+
return removed.reduceRight(function (restored, earlier) {
|
|
135
|
+
return restored.then(function () { return find(earlier); }).then(function (current) {
|
|
136
|
+
if (current.state === "blocked") throw refused(current);
|
|
137
|
+
if (!current.ref) return create(earlier.kind === "boot", definition(earlier), confirmation);
|
|
138
|
+
}).catch(function (e) { failed = reason(e); });
|
|
139
|
+
}, Promise.resolve()).then(function () { return failed; });
|
|
140
|
+
});
|
|
141
|
+
}
|
|
85
142
|
function definition(task) { return task.kind === "boot" ? (${bootDefinition.toString()})(task) : (${controlDefinition.toString()})(task); }
|
|
86
143
|
function approve(password, selectedScope) {
|
|
87
144
|
var selectionTask;
|
|
@@ -95,29 +152,41 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
95
152
|
if (!confirmation || confirmation.confirmed !== true) return {state: "cancelled", tasks: before.tasks};
|
|
96
153
|
// DSM 7.1 confirms the password server-side but issues no create token.
|
|
97
154
|
if (!legacy && (typeof confirmation.token !== "string" || !confirmation.token)) throw fail("executor_confirmation_missing");
|
|
98
|
-
var errors = {}, created = [], withdrawFailed;
|
|
155
|
+
var errors = {}, created = [], removed = [], withdrawFailed;
|
|
99
156
|
// Delete only a verified, same-header outdated task, then create the pinned
|
|
100
|
-
// replacement. set has no measured wire contract
|
|
157
|
+
// replacement. set has no measured wire contract, and a same-named task
|
|
158
|
+
// cannot be created first. A task to delete is read again by get just
|
|
159
|
+
// before; a missing one keeps the first read: DSM refuses a second boot
|
|
160
|
+
// task name, and the readback after the writes lists control tasks for
|
|
161
|
+
// duplicates. Stop at the first error.
|
|
101
162
|
var chain = Promise.resolve();
|
|
102
163
|
options.tasks.forEach(function (task) {
|
|
103
164
|
chain = chain.then(function () {
|
|
104
|
-
|
|
165
|
+
if (Object.keys(errors).length) return;
|
|
166
|
+
var first = before.tasks[task.kind];
|
|
167
|
+
return (first.ref ? find(task, true) : Promise.resolve(first)).then(function (current) {
|
|
105
168
|
if (current.state === "ready") return;
|
|
106
|
-
if (current.state === "blocked") throw
|
|
107
|
-
var
|
|
108
|
-
return
|
|
109
|
-
}).catch(function (e) { errors[task.kind] =
|
|
169
|
+
if (current.state === "blocked") throw refused(current);
|
|
170
|
+
var deleted = current.ref ? remove(task, current.ref).then(function () { removed.push({kind: task.kind, name: task.name, header: task.header, script: current.script}); }) : Promise.resolve();
|
|
171
|
+
return deleted.then(function () { return create(task.kind === "boot", definition(task), confirmation); }).then(function () { created.push(task); });
|
|
172
|
+
}).catch(function (e) { errors[task.kind] = e; });
|
|
110
173
|
});
|
|
111
174
|
});
|
|
112
175
|
// A partial approval must not leave an enabled root bootup task that no
|
|
113
|
-
// control task or uninstall inventory owns
|
|
176
|
+
// control task or uninstall inventory owns, nor lose the earlier approval:
|
|
177
|
+
// roll back to it. A retry converges from whatever the rollback left.
|
|
114
178
|
return chain.then(function () {
|
|
115
|
-
if (Object.keys(errors).length) return
|
|
116
|
-
}).then(read).then(function (after) {
|
|
179
|
+
if (Object.keys(errors).length) return rollback(created, removed, confirmation).then(function (failed) { withdrawFailed = failed; });
|
|
180
|
+
}).then(function () { return read(true); }).then(function (after) {
|
|
117
181
|
Object.keys(errors).forEach(function (kind) {
|
|
118
|
-
if (after.tasks[kind].state !== "ready") { after.tasks[kind]
|
|
182
|
+
if (after.tasks[kind].state !== "ready") { block(after.tasks[kind], errors[kind]); block(after, errors[kind]); }
|
|
119
183
|
});
|
|
120
|
-
if (withdrawFailed) { after.state = "blocked"; after.reason = (after.reason ? after.reason + " " : "") + "Withdrawing the tasks this approval created failed: " + withdrawFailed; }
|
|
184
|
+
if (withdrawFailed) { after.state = "blocked"; after.reason = (after.reason ? after.reason + " " : "") + (removed.length ? "Restoring the earlier approval failed: " : "Withdrawing the tasks this approval created failed: ") + withdrawFailed; after.outcome = removed.length ? "restore_failed" : "withdraw_failed"; }
|
|
185
|
+
else if (Object.keys(errors).length && after.state !== "ready") {
|
|
186
|
+
var restored = Object.keys(before.tasks).some(function (kind) { return before.tasks[kind].ref; });
|
|
187
|
+
after.reason += restored ? " The earlier approval was restored." : " The tasks this approval created were withdrawn.";
|
|
188
|
+
after.outcome = restored ? "restored" : "withdrawn"; after.retry = true;
|
|
189
|
+
}
|
|
121
190
|
// A scope change must not restart a running executor: the boot task
|
|
122
191
|
// stops it, which interrupts in-flight copies instead of canceling them.
|
|
123
192
|
return after.state === "ready" ? (selectionTask ? running(after) : Promise.resolve(null)).then(function (live) { return live || start(after); }).then(function (started) {
|
|
@@ -128,6 +197,7 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
128
197
|
return read().then(function (current) {
|
|
129
198
|
current.state = "blocked";
|
|
130
199
|
current.reason = started.reason + (failed ? " Withdrawing the tasks this approval created failed: " + failed : " The tasks this approval created were withdrawn.");
|
|
200
|
+
current.code = started.code; current.outcome = failed ? "withdraw_failed" : "withdrawn";
|
|
131
201
|
current.retry = !failed;
|
|
132
202
|
return current;
|
|
133
203
|
});
|
|
@@ -141,7 +211,7 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
141
211
|
// app converge by readback: applied once status().executor.selection
|
|
142
212
|
// shows this scope above this generation. A resubmit is pinned and safe.
|
|
143
213
|
return selectScope(selectionTask, confirmation, started, selectedScope).catch(function (e) {
|
|
144
|
-
return {
|
|
214
|
+
return block({tasks: started.tasks, requested: {scope: selectedScope, generation: generation}}, e, "selection_failed");
|
|
145
215
|
});
|
|
146
216
|
}) : after;
|
|
147
217
|
});
|
|
@@ -203,32 +273,52 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
203
273
|
// A started executor answered only after its own DSM lookups, which took
|
|
204
274
|
// 16-20 s on a measured NAS; giving up withdraws a fresh approval.
|
|
205
275
|
var startBudget = 30000;
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
276
|
+
// The pinned executor listens before its package start step, which runs as
|
|
277
|
+
// root for at most 60 s and answers state "starting" until it ends.
|
|
278
|
+
var stepBudget = 75000;
|
|
279
|
+
function starting(status) { return !!status && status.state === "starting" && status.executor_digest === (options.pins && options.pins.executorDigest) && status.policy_digest === options.pins.policyDigest; }
|
|
280
|
+
// The approved executor's status. budget: how long to keep asking after a
|
|
281
|
+
// start (ms); a start step's own wait is step. An executor without a start
|
|
282
|
+
// step answered 1.8-3 s after run on a measured NAS: ask every 500 ms for
|
|
283
|
+
// the first 4 s, then double to 2 s so an open window does not poll the
|
|
284
|
+
// executor twice a second.
|
|
285
|
+
function alive(budget, delay, step, waited) {
|
|
286
|
+
waited = waited || 0;
|
|
287
|
+
delay = waited < 4000 ? 500 : Math.min(delay * 2, 2000);
|
|
288
|
+
step = step === undefined ? stepBudget : step;
|
|
211
289
|
return options.call("executor.status", {}).then(function (status) {
|
|
290
|
+
if (starting(status)) throw fail("executor_starting", "the executor's start step has not ended.");
|
|
212
291
|
var why = mismatch(status);
|
|
213
292
|
if (why) throw fail("executor_status_mismatch", why);
|
|
214
|
-
|
|
293
|
+
return status;
|
|
215
294
|
}, function (e) {
|
|
216
295
|
throw fail(e && e.code, "the executor status request failed.");
|
|
217
296
|
}).catch(function (e) {
|
|
218
|
-
|
|
297
|
+
var steps = e.code === "executor_starting";
|
|
298
|
+
if ((steps ? step : budget) >= delay && options.wait) return options.wait(delay).then(function () { return steps ? alive(budget, delay, step - delay, waited + delay) : alive(budget - delay, delay, step, waited + delay); });
|
|
299
|
+
throw e;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function health(verified, budget) {
|
|
303
|
+
if (verified.state !== "ready") return Promise.resolve(verified);
|
|
304
|
+
return settle(verified, alive(budget));
|
|
305
|
+
}
|
|
306
|
+
function settle(verified, status) {
|
|
307
|
+
return status.then(function (current) { verified.executor = current; return verified; }, function (e) {
|
|
219
308
|
// The tasks are approved: approving again runs start, which restarts the
|
|
220
309
|
// executor without a password, so the panel offers it.
|
|
221
|
-
verified
|
|
310
|
+
block(verified, e); verified.retry = true; return verified;
|
|
222
311
|
});
|
|
223
312
|
}
|
|
224
313
|
function start(verified) {
|
|
225
314
|
return options.call("executor.control", {action: "start"}).then(function () {
|
|
226
315
|
return send(eventAPI, "run", 1, verified.tasks.boot.ref);
|
|
227
|
-
}).then(function () {
|
|
228
|
-
// run can succeed for a nonexistent task. Read both definitions again.
|
|
229
|
-
return read().then(function (after) { return health(after, startBudget); });
|
|
230
|
-
}).catch(function (e) { verified.state = "blocked"; verified.reason = reason(e); return verified; });
|
|
316
|
+
}).then(function () { return started(verified); }).catch(function (e) { return block(verified, e, "executor_start_failed"); });
|
|
231
317
|
}
|
|
318
|
+
// Every run follows an exact readback of both tasks in the same step, and a
|
|
319
|
+
// run of a missing task leaves no pinned executor to answer: only the
|
|
320
|
+
// executor status is asked after it.
|
|
321
|
+
function started(verified) { return settle(verified, alive(startBudget)); }
|
|
232
322
|
// The window panel and the app's own settings share this access; each
|
|
233
323
|
// result reaches every view so neither shows a stale state.
|
|
234
324
|
var listeners = [];
|
|
@@ -236,32 +326,56 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
236
326
|
return {
|
|
237
327
|
status: function () { return read().then(function (verified) { return health(verified, 0); }).then(publish); },
|
|
238
328
|
authorize: function (password, selectedScope) {
|
|
239
|
-
if (!busy) busy = Promise.resolve().then(function () { return approve(password, selectedScope); }).catch(function (e) {
|
|
329
|
+
if (!busy) busy = Promise.resolve().then(function () { return approve(password, selectedScope); }).catch(function (e) { var r = block({tasks: {}}, e); r.retry = !!(e && e.passwordConfirm); return r; }).then(function (r) { busy = null; return publish(r); });
|
|
240
330
|
return busy;
|
|
241
331
|
},
|
|
242
332
|
control: function (action) {
|
|
243
333
|
if (["start", "stop", "purge"].indexOf(action) < 0) return Promise.reject(fail("executor_control_invalid"));
|
|
244
|
-
return read().then(function (verified) {
|
|
334
|
+
return read(true).then(function (verified) {
|
|
245
335
|
if (verified.state !== "ready") throw fail("executor_not_ready");
|
|
246
336
|
return options.call("executor.control", {action: action}).then(function () {
|
|
247
337
|
return send(taskAPI, "run", 2, {tasks: [verified.tasks.control.ref]});
|
|
248
|
-
}).then(
|
|
338
|
+
}).then(function () { return action === "start" ? started(verified) : read(true); });
|
|
249
339
|
}).then(publish);
|
|
250
340
|
},
|
|
251
341
|
subscribe: function (listener) { listeners.push(listener); }
|
|
252
342
|
};
|
|
253
343
|
}`;
|
|
254
|
-
/** The plain-UI window panel's text
|
|
344
|
+
/** The plain-UI window panel's text: a state, and a result's code and outcome; reason stays the runtime's English detail. */
|
|
255
345
|
export const ROOT_PANEL_MESSAGES = {
|
|
256
346
|
title: { en: "Background access", "zh-TW": "背景存取", "zh-CN": "后台访问", ja: "バックグラウンドアクセス", ko: "백그라운드 액세스", de: "Hintergrundzugriff", fr: "Accès en arrière-plan", es: "Acceso en segundo plano", it: "Accesso in background" },
|
|
257
347
|
password: { en: "DSM administrator password", "zh-TW": "DSM 管理員密碼", "zh-CN": "DSM 管理员密码", ja: "DSM 管理者パスワード", ko: "DSM 관리자 비밀번호", de: "DSM-Administratorkennwort", fr: "Mot de passe administrateur DSM", es: "Contraseña de administrador de DSM", it: "Password amministratore DSM" },
|
|
258
348
|
approve: { en: "Approve", "zh-TW": "核准", "zh-CN": "批准", ja: "承認", ko: "승인", de: "Genehmigen", fr: "Approuver", es: "Aprobar", it: "Approva" },
|
|
259
349
|
check: { en: "Check status", "zh-TW": "檢查狀態", "zh-CN": "检查状态", ja: "状態を確認", ko: "상태 확인", de: "Status prüfen", fr: "Vérifier l'état", es: "Comprobar estado", it: "Verifica stato" },
|
|
350
|
+
working: { en: "Waiting for the background service. This can take up to a minute while it starts.", "zh-TW": "正在等候背景服務,啟動時最多可能需要一分鐘。", "zh-CN": "正在等候后台服务,启动时最多可能需要一分钟。", ja: "バックグラウンドサービスを待機しています。開始時は最大 1 分かかることがあります。", ko: "백그라운드 서비스를 기다리는 중입니다. 시작할 때 최대 1분이 걸릴 수 있습니다.", de: "Warten auf den Hintergrunddienst. Beim Start kann dies bis zu einer Minute dauern.", fr: "En attente du service en arrière-plan. Son démarrage peut prendre jusqu'à une minute.", es: "Esperando al servicio en segundo plano. Al iniciarse puede tardar hasta un minuto.", it: "In attesa del servizio in background. All'avvio può richiedere fino a un minuto." },
|
|
260
351
|
"state.ready": { en: "Approved and running.", "zh-TW": "已核准,正在執行。", "zh-CN": "已批准,正在运行。", ja: "承認済みで、実行中です。", ko: "승인되었으며 실행 중입니다.", de: "Genehmigt und aktiv.", fr: "Approuvé et actif.", es: "Aprobado y en ejecución.", it: "Approvato e in esecuzione." },
|
|
261
352
|
"state.needs-admin-approval": { en: "Needs administrator approval.", "zh-TW": "需要管理員核准。", "zh-CN": "需要管理员批准。", ja: "管理者の承認が必要です。", ko: "관리자 승인이 필요합니다.", de: "Genehmigung durch einen Administrator erforderlich.", fr: "Approbation d'un administrateur requise.", es: "Requiere la aprobación de un administrador.", it: "Richiede l'approvazione di un amministratore." },
|
|
262
353
|
"state.blocked": { en: "Not available.", "zh-TW": "目前無法使用。", "zh-CN": "目前无法使用。", ja: "現在は利用できません。", ko: "현재 사용할 수 없습니다.", de: "Derzeit nicht verfügbar.", fr: "Indisponible pour le moment.", es: "No disponible por ahora.", it: "Non disponibile al momento." },
|
|
263
354
|
"state.unsupported": { en: "This DSM does not support background access.", "zh-TW": "這台 DSM 不支援背景存取。", "zh-CN": "此 DSM 不支持后台访问。", ja: "この DSM はバックグラウンドアクセスに対応していません。", ko: "이 DSM은 백그라운드 액세스를 지원하지 않습니다.", de: "Dieses DSM unterstützt keinen Hintergrundzugriff.", fr: "Ce DSM ne prend pas en charge l'accès en arrière-plan.", es: "Este DSM no admite el acceso en segundo plano.", it: "Questo DSM non supporta l'accesso in background." },
|
|
264
355
|
"state.cancelled": { en: "Approval was cancelled.", "zh-TW": "已取消核准。", "zh-CN": "已取消批准。", ja: "承認はキャンセルされました。", ko: "승인이 취소되었습니다.", de: "Genehmigung abgebrochen.", fr: "Approbation annulée.", es: "Aprobación cancelada.", it: "Approvazione annullata." },
|
|
356
|
+
"reason.password_rejected": { en: "DSM rejected the administrator password. Nothing was changed.", "zh-TW": "DSM 拒絕了管理員密碼,沒有做任何變更。", "zh-CN": "DSM 拒绝了管理员密码,没有做任何更改。", ja: "DSM が管理者パスワードを拒否しました。何も変更されていません。", ko: "DSM에서 관리자 비밀번호를 거부했습니다. 변경된 내용은 없습니다.", de: "DSM hat das Administratorkennwort abgelehnt. Es wurde nichts geändert.", fr: "DSM a refusé le mot de passe administrateur. Rien n'a été modifié.", es: "DSM rechazó la contraseña de administrador. No se ha cambiado nada.", it: "DSM ha rifiutato la password amministratore. Non è stato modificato nulla." },
|
|
357
|
+
"reason.password_required": { en: "Enter the DSM administrator password.", "zh-TW": "請輸入 DSM 管理員密碼。", "zh-CN": "请输入 DSM 管理员密码。", ja: "DSM 管理者パスワードを入力してください。", ko: "DSM 관리자 비밀번호를 입력하십시오.", de: "Geben Sie das DSM-Administratorkennwort ein.", fr: "Saisissez le mot de passe administrateur DSM.", es: "Introduzca la contraseña de administrador de DSM.", it: "Immettere la password amministratore DSM." },
|
|
358
|
+
"reason.password_confirm_failed": { en: "DSM could not confirm the administrator password. Nothing was changed.", "zh-TW": "DSM 無法確認管理員密碼,沒有做任何變更。", "zh-CN": "DSM 无法确认管理员密码,没有做任何更改。", ja: "DSM で管理者パスワードを確認できませんでした。何も変更されていません。", ko: "DSM에서 관리자 비밀번호를 확인할 수 없습니다. 변경된 내용은 없습니다.", de: "DSM konnte das Administratorkennwort nicht bestätigen. Es wurde nichts geändert.", fr: "DSM n'a pas pu confirmer le mot de passe administrateur. Rien n'a été modifié.", es: "DSM no pudo confirmar la contraseña de administrador. No se ha cambiado nada.", it: "DSM non ha potuto confermare la password amministratore. Non è stato modificato nulla." },
|
|
359
|
+
"reason.session_ended": { en: "The DSM session ended. Sign in to DSM again, then approve again.", "zh-TW": "DSM 工作階段已結束。請重新登入 DSM 後再核准一次。", "zh-CN": "DSM 会话已结束。请重新登录 DSM 后再批准一次。", ja: "DSM セッションが終了しました。DSM に再度サインインしてから、もう一度承認してください。", ko: "DSM 세션이 종료되었습니다. DSM에 다시 로그인한 후 다시 승인하십시오.", de: "Die DSM-Sitzung ist abgelaufen. Melden Sie sich erneut bei DSM an und genehmigen Sie dann erneut.", fr: "La session DSM a expiré. Reconnectez-vous à DSM, puis approuvez de nouveau.", es: "La sesión de DSM ha finalizado. Vuelva a iniciar sesión en DSM y apruebe de nuevo.", it: "La sessione DSM è terminata. Accedere di nuovo a DSM, quindi approvare di nuovo." },
|
|
360
|
+
"reason.admin_required": { en: "Only a DSM administrator can approve.", "zh-TW": "只有 DSM 管理員可以核准。", "zh-CN": "只有 DSM 管理员可以批准。", ja: "承認できるのは DSM 管理者のみです。", ko: "DSM 관리자만 승인할 수 있습니다.", de: "Nur ein DSM-Administrator kann genehmigen.", fr: "Seul un administrateur DSM peut approuver.", es: "Solo un administrador de DSM puede aprobar.", it: "Solo un amministratore DSM può approvare." },
|
|
361
|
+
"reason.scheduler_denied": { en: "DSM denied the administrator access to Task Scheduler. Check the DSM session; restarting the NAS resolved this before.", "zh-TW": "DSM 拒絕了管理員對任務排程表的存取。請檢查 DSM 工作階段;先前重新啟動 NAS 可解決此問題。", "zh-CN": "DSM 拒绝了管理员对任务计划的访问。请检查 DSM 会话;之前重新启动 NAS 可解决此问题。", ja: "DSM が管理者のタスクスケジューラへのアクセスを拒否しました。DSM セッションを確認してください。以前は NAS の再起動で解決しました。", ko: "DSM에서 관리자의 작업 스케줄러 액세스를 거부했습니다. DSM 세션을 확인하십시오. 이전에는 NAS를 다시 시작하여 해결되었습니다.", de: "DSM hat dem Administrator den Zugriff auf den Aufgabenplaner verweigert. Prüfen Sie die DSM-Sitzung; ein Neustart des NAS hat dies bisher behoben.", fr: "DSM a refusé à l'administrateur l'accès au Planificateur de tâches. Vérifiez la session DSM ; un redémarrage du NAS a déjà résolu ce problème.", es: "DSM denegó al administrador el acceso al Programador de tareas. Compruebe la sesión de DSM; reiniciar el NAS lo resolvió anteriormente.", it: "DSM ha negato all'amministratore l'accesso a Utilità di pianificazione. Controllare la sessione DSM; in passato il riavvio del NAS ha risolto il problema." },
|
|
362
|
+
"reason.scheduler_failed": { en: "Reading or changing the scheduled tasks failed.", "zh-TW": "讀取或變更排程任務失敗。", "zh-CN": "读取或更改计划任务失败。", ja: "スケジュール済みタスクの読み取りまたは変更に失敗しました。", ko: "예약된 작업을 읽거나 변경하지 못했습니다.", de: "Die geplanten Aufgaben konnten nicht gelesen oder geändert werden.", fr: "La lecture ou la modification des tâches planifiées a échoué.", es: "No se pudieron leer ni cambiar las tareas programadas.", it: "Impossibile leggere o modificare le attività pianificate." },
|
|
363
|
+
"reason.scheduler_unsupported": { en: "This DSM's Task Scheduler does not offer what background access needs.", "zh-TW": "這台 DSM 的任務排程表不提供背景存取所需的功能。", "zh-CN": "此 DSM 的任务计划不提供后台访问所需的功能。", ja: "この DSM のタスクスケジューラには、バックグラウンドアクセスに必要な機能がありません。", ko: "이 DSM의 작업 스케줄러는 백그라운드 액세스에 필요한 기능을 제공하지 않습니다.", de: "Der Aufgabenplaner dieses DSM bietet nicht, was der Hintergrundzugriff benötigt.", fr: "Le Planificateur de tâches de ce DSM n'offre pas ce dont l'accès en arrière-plan a besoin.", es: "El Programador de tareas de este DSM no ofrece lo que necesita el acceso en segundo plano.", it: "L'Utilità di pianificazione di questo DSM non offre quanto necessario all'accesso in background." },
|
|
364
|
+
"reason.task_missing": { en: "The approved tasks are not in Task Scheduler.", "zh-TW": "任務排程表中沒有已核准的任務。", "zh-CN": "任务计划中没有已批准的任务。", ja: "承認済みのタスクがタスクスケジューラにありません。", ko: "승인된 작업이 작업 스케줄러에 없습니다.", de: "Die genehmigten Aufgaben fehlen im Aufgabenplaner.", fr: "Les tâches approuvées ne figurent pas dans le Planificateur de tâches.", es: "Las tareas aprobadas no están en el Programador de tareas.", it: "Le attività approvate non sono presenti in Utilità di pianificazione." },
|
|
365
|
+
"reason.task_duplicate": { en: "Task Scheduler has several tasks with this package's task name.", "zh-TW": "任務排程表中有多個任務使用此套件的任務名稱。", "zh-CN": "任务计划中有多个任务使用此套件的任务名称。", ja: "タスクスケジューラに、このパッケージのタスク名を持つタスクが複数あります。", ko: "작업 스케줄러에 이 패키지의 작업 이름을 가진 작업이 여러 개 있습니다.", de: "Im Aufgabenplaner gibt es mehrere Aufgaben mit dem Aufgabennamen dieses Pakets.", fr: "Le Planificateur de tâches contient plusieurs tâches portant le nom de tâche de ce paquet.", es: "El Programador de tareas tiene varias tareas con el nombre de tarea de este paquete.", it: "Utilità di pianificazione contiene più attività con il nome attività di questo pacchetto." },
|
|
366
|
+
"reason.task_unverified": { en: "The owner or definition of a scheduled task cannot be verified.", "zh-TW": "無法驗證排程任務的擁有者或定義。", "zh-CN": "无法验证计划任务的所有者或定义。", ja: "スケジュール済みタスクの所有者または定義を検証できません。", ko: "예약된 작업의 소유자 또는 정의를 확인할 수 없습니다.", de: "Besitzer oder Definition einer geplanten Aufgabe lassen sich nicht überprüfen.", fr: "Le propriétaire ou la définition d'une tâche planifiée ne peut pas être vérifié.", es: "No se puede verificar el propietario o la definición de una tarea programada.", it: "Impossibile verificare il proprietario o la definizione di un'attività pianificata." },
|
|
367
|
+
"reason.task_outdated": { en: "The package changed since the last approval.", "zh-TW": "套件在上次核准後已變更。", "zh-CN": "套件在上次批准后已更改。", ja: "前回の承認以降にパッケージが変更されました。", ko: "마지막 승인 이후 패키지가 변경되었습니다.", de: "Das Paket hat sich seit der letzten Genehmigung geändert.", fr: "Le paquet a changé depuis la dernière approbation.", es: "El paquete ha cambiado desde la última aprobación.", it: "Il pacchetto è cambiato dall'ultima approvazione." },
|
|
368
|
+
"reason.task_conflict": { en: "A scheduled task with this package's task name runs another script.", "zh-TW": "有個使用此套件任務名稱的排程任務執行的是其他指令碼。", "zh-CN": "有个使用此套件任务名称的计划任务运行的是其他脚本。", ja: "このパッケージのタスク名を持つスケジュール済みタスクが、別のスクリプトを実行します。", ko: "이 패키지의 작업 이름을 가진 예약된 작업이 다른 스크립트를 실행합니다.", de: "Eine geplante Aufgabe mit dem Aufgabennamen dieses Pakets führt ein anderes Skript aus.", fr: "Une tâche planifiée portant le nom de tâche de ce paquet exécute un autre script.", es: "Una tarea programada con el nombre de tarea de este paquete ejecuta otro script.", it: "Un'attività pianificata con il nome attività di questo pacchetto esegue un altro script." },
|
|
369
|
+
"reason.executor_unreachable": { en: "The background service of this package does not answer.", "zh-TW": "此套件的背景服務沒有回應。", "zh-CN": "此套件的后台服务没有响应。", ja: "このパッケージのバックグラウンドサービスが応答しません。", ko: "이 패키지의 백그라운드 서비스가 응답하지 않습니다.", de: "Der Hintergrunddienst dieses Pakets antwortet nicht.", fr: "Le service en arrière-plan de ce paquet ne répond pas.", es: "El servicio en segundo plano de este paquete no responde.", it: "Il servizio in background di questo pacchetto non risponde." },
|
|
370
|
+
"reason.executor_mismatch": { en: "The running background service is not the one this package approved.", "zh-TW": "正在執行的背景服務不是此套件核准的版本。", "zh-CN": "正在运行的后台服务不是此套件批准的版本。", ja: "実行中のバックグラウンドサービスは、このパッケージが承認したものではありません。", ko: "실행 중인 백그라운드 서비스는 이 패키지가 승인한 서비스가 아닙니다.", de: "Der laufende Hintergrunddienst ist nicht der von diesem Paket genehmigte.", fr: "Le service en arrière-plan en cours n'est pas celui approuvé par ce paquet.", es: "El servicio en segundo plano en ejecución no es el aprobado por este paquete.", it: "Il servizio in background in esecuzione non è quello approvato da questo pacchetto." },
|
|
371
|
+
"reason.executor_start_failed": { en: "The background service of this package could not be started.", "zh-TW": "無法啟動此套件的背景服務。", "zh-CN": "无法启动此套件的后台服务。", ja: "このパッケージのバックグラウンドサービスを開始できませんでした。", ko: "이 패키지의 백그라운드 서비스를 시작할 수 없습니다.", de: "Der Hintergrunddienst dieses Pakets konnte nicht gestartet werden.", fr: "Le service en arrière-plan de ce paquet n'a pas pu être démarré.", es: "No se pudo iniciar el servicio en segundo plano de este paquete.", it: "Impossibile avviare il servizio in background di questo pacchetto." },
|
|
372
|
+
"reason.executor_starting": { en: "The background service of this package is still running its start step. Check the status again in a moment.", "zh-TW": "此套件的背景服務仍在執行啟動步驟。請稍後再檢查狀態。", "zh-CN": "此套件的后台服务仍在运行启动步骤。请稍后再检查状态。", ja: "このパッケージのバックグラウンドサービスは、まだ開始ステップを実行中です。しばらくしてから状態を確認してください。", ko: "이 패키지의 백그라운드 서비스가 아직 시작 단계를 실행 중입니다. 잠시 후 상태를 다시 확인하십시오.", de: "Der Hintergrunddienst dieses Pakets führt noch seinen Startschritt aus. Prüfen Sie den Status gleich noch einmal.", fr: "Le service en arrière-plan de ce paquet exécute encore son étape de démarrage. Vérifiez l'état de nouveau dans un instant.", es: "El servicio en segundo plano de este paquete aún está ejecutando su paso de inicio. Vuelva a comprobar el estado en un momento.", it: "Il servizio in background di questo pacchetto sta ancora eseguendo il passaggio di avvio. Verificare di nuovo lo stato tra poco." },
|
|
373
|
+
"reason.selection_failed": { en: "The selected shared folder was not confirmed.", "zh-TW": "未能確認所選的共用資料夾。", "zh-CN": "未能确认所选的共享文件夹。", ja: "選択した共有フォルダを確認できませんでした。", ko: "선택한 공유 폴더를 확인하지 못했습니다.", de: "Der ausgewählte freigegebene Ordner wurde nicht bestätigt.", fr: "Le dossier partagé sélectionné n'a pas été confirmé.", es: "No se confirmó la carpeta compartida seleccionada.", it: "La cartella condivisa selezionata non è stata confermata." },
|
|
374
|
+
"reason.failed": { en: "The operation failed.", "zh-TW": "操作失敗。", "zh-CN": "操作失败。", ja: "操作に失敗しました。", ko: "작업에 실패했습니다.", de: "Der Vorgang ist fehlgeschlagen.", fr: "L'opération a échoué.", es: "La operación ha fallado.", it: "L'operazione non è riuscita." },
|
|
375
|
+
"outcome.restored": { en: "The earlier approval was restored.", "zh-TW": "已還原先前的核准。", "zh-CN": "已还原先前的批准。", ja: "以前の承認を復元しました。", ko: "이전 승인을 복원했습니다.", de: "Die frühere Genehmigung wurde wiederhergestellt.", fr: "L'approbation précédente a été restaurée.", es: "Se restauró la aprobación anterior.", it: "L'approvazione precedente è stata ripristinata." },
|
|
376
|
+
"outcome.withdrawn": { en: "The tasks this approval created were withdrawn.", "zh-TW": "已撤回這次核准建立的任務。", "zh-CN": "已撤回这次批准创建的任务。", ja: "この承認で作成したタスクを取り消しました。", ko: "이번 승인으로 생성한 작업을 철회했습니다.", de: "Die von dieser Genehmigung erstellten Aufgaben wurden zurückgezogen.", fr: "Les tâches créées par cette approbation ont été retirées.", es: "Se retiraron las tareas que creó esta aprobación.", it: "Le attività create da questa approvazione sono state ritirate." },
|
|
377
|
+
"outcome.restore_failed": { en: "Restoring the earlier approval failed.", "zh-TW": "還原先前的核准失敗。", "zh-CN": "还原先前的批准失败。", ja: "以前の承認を復元できませんでした。", ko: "이전 승인을 복원하지 못했습니다.", de: "Die frühere Genehmigung konnte nicht wiederhergestellt werden.", fr: "La restauration de l'approbation précédente a échoué.", es: "No se pudo restaurar la aprobación anterior.", it: "Impossibile ripristinare l'approvazione precedente." },
|
|
378
|
+
"outcome.withdraw_failed": { en: "Withdrawing the tasks this approval created failed.", "zh-TW": "撤回這次核准建立的任務失敗。", "zh-CN": "撤回这次批准创建的任务失败。", ja: "この承認で作成したタスクを取り消せませんでした。", ko: "이번 승인으로 생성한 작업을 철회하지 못했습니다.", de: "Die von dieser Genehmigung erstellten Aufgaben konnten nicht zurückgezogen werden.", fr: "Le retrait des tâches créées par cette approbation a échoué.", es: "No se pudieron retirar las tareas que creó esta aprobación.", it: "Impossibile ritirare le attività create da questa approvazione." },
|
|
265
379
|
};
|
|
266
380
|
/** ROOT_PANEL_MESSAGES as createI18n catalogs, one per locale. */
|
|
267
381
|
export function rootPanelCatalogs() {
|
|
@@ -288,12 +402,22 @@ export const ROOT_PANEL = `function mountRootPanel(host, access, t, admin) {
|
|
|
288
402
|
form.appendChild(label); form.appendChild(actions);
|
|
289
403
|
panel.appendChild(title); panel.appendChild(status); panel.appendChild(reason); panel.appendChild(form);
|
|
290
404
|
host.appendChild(panel);
|
|
405
|
+
// The code and outcome in the DSM language, then the DSM code the English reason starts with.
|
|
406
|
+
function explain(result) {
|
|
407
|
+
if (!result.reason && !result.code) return "";
|
|
408
|
+
var parts = [t("reason." + (result.code || "failed"))];
|
|
409
|
+
if (result.outcome) parts.push(t("outcome." + result.outcome));
|
|
410
|
+
var detail = /^([A-Za-z0-9_]+):/.exec(result.reason || "");
|
|
411
|
+
if (detail) parts.push("(" + detail[1] + ")");
|
|
412
|
+
return parts.reduce(function (text, part) { return text + (/[\u3000-\u30ff\u4e00-\u9fff\uac00-\ud7af\uff00-\uffef]$/.test(text) ? "" : " ") + part; });
|
|
413
|
+
}
|
|
291
414
|
function show(result) {
|
|
292
415
|
status.textContent = t("state." + result.state);
|
|
293
|
-
reason.textContent = result
|
|
416
|
+
reason.textContent = explain(result); reason.hidden = !reason.textContent;
|
|
294
417
|
label.hidden = approve.hidden = result.state !== "needs-admin-approval" && result.retry !== true;
|
|
295
418
|
}
|
|
296
|
-
|
|
419
|
+
// A start step can hold the background service for up to a minute.
|
|
420
|
+
function run(fn) { approve.disabled = check.disabled = true; reason.textContent = t("working"); reason.hidden = false; return fn().then(show, function(e) { show({state: "blocked", reason: e && e.code ? e.code + ":" : "", code: "failed"}); }).then(function() { approve.disabled = check.disabled = false; }); }
|
|
297
421
|
access.subscribe(show);
|
|
298
422
|
form.onsubmit = function(event) {
|
|
299
423
|
if (event) event.preventDefault();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { CLI_VERSION } from "../cli-version.js";
|
|
4
|
-
import { appSdkVersion, SDK_DEPENDENCY, SDK_PACKAGE } from "../runtime/sdk-source.js";
|
|
4
|
+
import { appSdkVersion, SDK_DEPENDENCY, SDK_FLOOR, SDK_PACKAGE, sdkBelowFloor } from "../runtime/sdk-source.js";
|
|
5
5
|
const minorLine = (version) => version.split(".").slice(0, 2).join(".");
|
|
6
6
|
/** The file that names each backend kind's SDK version. */
|
|
7
7
|
const SDK_VERSION_FILE = { go: "backend/go.mod", rust: "Cargo.lock", python: "backend.sdk", node: "backend.sdk" };
|
|
@@ -29,6 +29,9 @@ export function sdkProblems(backend, frontendDir, needsSdk, cliVersion = CLI_VER
|
|
|
29
29
|
if (minorLine(version) !== minorLine(cliVersion)) {
|
|
30
30
|
problems.push({ code: "SDK_UNSUPPORTED", message: `${file} requires SDK ${version}; CLI ${cliVersion} supports only SDK ${minorLine(cliVersion)}.x. Use the matching CLI, or raise the SDK to ${minorLine(cliVersion)}.x.` });
|
|
31
31
|
}
|
|
32
|
+
else if (sdkBelowFloor(version)) {
|
|
33
|
+
problems.push({ code: "SDK_UNSUPPORTED", message: `${file} requires SDK ${version}; CLI ${cliVersion} supports SDK ${SDK_FLOOR} or later. Raise the SDK to ${SDK_FLOOR} or later.` });
|
|
34
|
+
}
|
|
32
35
|
const frontend = frontendDir === undefined ? undefined : frontendSdkVersion(frontendDir);
|
|
33
36
|
if (frontend && frontend.version !== version) {
|
|
34
37
|
problems.push({ code: "SDK_VERSION_MISMATCH", message: `frontend/package.json has ${SDK_PACKAGE} ${frontend.spec} (${frontend.version ?? "version unreadable"}), while ${file} requires ${version}; the two must match.` });
|
package/dist/standalone/spk.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
2
2
|
import type { StandaloneManifest } from "./manifest.js";
|
|
3
3
|
import { type ExecutorArtifact } from "../runtime/executor-build.js";
|
|
4
|
+
import type { PortalCatalogs } from "./portal-window.js";
|
|
4
5
|
export declare const LIFECYCLE_SCRIPTS: readonly ["preinst", "postinst", "preuninst", "postuninst", "preupgrade", "postupgrade", "start-stop-status"];
|
|
5
6
|
export declare class StandaloneBuildError extends Error {
|
|
6
7
|
readonly code: string;
|
|
@@ -18,6 +19,8 @@ export interface AssembleOptions {
|
|
|
18
19
|
backgroundWorker?: string;
|
|
19
20
|
/** A Python, Node or Rust App's finished directory, installed as target/app. */
|
|
20
21
|
app?: string;
|
|
22
|
+
/** The SDK's portal panel text, required exactly when the manifest declares a portal. */
|
|
23
|
+
portalText?: PortalCatalogs;
|
|
21
24
|
}
|
|
22
25
|
/**
|
|
23
26
|
* Packs an unsigned Standalone Package SPK. DSM runs installation/lifecycle
|
package/dist/standalone/spk.js
CHANGED
|
@@ -12,10 +12,12 @@ import { writeWizardUiFiles } from "../build-spk/wizard-i18n.js";
|
|
|
12
12
|
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
|
+
import { APP_SCRIPTS_DIR, APP_SCRIPTS_SPK_DIR } from "./app-scripts.js";
|
|
15
16
|
import { codedMessage } from "../errors.js";
|
|
16
17
|
import { DSM_LANGS, LOCALES } from "./i18n.js";
|
|
17
18
|
import { appTitle, CGI_PAYLOAD_PATH, renderUiConfig, renderWindowScript, windowScriptName } from "./dsm-window.js";
|
|
18
19
|
import { writeUiTexts } from "../build-spk/ui-texts.js";
|
|
20
|
+
import { loginStyleAssets } from "../build-spk/login-style.js";
|
|
19
21
|
import { copyStandaloneNotices } from "../build-spk/notices.js";
|
|
20
22
|
import { copyAppTree } from "./bridge-app.js";
|
|
21
23
|
import { isBridgeBackend } from "./bridge-backend.js";
|
|
@@ -65,8 +67,11 @@ export async function assembleStandaloneSpk(options) {
|
|
|
65
67
|
const appDir = join(uiRoot, "app");
|
|
66
68
|
copyUiTree(manifest.ui.dir, appDir);
|
|
67
69
|
const assetVersion = digestTree(appDir).slice(0, 12);
|
|
68
|
-
|
|
69
|
-
writeFile(join(uiRoot,
|
|
70
|
+
const login = manifest.portal ? loginStyleAssets(manifest.loginStyle) : undefined;
|
|
71
|
+
writeFile(join(uiRoot, "config"), renderUiConfig(manifest, icons, login?.fields));
|
|
72
|
+
for (const [path, bytes] of login?.files ?? [])
|
|
73
|
+
writeFile(join(uiRoot, path), bytes);
|
|
74
|
+
writeFile(join(uiRoot, windowScriptName(manifest)), renderWindowScript(manifest, assetVersion, icons, options.portalText));
|
|
70
75
|
writeFile(join(uiRoot, hostScriptName(manifest.build)), renderHostAdapterScript(manifest.package));
|
|
71
76
|
writeDsmWindowIcons(uiRoot, icons);
|
|
72
77
|
writeUiTexts(uiRoot, appTitle(manifest));
|
|
@@ -141,6 +146,8 @@ export async function assembleStandaloneSpk(options) {
|
|
|
141
146
|
for (const name of LIFECYCLE_SCRIPTS) {
|
|
142
147
|
writeFile(join(staging, "scripts", name), renderStandaloneScript(name, manifest), EXEC_MODE);
|
|
143
148
|
}
|
|
149
|
+
for (const name of manifest.appScripts ?? [])
|
|
150
|
+
copyExecutable(join(manifest.root, APP_SCRIPTS_DIR, name), join(staging, APP_SCRIPTS_SPK_DIR, name));
|
|
144
151
|
const folder = manifest.sharedFolder;
|
|
145
152
|
const steps = manifest.capabilities.delegate?.scope === 'install-selected'
|
|
146
153
|
? (t) => [...renderDelegateScopeSteps(t), ...(folder ? sharedFolderInstallSteps(manifest.package, folder)(t) : [])]
|
|
@@ -171,7 +178,7 @@ function copyExecutable(source, target) {
|
|
|
171
178
|
}
|
|
172
179
|
function writeFile(path, content, mode = FILE_MODE) {
|
|
173
180
|
mkdirSync(dirname(path), { recursive: true });
|
|
174
|
-
writeFileSync(path, content
|
|
181
|
+
writeFileSync(path, content);
|
|
175
182
|
chmodSync(path, mode);
|
|
176
183
|
}
|
|
177
184
|
function quote(value) {
|
package/dist/validate.js
CHANGED
|
@@ -9,6 +9,7 @@ import { classifyComposeRoute, collectComposeInterpolations, collectVolumeSource
|
|
|
9
9
|
import { usesAuthGateway } from "./contract/web-launch.js";
|
|
10
10
|
import { isNativeLayout, loadNativeLayout } from "./standalone/native-layout.js";
|
|
11
11
|
import { loadStandaloneManifest } from "./standalone/manifest.js";
|
|
12
|
+
import { APP_SCRIPTS_DIR, readAppScripts } from "./standalone/app-scripts.js";
|
|
12
13
|
import { manifestPolicyProblem } from "./runtime/executor-policy-check.js";
|
|
13
14
|
import { COMPOSE_FILENAME, CONTRACT_FILENAME, DSM_PACKAGE_FILENAME, } from "./discovery.js";
|
|
14
15
|
import { declaredPrivilegeFlags } from "./compose/privilege.js";
|
|
@@ -43,6 +44,7 @@ export function validateProject(project, options = {}) {
|
|
|
43
44
|
if (capabilities !== undefined) {
|
|
44
45
|
errors.push(makeError("CAPABILITY_UNSUPPORTED", contractSpan(contractFile, 1, 1, "capabilities"), "A compose project does not support capabilities.", "Remove capabilities, or use a dsm-package.yaml native-go project."));
|
|
45
46
|
}
|
|
47
|
+
readAppScripts(project.root, e => errors.push({ ...e, span: { file: APP_SCRIPTS_DIR, line: 1, column: 1 }, suggestion: getErrorCode(e.code).suggestion }));
|
|
46
48
|
if (profile === "listing") {
|
|
47
49
|
for (const { name, type, code } of findListingUnsupportedInstallTypes(parsed.contract)) {
|
|
48
50
|
const pos = getValuePosition(parsed.doc, ["install", name, "type"]) ??
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"prepublishOnly": "npm run build"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@synopackageland/templates": "0.10.
|
|
27
|
+
"@synopackageland/templates": "0.10.3",
|
|
28
28
|
"ajv": "^8.17.1",
|
|
29
29
|
"commander": "^13.1.0",
|
|
30
30
|
"tar": "^7.4.3",
|
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
"url": "https://github.com/synopackageland/syno-pkg-land-cli/issues"
|
|
50
50
|
},
|
|
51
51
|
"exports": {
|
|
52
|
-
"./authorization-sdk": "./dist/authorization-sdk.js",
|
|
53
52
|
"./dist/*": "./dist/*",
|
|
54
53
|
"./package.json": "./package.json",
|
|
55
54
|
"./native-layout": "./dist/standalone/native-layout.js"
|