@synopackageland/cli 0.10.2 → 0.10.4
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 +259 -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 +22 -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 +58 -12
- 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 +191 -53
- 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 +5 -5
- 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 || {}); }
|
|
@@ -37,37 +58,62 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
37
58
|
var boot = task.kind === "boot";
|
|
38
59
|
return send(boot ? eventAPI : taskAPI, "delete", boot ? 1 : 2, boot ? ref : {tasks: [ref]});
|
|
39
60
|
}
|
|
40
|
-
function
|
|
41
|
-
function
|
|
61
|
+
function isAdmin() { return !!options.user() && options.user().admin === true; }
|
|
62
|
+
function admin() { if (!isAdmin()) throw fail("admin_required"); }
|
|
63
|
+
function missing() { return {state: "needs-admin-approval", reason: "Task is missing.", code: "task_missing"}; }
|
|
64
|
+
// EventScheduler keys a task by its name (DSM refuses a second one with 117
|
|
65
|
+
// DUPLICATE_PRIMARY_KEY) and get answers no data for a missing name, so get
|
|
66
|
+
// alone finds it. Task Scheduler names repeat: a list finds the id and
|
|
67
|
+
// refuses duplicates, and later cached reads get that id until it fails.
|
|
68
|
+
var listed = {};
|
|
69
|
+
function locate(task, cached) {
|
|
70
|
+
if (task.kind === "boot") return Promise.resolve({task_name: task.name});
|
|
71
|
+
if (cached && listed[task.name] !== undefined) return Promise.resolve({id: listed[task.name], real_owner: "root", cached: true});
|
|
72
|
+
return taskRead("list", 3, {start: 0, limit: 500}, 2, {offset: 0, limit: 500}).then(function (data) {
|
|
73
|
+
var records = data && data.tasks;
|
|
74
|
+
if (!Array.isArray(records) || records.length > 500 || !Number.isInteger(data.total) || data.total !== records.length) throw fail("executor_bad_response");
|
|
75
|
+
var named = records.filter(function (r) { return r && r.name === task.name; });
|
|
76
|
+
delete listed[task.name];
|
|
77
|
+
if (!named.length) return null;
|
|
78
|
+
if (named.length !== 1) return {duplicate: true};
|
|
79
|
+
if (!/^[0-9]+$/.test(String(named[0].id))) throw fail("executor_bad_response");
|
|
80
|
+
listed[task.name] = named[0].id;
|
|
81
|
+
return {id: named[0].id, real_owner: "root"};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// cached: get the control task by the id an earlier list in this window showed.
|
|
85
|
+
function find(task, cached) {
|
|
42
86
|
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"};
|
|
87
|
+
return locate(task, cached).then(function (located) {
|
|
88
|
+
if (!located) return missing();
|
|
89
|
+
if (located.duplicate) return {state: "blocked", reason: "Duplicate task names.", code: "task_duplicate"};
|
|
90
|
+
var ref = boot ? located : {id: located.id, real_owner: "root"};
|
|
52
91
|
var detail = boot ? send(eventAPI, "get", 1, ref) : taskRead("get", 4, ref, 3, ref);
|
|
92
|
+
// A cached id that is gone or now names another task is listed again.
|
|
93
|
+
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
94
|
return detail.then(function (r) {
|
|
95
|
+
if (boot && (r === undefined || r === null || (typeof r === "object" && !Object.keys(r).length))) return missing();
|
|
54
96
|
var owner = r && r.owner;
|
|
55
97
|
var root = owner === "root" || (boot && owner && owner["0"] === "root");
|
|
56
98
|
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."};
|
|
99
|
+
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
100
|
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."};
|
|
101
|
+
if (script.indexOf(task.header) === 0) return {state: "needs-admin-approval", ref: ref, script: script, reason: "Approved digests differ.", code: "task_outdated"};
|
|
102
|
+
return {state: "blocked", reason: "Same-named task has an unrelated script.", code: "task_conflict"};
|
|
103
|
+
}, function (e) {
|
|
104
|
+
if (!located.cached || !(e && (e.relist || e.code === "dsm_4801"))) throw e;
|
|
105
|
+
delete listed[task.name];
|
|
106
|
+
return find(task, false);
|
|
61
107
|
});
|
|
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)}; });
|
|
108
|
+
}).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
109
|
}
|
|
64
|
-
function read() {
|
|
65
|
-
try { admin(); } catch (e) { return Promise.resolve({
|
|
66
|
-
return Promise.all(options.tasks.map(find)).then(function (found) {
|
|
110
|
+
function read(cached) {
|
|
111
|
+
try { admin(); } catch (e) { return Promise.resolve(block({tasks: {}}, e)); }
|
|
112
|
+
return Promise.all(options.tasks.map(function (task) { return find(task, cached === true); })).then(function (found) {
|
|
67
113
|
var result = {state: "ready", tasks: {}};
|
|
68
114
|
options.tasks.forEach(function (t, i) {
|
|
69
115
|
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; }
|
|
116
|
+
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
117
|
});
|
|
72
118
|
return result;
|
|
73
119
|
});
|
|
@@ -82,6 +128,18 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
82
128
|
}).catch(function (e) { failed = reason(e); });
|
|
83
129
|
}, Promise.resolve()).then(function () { return failed; });
|
|
84
130
|
}
|
|
131
|
+
// Undoes one approval: withdraws what it created, then recreates, newest
|
|
132
|
+
// first, the earlier approval's tasks it removed. Resolves like withdraw.
|
|
133
|
+
function rollback(created, removed, confirmation) {
|
|
134
|
+
return withdraw(created).then(function (failed) {
|
|
135
|
+
return removed.reduceRight(function (restored, earlier) {
|
|
136
|
+
return restored.then(function () { return find(earlier); }).then(function (current) {
|
|
137
|
+
if (current.state === "blocked") throw refused(current);
|
|
138
|
+
if (!current.ref) return create(earlier.kind === "boot", definition(earlier), confirmation);
|
|
139
|
+
}).catch(function (e) { failed = reason(e); });
|
|
140
|
+
}, Promise.resolve()).then(function () { return failed; });
|
|
141
|
+
});
|
|
142
|
+
}
|
|
85
143
|
function definition(task) { return task.kind === "boot" ? (${bootDefinition.toString()})(task) : (${controlDefinition.toString()})(task); }
|
|
86
144
|
function approve(password, selectedScope) {
|
|
87
145
|
var selectionTask;
|
|
@@ -95,29 +153,44 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
95
153
|
if (!confirmation || confirmation.confirmed !== true) return {state: "cancelled", tasks: before.tasks};
|
|
96
154
|
// DSM 7.1 confirms the password server-side but issues no create token.
|
|
97
155
|
if (!legacy && (typeof confirmation.token !== "string" || !confirmation.token)) throw fail("executor_confirmation_missing");
|
|
98
|
-
var errors = {}, created = [], withdrawFailed;
|
|
156
|
+
var errors = {}, created = [], removed = [], withdrawFailed;
|
|
99
157
|
// Delete only a verified, same-header outdated task, then create the pinned
|
|
100
|
-
// replacement. set has no measured wire contract
|
|
158
|
+
// replacement. set has no measured wire contract, and a same-named task
|
|
159
|
+
// cannot be created first. The boot task keeps this approval's first
|
|
160
|
+
// read: EventScheduler get by name is the whole lookup, it was made in
|
|
161
|
+
// this step moments ago, and delete and create address the name. A
|
|
162
|
+
// control task to delete is read again by the id that read listed
|
|
163
|
+
// (spec/scheduler/v1); a missing one keeps the first read: DSM refuses a
|
|
164
|
+
// second boot task name, and the readback after the writes lists control
|
|
165
|
+
// tasks for duplicates. Stop at the first error.
|
|
101
166
|
var chain = Promise.resolve();
|
|
102
167
|
options.tasks.forEach(function (task) {
|
|
103
168
|
chain = chain.then(function () {
|
|
104
|
-
|
|
169
|
+
if (Object.keys(errors).length) return;
|
|
170
|
+
var first = before.tasks[task.kind];
|
|
171
|
+
return (first.ref && task.kind !== "boot" ? find(task, true) : Promise.resolve(first)).then(function (current) {
|
|
105
172
|
if (current.state === "ready") return;
|
|
106
|
-
if (current.state === "blocked") throw
|
|
107
|
-
var
|
|
108
|
-
return
|
|
109
|
-
}).catch(function (e) { errors[task.kind] =
|
|
173
|
+
if (current.state === "blocked") throw refused(current);
|
|
174
|
+
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();
|
|
175
|
+
return deleted.then(function () { return create(task.kind === "boot", definition(task), confirmation); }).then(function () { created.push(task); });
|
|
176
|
+
}).catch(function (e) { errors[task.kind] = e; });
|
|
110
177
|
});
|
|
111
178
|
});
|
|
112
179
|
// A partial approval must not leave an enabled root bootup task that no
|
|
113
|
-
// control task or uninstall inventory owns
|
|
180
|
+
// control task or uninstall inventory owns, nor lose the earlier approval:
|
|
181
|
+
// roll back to it. A retry converges from whatever the rollback left.
|
|
114
182
|
return chain.then(function () {
|
|
115
|
-
if (Object.keys(errors).length) return
|
|
116
|
-
}).then(read).then(function (after) {
|
|
183
|
+
if (Object.keys(errors).length) return rollback(created, removed, confirmation).then(function (failed) { withdrawFailed = failed; });
|
|
184
|
+
}).then(function () { return read(true); }).then(function (after) {
|
|
117
185
|
Object.keys(errors).forEach(function (kind) {
|
|
118
|
-
if (after.tasks[kind].state !== "ready") { after.tasks[kind]
|
|
186
|
+
if (after.tasks[kind].state !== "ready") { block(after.tasks[kind], errors[kind]); block(after, errors[kind]); }
|
|
119
187
|
});
|
|
120
|
-
if (withdrawFailed) { after.state = "blocked"; after.reason = (after.reason ? after.reason + " " : "") + "Withdrawing the tasks this approval created failed: " + withdrawFailed; }
|
|
188
|
+
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"; }
|
|
189
|
+
else if (Object.keys(errors).length && after.state !== "ready") {
|
|
190
|
+
var restored = Object.keys(before.tasks).some(function (kind) { return before.tasks[kind].ref; });
|
|
191
|
+
after.reason += restored ? " The earlier approval was restored." : " The tasks this approval created were withdrawn.";
|
|
192
|
+
after.outcome = restored ? "restored" : "withdrawn"; after.retry = true;
|
|
193
|
+
}
|
|
121
194
|
// A scope change must not restart a running executor: the boot task
|
|
122
195
|
// stops it, which interrupts in-flight copies instead of canceling them.
|
|
123
196
|
return after.state === "ready" ? (selectionTask ? running(after) : Promise.resolve(null)).then(function (live) { return live || start(after); }).then(function (started) {
|
|
@@ -128,6 +201,7 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
128
201
|
return read().then(function (current) {
|
|
129
202
|
current.state = "blocked";
|
|
130
203
|
current.reason = started.reason + (failed ? " Withdrawing the tasks this approval created failed: " + failed : " The tasks this approval created were withdrawn.");
|
|
204
|
+
current.code = started.code; current.outcome = failed ? "withdraw_failed" : "withdrawn";
|
|
131
205
|
current.retry = !failed;
|
|
132
206
|
return current;
|
|
133
207
|
});
|
|
@@ -141,7 +215,7 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
141
215
|
// app converge by readback: applied once status().executor.selection
|
|
142
216
|
// shows this scope above this generation. A resubmit is pinned and safe.
|
|
143
217
|
return selectScope(selectionTask, confirmation, started, selectedScope).catch(function (e) {
|
|
144
|
-
return {
|
|
218
|
+
return block({tasks: started.tasks, requested: {scope: selectedScope, generation: generation}}, e, "selection_failed");
|
|
145
219
|
});
|
|
146
220
|
}) : after;
|
|
147
221
|
});
|
|
@@ -157,9 +231,10 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
157
231
|
}
|
|
158
232
|
// The task waits up to 35 s for the executor, whose account, AppPriv and
|
|
159
233
|
// share lookups took 16-20 s on a measured NAS: read back for 40 s,
|
|
160
|
-
// backing off from 500 ms to
|
|
234
|
+
// backing off from 500 ms to 1 s, so the window closes within a second
|
|
235
|
+
// of the selection landing.
|
|
161
236
|
function readSelection(budget, delay) {
|
|
162
|
-
delay = Math.min(delay || 500,
|
|
237
|
+
delay = Math.min(delay || 500, 1000);
|
|
163
238
|
verified.state = "ready";
|
|
164
239
|
return health(verified, 0).then(function (current) {
|
|
165
240
|
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;
|
|
@@ -203,65 +278,118 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
203
278
|
// A started executor answered only after its own DSM lookups, which took
|
|
204
279
|
// 16-20 s on a measured NAS; giving up withdraws a fresh approval.
|
|
205
280
|
var startBudget = 30000;
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
281
|
+
// The pinned executor listens before its package start step, which runs as
|
|
282
|
+
// root for at most 60 s and answers state "starting" until it ends.
|
|
283
|
+
var stepBudget = 75000;
|
|
284
|
+
function starting(status) { return !!status && status.state === "starting" && status.executor_digest === (options.pins && options.pins.executorDigest) && status.policy_digest === options.pins.policyDigest; }
|
|
285
|
+
// The approved executor's status. budget: how long to keep asking after a
|
|
286
|
+
// start (ms); a start step's own wait is step. An executor without a start
|
|
287
|
+
// step answered 1.8-3 s after run on a measured NAS: ask every 500 ms for
|
|
288
|
+
// the first 4 s, then double to 2 s so an open window does not poll the
|
|
289
|
+
// executor twice a second.
|
|
290
|
+
function alive(budget, delay, step, waited) {
|
|
291
|
+
waited = waited || 0;
|
|
292
|
+
delay = waited < 4000 ? 500 : Math.min(delay * 2, 2000);
|
|
293
|
+
step = step === undefined ? stepBudget : step;
|
|
211
294
|
return options.call("executor.status", {}).then(function (status) {
|
|
295
|
+
if (starting(status)) throw fail("executor_starting", "the executor's start step has not ended.");
|
|
212
296
|
var why = mismatch(status);
|
|
213
297
|
if (why) throw fail("executor_status_mismatch", why);
|
|
214
|
-
|
|
298
|
+
return status;
|
|
215
299
|
}, function (e) {
|
|
216
300
|
throw fail(e && e.code, "the executor status request failed.");
|
|
217
301
|
}).catch(function (e) {
|
|
218
|
-
|
|
302
|
+
var steps = e.code === "executor_starting";
|
|
303
|
+
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); });
|
|
304
|
+
throw e;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function health(verified, budget) {
|
|
308
|
+
if (verified.state !== "ready") return Promise.resolve(verified);
|
|
309
|
+
return settle(verified, alive(budget));
|
|
310
|
+
}
|
|
311
|
+
function settle(verified, status) {
|
|
312
|
+
return status.then(function (current) { verified.executor = current; return verified; }, function (e) {
|
|
219
313
|
// The tasks are approved: approving again runs start, which restarts the
|
|
220
314
|
// executor without a password, so the panel offers it.
|
|
221
|
-
verified
|
|
315
|
+
block(verified, e); verified.retry = true; return verified;
|
|
222
316
|
});
|
|
223
317
|
}
|
|
224
318
|
function start(verified) {
|
|
225
319
|
return options.call("executor.control", {action: "start"}).then(function () {
|
|
226
320
|
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; });
|
|
321
|
+
}).then(function () { return started(verified); }).catch(function (e) { return block(verified, e, "executor_start_failed"); });
|
|
231
322
|
}
|
|
323
|
+
// Every run follows an exact readback of both tasks in the same step, and a
|
|
324
|
+
// run of a missing task leaves no pinned executor to answer: only the
|
|
325
|
+
// executor status is asked after it.
|
|
326
|
+
function started(verified) { return settle(verified, alive(startBudget)); }
|
|
232
327
|
// The window panel and the app's own settings share this access; each
|
|
233
328
|
// result reaches every view so neither shows a stale state.
|
|
234
329
|
var listeners = [];
|
|
235
330
|
function publish(result) { listeners.forEach(function (listener) { listener(result); }); return result; }
|
|
331
|
+
// TaskScheduler answers administrators only. Anyone else reads the approved
|
|
332
|
+
// executor through the App's executor.status, which answers every user: the
|
|
333
|
+
// pinned, running executor is ready; otherwise an administrator is needed.
|
|
334
|
+
function userStatus() {
|
|
335
|
+
var needed = block({tasks: {}}, fail("admin_required"));
|
|
336
|
+
return options.call("executor.status", {}).then(function (status) {
|
|
337
|
+
return pinned(status) ? {state: "ready", tasks: {}, executor: status} : needed;
|
|
338
|
+
}, function () { return needed; });
|
|
339
|
+
}
|
|
236
340
|
return {
|
|
237
|
-
status: function () { return read().then(function (verified) { return health(verified, 0); }).then(publish); },
|
|
341
|
+
status: function () { return (isAdmin() ? read().then(function (verified) { return health(verified, 0); }) : userStatus()).then(publish); },
|
|
238
342
|
authorize: function (password, selectedScope) {
|
|
239
|
-
if (!busy) busy = Promise.resolve().then(function () { return approve(password, selectedScope); }).catch(function (e) {
|
|
343
|
+
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
344
|
return busy;
|
|
241
345
|
},
|
|
242
346
|
control: function (action) {
|
|
243
347
|
if (["start", "stop", "purge"].indexOf(action) < 0) return Promise.reject(fail("executor_control_invalid"));
|
|
244
|
-
return read().then(function (verified) {
|
|
348
|
+
return read(true).then(function (verified) {
|
|
245
349
|
if (verified.state !== "ready") throw fail("executor_not_ready");
|
|
246
350
|
return options.call("executor.control", {action: action}).then(function () {
|
|
247
351
|
return send(taskAPI, "run", 2, {tasks: [verified.tasks.control.ref]});
|
|
248
|
-
}).then(
|
|
352
|
+
}).then(function () { return action === "start" ? started(verified) : read(true); });
|
|
249
353
|
}).then(publish);
|
|
250
354
|
},
|
|
251
355
|
subscribe: function (listener) { listeners.push(listener); }
|
|
252
356
|
};
|
|
253
357
|
}`;
|
|
254
|
-
/** The plain-UI window panel's text
|
|
358
|
+
/** The plain-UI window panel's text: a state, and a result's code and outcome; reason stays the runtime's English detail. */
|
|
255
359
|
export const ROOT_PANEL_MESSAGES = {
|
|
256
360
|
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
361
|
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
362
|
approve: { en: "Approve", "zh-TW": "核准", "zh-CN": "批准", ja: "承認", ko: "승인", de: "Genehmigen", fr: "Approuver", es: "Aprobar", it: "Approva" },
|
|
259
363
|
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" },
|
|
364
|
+
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
365
|
"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
366
|
"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
367
|
"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
368
|
"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
369
|
"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." },
|
|
370
|
+
"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." },
|
|
371
|
+
"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." },
|
|
372
|
+
"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." },
|
|
373
|
+
"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." },
|
|
374
|
+
"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." },
|
|
375
|
+
"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." },
|
|
376
|
+
"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." },
|
|
377
|
+
"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." },
|
|
378
|
+
"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." },
|
|
379
|
+
"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." },
|
|
380
|
+
"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." },
|
|
381
|
+
"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." },
|
|
382
|
+
"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." },
|
|
383
|
+
"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." },
|
|
384
|
+
"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." },
|
|
385
|
+
"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." },
|
|
386
|
+
"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." },
|
|
387
|
+
"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." },
|
|
388
|
+
"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." },
|
|
389
|
+
"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." },
|
|
390
|
+
"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." },
|
|
391
|
+
"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." },
|
|
392
|
+
"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
393
|
};
|
|
266
394
|
/** ROOT_PANEL_MESSAGES as createI18n catalogs, one per locale. */
|
|
267
395
|
export function rootPanelCatalogs() {
|
|
@@ -288,12 +416,22 @@ export const ROOT_PANEL = `function mountRootPanel(host, access, t, admin) {
|
|
|
288
416
|
form.appendChild(label); form.appendChild(actions);
|
|
289
417
|
panel.appendChild(title); panel.appendChild(status); panel.appendChild(reason); panel.appendChild(form);
|
|
290
418
|
host.appendChild(panel);
|
|
419
|
+
// The code and outcome in the DSM language, then the DSM code the English reason starts with.
|
|
420
|
+
function explain(result) {
|
|
421
|
+
if (!result.reason && !result.code) return "";
|
|
422
|
+
var parts = [t("reason." + (result.code || "failed"))];
|
|
423
|
+
if (result.outcome) parts.push(t("outcome." + result.outcome));
|
|
424
|
+
var detail = /^([A-Za-z0-9_]+):/.exec(result.reason || "");
|
|
425
|
+
if (detail) parts.push("(" + detail[1] + ")");
|
|
426
|
+
return parts.reduce(function (text, part) { return text + (/[\u3000-\u30ff\u4e00-\u9fff\uac00-\ud7af\uff00-\uffef]$/.test(text) ? "" : " ") + part; });
|
|
427
|
+
}
|
|
291
428
|
function show(result) {
|
|
292
429
|
status.textContent = t("state." + result.state);
|
|
293
|
-
reason.textContent = result
|
|
430
|
+
reason.textContent = explain(result); reason.hidden = !reason.textContent;
|
|
294
431
|
label.hidden = approve.hidden = result.state !== "needs-admin-approval" && result.retry !== true;
|
|
295
432
|
}
|
|
296
|
-
|
|
433
|
+
// A start step can hold the background service for up to a minute.
|
|
434
|
+
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
435
|
access.subscribe(show);
|
|
298
436
|
form.onsubmit = function(event) {
|
|
299
437
|
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"]) ??
|