@synopackageland/cli 0.9.4 → 0.9.6
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 +94 -0
- package/README.md +1 -1
- package/assets/licenses/MUSL-COPYRIGHT +193 -0
- package/assets/licenses/RUST-COPYRIGHT-library.html +5556 -0
- package/dist/add.js +29 -29
- package/dist/build-spk/dsm-version.d.ts +14 -4
- package/dist/build-spk/dsm-version.js +17 -5
- package/dist/build-spk/notices.d.ts +8 -1
- package/dist/build-spk/notices.js +13 -4
- package/dist/build-spk/uninstall-wizard.d.ts +11 -0
- package/dist/build-spk/uninstall-wizard.js +16 -3
- package/dist/build-spk/wizard-i18n.d.ts +1 -1
- package/dist/build-spk/wizard-i18n.js +1 -1
- package/dist/build-spk/wizard-messages.d.ts +44 -0
- package/dist/build-spk/wizard-messages.js +44 -0
- package/dist/capability-overlays.d.ts +10 -5
- package/dist/capability-overlays.js +32 -10
- package/dist/cli.js +5 -5
- package/dist/deploy/plan.d.ts +1 -1
- package/dist/deploy/plan.js +1 -1
- package/dist/error-registry.js +46 -4
- package/dist/info.js +4 -3
- package/dist/init.js +53 -28
- package/dist/portal/alias.d.ts +8 -0
- package/dist/portal/alias.js +16 -0
- package/dist/runtime/executor-build.d.ts +1 -1
- package/dist/runtime/executor-build.js +2 -1
- package/dist/runtime/executor-digests.json +22 -0
- package/dist/runtime/executor-policy.d.ts +15 -0
- package/dist/runtime/executor-policy.js +32 -6
- package/dist/runtime/sdk-source.d.ts +14 -2
- package/dist/runtime/sdk-source.js +39 -4
- package/dist/runtime/uninstall-cleanup.d.ts +5 -1
- package/dist/runtime/uninstall-cleanup.js +11 -1
- package/dist/standalone/api-permissions.d.ts +4 -2
- package/dist/standalone/api-permissions.js +6 -5
- package/dist/standalone/binary.d.ts +3 -2
- package/dist/standalone/binary.js +17 -5
- package/dist/standalone/build.js +16 -12
- package/dist/standalone/daemon.d.ts +6 -0
- package/dist/standalone/daemon.js +8 -4
- package/dist/standalone/dsm-window.js +16 -3
- package/dist/standalone/inspect-spk.d.ts +1 -1
- package/dist/standalone/inspect-spk.js +46 -13
- package/dist/standalone/lifecycle.js +38 -4
- package/dist/standalone/manifest.d.ts +12 -0
- package/dist/standalone/manifest.js +62 -20
- package/dist/standalone/native-layout.d.ts +4 -7
- package/dist/standalone/native-layout.js +41 -102
- package/dist/standalone/native-root-window.d.ts +2 -1
- package/dist/standalone/native-root-window.js +15 -5
- package/dist/standalone/portal-window.d.ts +4 -2
- package/dist/standalone/portal-window.js +54 -12
- package/dist/standalone/portal.d.ts +4 -1
- package/dist/standalone/portal.js +8 -2
- package/dist/standalone/root-ops.d.ts +2 -0
- package/dist/standalone/root-ops.js +2 -0
- package/dist/standalone/rust-build.d.ts +35 -0
- package/dist/standalone/rust-build.js +127 -0
- package/dist/standalone/sdk-check.d.ts +5 -3
- package/dist/standalone/sdk-check.js +21 -8
- package/dist/standalone/spk.js +15 -3
- package/dist/standalone/toolkit.d.ts +10 -0
- package/dist/standalone/toolkit.js +101 -0
- package/dist/standalone/uninstall-hook.d.ts +10 -0
- package/dist/standalone/uninstall-hook.js +71 -0
- package/dist/standalone/uninstall.d.ts +53 -0
- package/dist/standalone/uninstall.js +136 -0
- package/dist/types.d.ts +1 -1
- package/dist/yaml-edit.d.ts +6 -0
- package/dist/yaml-edit.js +31 -0
- package/package.json +2 -2
- package/skills/reference/portal.md +8 -4
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
/** The root commands that remove an Application Portal entry the executor could not unregister. */
|
|
2
|
+
export function portalEntryCleanupCommands(pkg) {
|
|
3
|
+
return [
|
|
4
|
+
`rm -f /usr/syno/etc/www/app.d/${pkg}.json /usr/syno/etc/www/app.d/${pkg}.mustache /usr/syno/etc/packages/${pkg}/AppPortal.json`,
|
|
5
|
+
`for c in dsm www alias .alias; do n=$c.syno-app-portal.${pkg}.conf; synow3tool --disable-nginx-confd=$n; synow3tool --unreg-nginx-confd=$n; done`,
|
|
6
|
+
`for c in server .server; do n=$c.syno-app-portal.${pkg}.conf; synow3tool --disable-nginx-sites=$n; synow3tool --unreg-nginx-sites=$n; done`,
|
|
7
|
+
'nginx -t && synow3tool --nginx=reload',
|
|
8
|
+
];
|
|
9
|
+
}
|
|
1
10
|
const AUTOMATIC_FAILED = 'Only if automatic cleanup failed after uninstall; see the system log entry synopkgland-executor.';
|
|
2
|
-
export function uninstallCleanup(pkg, { executor = false, portal = false } = {}) {
|
|
11
|
+
export function uninstallCleanup(pkg, { executor = false, portal = false, portalEntry = false } = {}) {
|
|
3
12
|
const items = [
|
|
4
13
|
{ target: `@appdata/${pkg}, @appconf/${pkg}, @apphome/${pkg}`, condition: 'After uninstall, if retained data or empty DSM directories are no longer needed.', action: 'Back up retained data; an administrator may remove only these package-specific directories. The delete-data checkbox empties their contents and preserves DSM directory roots.' },
|
|
5
14
|
{ target: `@appshare/${pkg}, /usr/syno/etc/packages/${pkg}`, condition: 'After uninstall, if DSM leaves package infrastructure or a package-config symlink.', action: 'Administrator: inspect these package-specific paths and remove unused infrastructure; do not follow the symlink to delete shared data.' },
|
|
@@ -8,6 +17,7 @@ export function uninstallCleanup(pkg, { executor = false, portal = false } = {})
|
|
|
8
17
|
? { target: `SYNO.SDS.${pkg}.Instance rules in Application Privileges`, condition: AUTOMATIC_FAILED, action: 'The root executor removes these rules automatically through the Application Privileges API on uninstall. Administrator: remove surviving rules in Control Panel > Application Privileges; DSM would apply them again to a later install under the same name.' }
|
|
9
18
|
: { target: `SYNO.SDS.${pkg}.Instance rules in Application Privileges`, condition: 'After uninstall, if this package will not be reinstalled.', action: 'DSM keeps these rules in /etc/synoappprivilege.db and applies them again to a later install under the same name. Administrator: remove them in Control Panel > Application Privileges.' },
|
|
10
19
|
...(executor && portal ? [{ target: `${pkg} Login Portal alias`, condition: AUTOMATIC_FAILED, action: 'DSM removes the alias configuration without reloading its web server; the root executor then reloads the web server automatically. Administrator: if the alias still opens a DSM page, save an application alias in Control Panel > Login Portal > Applications to reload it, or restart the NAS.' }] : []),
|
|
20
|
+
...(executor && portalEntry ? [{ target: `${pkg} Application Portal entry`, condition: 'After uninstall, if the executor was not running or was never approved, so it could not remove the entry.', action: `Administrator, as root: ${portalEntryCleanupCommands(pkg).join('; ')}` }] : []),
|
|
11
21
|
{ target: `${pkg} package account/group`, condition: 'After uninstall, only if this package will not be reinstalled and the identity is unused.', action: 'Administrator: review DSM internal account/group and associated ACLs before cleanup. Package scripts do not delete system identities.' },
|
|
12
22
|
];
|
|
13
23
|
if (executor)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Capabilities } from "./manifest.js";
|
|
2
|
-
|
|
2
|
+
import { type SdkBackend } from "../runtime/sdk-source.js";
|
|
3
|
+
/** The App route policy inside each backend kind's directory. */
|
|
4
|
+
export declare const API_PERMISSIONS_FILE: Record<SdkBackend["kind"], string>;
|
|
3
5
|
type SdkPermissions = Record<string, Record<string, ApiPermission>>;
|
|
4
6
|
export declare const MAX_PERMISSIONS_BYTES = 65536;
|
|
5
7
|
export declare const MAX_PERMISSION_METHODS = 1024;
|
|
@@ -10,7 +12,7 @@ export interface ApiPermissions {
|
|
|
10
12
|
methods: Record<string, ApiPermission>;
|
|
11
13
|
}
|
|
12
14
|
export declare function parseApiPermissions(raw: Uint8Array): ApiPermissions;
|
|
13
|
-
export declare function readApiPermissions(
|
|
15
|
+
export declare function readApiPermissions(backend: SdkBackend, capabilities?: Capabilities): {
|
|
14
16
|
policy: ApiPermissions;
|
|
15
17
|
digest: string;
|
|
16
18
|
sdkDigest: string;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { lstatSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { appSdkVersion, sdkSource } from "../runtime/sdk-source.js";
|
|
5
5
|
import { parseDocument } from "yaml";
|
|
6
|
-
|
|
6
|
+
/** The App route policy inside each backend kind's directory. */
|
|
7
|
+
export const API_PERMISSIONS_FILE = { go: "internal/api/permissions.json", rust: "permissions.json" };
|
|
7
8
|
/** SDK-owned routes per capability, in the Go module of the SDK version the App requires. */
|
|
8
9
|
const SDK_PERMISSIONS_FILE = "api/sdk-permissions.json";
|
|
9
10
|
export const MAX_PERMISSIONS_BYTES = 65536;
|
|
@@ -32,14 +33,14 @@ export function parseApiPermissions(raw) {
|
|
|
32
33
|
}
|
|
33
34
|
return value;
|
|
34
35
|
}
|
|
35
|
-
export function readApiPermissions(
|
|
36
|
-
const file = join(
|
|
36
|
+
export function readApiPermissions(backend, capabilities = {}) {
|
|
37
|
+
const file = join(backend.dir, API_PERMISSIONS_FILE[backend.kind]);
|
|
37
38
|
const stat = lstatSync(file);
|
|
38
39
|
if (!stat.isFile() || stat.size > MAX_PERMISSIONS_BYTES)
|
|
39
40
|
throw new Error("permissions must be a regular bounded JSON file");
|
|
40
41
|
const raw = readFileSync(file);
|
|
41
42
|
const policy = parseApiPermissions(raw);
|
|
42
|
-
const version =
|
|
43
|
+
const version = appSdkVersion(backend);
|
|
43
44
|
const sdkRaw = version ? readFileSync(join(sdkSource(version).goDir, SDK_PERMISSIONS_FILE)) : Buffer.from("{}");
|
|
44
45
|
const sdk = JSON.parse(sdkRaw.toString("utf8"));
|
|
45
46
|
for (const methods of Object.values(sdk))
|
|
@@ -4,7 +4,8 @@ export declare const ELF_MACHINE: Record<NativeArchFamily, number>;
|
|
|
4
4
|
/**
|
|
5
5
|
* Checks a prebuilt CGI before it is packaged: a regular file inside the
|
|
6
6
|
* project, a little-endian ELF of `arch`'s class and machine, and statically linked
|
|
7
|
-
* (no PT_INTERP), because DSM models ship different libc versions
|
|
7
|
+
* (no PT_INTERP), because DSM models ship different libc versions; only a
|
|
8
|
+
* build that links against a known DSM glibc may name its `interpreter`.
|
|
8
9
|
* Returns undefined when the file is acceptable.
|
|
9
10
|
*/
|
|
10
|
-
export declare function checkCgiBinary(projectRoot: string, file: string, arch: NativeArchFamily): StandaloneError | undefined;
|
|
11
|
+
export declare function checkCgiBinary(projectRoot: string, file: string, arch: NativeArchFamily, interpreter?: string): StandaloneError | undefined;
|
|
@@ -11,10 +11,11 @@ function fail(code, message) {
|
|
|
11
11
|
/**
|
|
12
12
|
* Checks a prebuilt CGI before it is packaged: a regular file inside the
|
|
13
13
|
* project, a little-endian ELF of `arch`'s class and machine, and statically linked
|
|
14
|
-
* (no PT_INTERP), because DSM models ship different libc versions
|
|
14
|
+
* (no PT_INTERP), because DSM models ship different libc versions; only a
|
|
15
|
+
* build that links against a known DSM glibc may name its `interpreter`.
|
|
15
16
|
* Returns undefined when the file is acceptable.
|
|
16
17
|
*/
|
|
17
|
-
export function checkCgiBinary(projectRoot, file, arch) {
|
|
18
|
+
export function checkCgiBinary(projectRoot, file, arch, interpreter) {
|
|
18
19
|
let stat;
|
|
19
20
|
try {
|
|
20
21
|
stat = lstatSync(file);
|
|
@@ -32,7 +33,7 @@ export function checkCgiBinary(projectRoot, file, arch) {
|
|
|
32
33
|
if (stat.size > MAX_BINARY_BYTES) {
|
|
33
34
|
return fail("STANDALONE_BINARY_TOO_LARGE", `${file} 超過 256 MiB。`);
|
|
34
35
|
}
|
|
35
|
-
return checkElf(readHeader(file), file, arch);
|
|
36
|
+
return checkElf(readHeader(file), file, arch, interpreter);
|
|
36
37
|
}
|
|
37
38
|
function readHeader(file) {
|
|
38
39
|
const fd = openSync(file, "r");
|
|
@@ -58,7 +59,7 @@ function readElfHeader(buf) {
|
|
|
58
59
|
}
|
|
59
60
|
return undefined;
|
|
60
61
|
}
|
|
61
|
-
function checkElf(buf, file, arch) {
|
|
62
|
+
function checkElf(buf, file, arch, interpreter) {
|
|
62
63
|
const bits = ELF_CLASS64[arch] ? 64 : 32;
|
|
63
64
|
const notElf = fail("STANDALONE_BINARY_NOT_ELF", `${file} 不是 ${arch} 需要的 ${bits}-bit little-endian Linux ELF 執行檔。`);
|
|
64
65
|
const header = readElfHeader(buf);
|
|
@@ -73,9 +74,20 @@ function checkElf(buf, file, arch) {
|
|
|
73
74
|
return notElf;
|
|
74
75
|
}
|
|
75
76
|
for (let i = 0; i < phnum; i++) {
|
|
76
|
-
|
|
77
|
+
const entry = phoff + i * phentsize;
|
|
78
|
+
if (buf.readUInt32LE(entry) === PT_INTERP && (interpreter === undefined || interpreterOf(buf, entry, header.class64) !== interpreter)) {
|
|
77
79
|
return fail("STANDALONE_BINARY_DYNAMIC", `${file} 是動態連結執行檔。請改用靜態連結(Go:CGO_ENABLED=0;Rust:*-unknown-linux-musl)。`);
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
return undefined;
|
|
81
83
|
}
|
|
84
|
+
/** The path a PT_INTERP program header names, when it lies within the read header bytes. */
|
|
85
|
+
function interpreterOf(buf, entry, class64) {
|
|
86
|
+
if (entry + (class64 ? 40 : 20) > buf.length)
|
|
87
|
+
return undefined;
|
|
88
|
+
const offset = class64 ? Number(buf.readBigUInt64LE(entry + 8)) : buf.readUInt32LE(entry + 4);
|
|
89
|
+
const size = class64 ? Number(buf.readBigUInt64LE(entry + 32)) : buf.readUInt32LE(entry + 16);
|
|
90
|
+
if (size < 1 || offset + size > buf.length || buf[offset + size - 1] !== 0)
|
|
91
|
+
return undefined;
|
|
92
|
+
return buf.toString("latin1", offset, offset + size - 1);
|
|
93
|
+
}
|
package/dist/standalone/build.js
CHANGED
|
@@ -4,9 +4,10 @@ import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { goBuildEnv } from "../build-spk/dsm-arch.js";
|
|
7
|
-
import { resolveMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast,
|
|
7
|
+
import { resolveMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, portalByExecutor, portalMinDsmMessage } from "../build-spk/dsm-version.js";
|
|
8
8
|
import { checkCgiBinary } from "./binary.js";
|
|
9
9
|
import { loadStandaloneManifest } from "./manifest.js";
|
|
10
|
+
import { buildRustBackend } from "./rust-build.js";
|
|
10
11
|
import { assembleStandaloneSpk, StandaloneBuildError } from "./spk.js";
|
|
11
12
|
import { buildDaemonControl } from "./daemon.js";
|
|
12
13
|
import { buildExecutor } from "../runtime/executor-build.js";
|
|
@@ -80,7 +81,7 @@ export async function buildStandalone(options) {
|
|
|
80
81
|
if (manifest.capabilities.rootOps === true && !isDsmAtLeast(manifest.os_min_ver, DEFAULT_MIN_DSM)) {
|
|
81
82
|
throw new StandaloneBuildError("STANDALONE_OS_MIN_VER_INVALID", `root templates require DSM ${DEFAULT_MIN_DSM} or later.`);
|
|
82
83
|
}
|
|
83
|
-
if (manifest.portal && !isDsmAtLeast(manifest.os_min_ver,
|
|
84
|
+
if (manifest.portal && !isDsmAtLeast(manifest.os_min_ver, PORTAL_EXECUTOR_MIN_DSM)) {
|
|
84
85
|
throw new StandaloneBuildError("STANDALONE_PORTAL_MIN_DSM", `[STANDALONE_PORTAL_MIN_DSM] ${portalMinDsmMessage(manifest.os_min_ver, "portal")}`);
|
|
85
86
|
}
|
|
86
87
|
const jobs = checkJobs(options.jobs);
|
|
@@ -102,14 +103,9 @@ export async function buildStandalone(options) {
|
|
|
102
103
|
const executor = await buildExecutor(manifest, arch, archWork, run);
|
|
103
104
|
executor?.warnings.forEach(warning => warnings.add(warning));
|
|
104
105
|
const builtManifest = executor ? { ...manifest, executor } : manifest;
|
|
105
|
-
const cgiPath = await backendFor(builtManifest, arch, archWork, run);
|
|
106
|
-
const problem = checkCgiBinary(manifest.backend.kind === "go" ? archWork : manifest.root, cgiPath, arch);
|
|
107
|
-
if (problem) {
|
|
108
|
-
throw new StandaloneBuildError(problem.code, problem.message);
|
|
109
|
-
}
|
|
106
|
+
const { cgi: cgiPath, worker: backgroundWorker } = await backendFor(builtManifest, arch, archWork, run);
|
|
110
107
|
const outputPath = join(stage, `${manifest.package}-${manifest.spkVersion}-${arch}.spk`);
|
|
111
108
|
const daemonControl = await buildDaemonControl(manifest, arch, archWork, run);
|
|
112
|
-
const backgroundWorker = await buildBackgroundWorker(builtManifest, arch, archWork, run);
|
|
113
109
|
await assembleStandaloneSpk({ manifest: builtManifest, arch, cgiPath, outputPath, daemonControl, backgroundWorker, executor });
|
|
114
110
|
return outputPath;
|
|
115
111
|
}));
|
|
@@ -133,14 +129,22 @@ async function buildUi(manifest, run) {
|
|
|
133
129
|
throw new StandaloneBuildError("STANDALONE_UI_BUILD_FAILED", `UI build(${build.command.join(" ")})失敗。${installHint}\n${result.stderr}`);
|
|
134
130
|
}
|
|
135
131
|
}
|
|
132
|
+
/** The checked CGI of one architecture and, with capabilities.jobs, its fixed worker. */
|
|
136
133
|
async function backendFor(manifest, arch, work, run) {
|
|
137
134
|
const backend = manifest.backend;
|
|
138
|
-
if (backend.kind === "
|
|
139
|
-
return
|
|
135
|
+
if (backend.kind === "rust")
|
|
136
|
+
return buildRustBackend(manifest, arch, work, run);
|
|
137
|
+
const cgi = backend.kind === "binary" ? backend.files[arch] : await buildGoCgi(manifest, backend, arch, work, run);
|
|
138
|
+
const problem = checkCgiBinary(backend.kind === "go" ? work : manifest.root, cgi, arch);
|
|
139
|
+
if (problem) {
|
|
140
|
+
throw new StandaloneBuildError(problem.code, problem.message);
|
|
140
141
|
}
|
|
142
|
+
return { cgi, worker: await buildBackgroundWorker(manifest, arch, work, run) };
|
|
143
|
+
}
|
|
144
|
+
async function buildGoCgi(manifest, backend, arch, work, run) {
|
|
141
145
|
// UI build hooks run after validation. Refuse policy changes rather than
|
|
142
146
|
// compiling CGI permissions different from the launcher metadata snapshot.
|
|
143
|
-
if (readApiPermissions(backend
|
|
147
|
+
if (readApiPermissions(backend, manifest.capabilities).digest !== manifest.apiPermissionsDigest || readApiPermissions(backend, manifest.capabilities).sdkDigest !== manifest.apiSDKPermissionsDigest) {
|
|
144
148
|
throw new StandaloneBuildError("STANDALONE_API_PERMISSIONS_CHANGED", "API permissions changed during build; check and rebuild.");
|
|
145
149
|
}
|
|
146
150
|
const module = goModulePath(backend.dir);
|
|
@@ -160,7 +164,7 @@ async function backendFor(manifest, arch, work, run) {
|
|
|
160
164
|
...(manifest.capabilities.identity ? [`-X main.identity=${manifest.capabilities.identity}`] : []),
|
|
161
165
|
...(manifest.capabilities.files || manifest.capabilities.delegate ? [`-X main.fileStation=session`] : []),
|
|
162
166
|
...(manifest.entry ? [`-X main.entry=${manifest.entry}`] : []),
|
|
163
|
-
...(manifest.portal ? [
|
|
167
|
+
...(manifest.portal ? [`-X main.portal=${portalByExecutor(manifest.os_min_ver) ? "executor" : "on"}`] : []),
|
|
164
168
|
].join(" ");
|
|
165
169
|
const result = await run("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, "-o", out, backend.main], {
|
|
166
170
|
cwd: backend.dir,
|
|
@@ -18,6 +18,12 @@ export interface Daemon {
|
|
|
18
18
|
uninstall: number;
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
+
/** A fixed argv or env value: only the package's own directories expand. */
|
|
22
|
+
export declare const token: (v: unknown) => v is string;
|
|
23
|
+
/** An application variable that cannot steer the loader, the shell or DSM. */
|
|
24
|
+
export declare const envKey: (k: string) => boolean;
|
|
25
|
+
/** A relative path to a fixed executable, without `.` or `..` parts. */
|
|
26
|
+
export declare const relativeExec: (v: unknown) => v is string;
|
|
21
27
|
export declare function readDaemon(value: unknown, root: string, arches: NativeArchFamily[], fail: (error: StandaloneError) => void): Daemon | undefined;
|
|
22
28
|
export declare function daemonConfig(manifest: StandaloneManifest): string;
|
|
23
29
|
export declare function buildDaemonControl(manifest: StandaloneManifest, arch: NativeArchFamily, work: string, run: CommandRunner): Promise<string | undefined>;
|
|
@@ -6,18 +6,22 @@ import { checkCgiBinary } from "./binary.js";
|
|
|
6
6
|
import { StandaloneBuildError } from "./spk.js";
|
|
7
7
|
const fields = new Set(["exec", "targets", "args", "env", "timeouts", "uninstall_args"]);
|
|
8
8
|
const record = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
9
|
-
|
|
9
|
+
/** A fixed argv or env value: only the package's own directories expand. */
|
|
10
|
+
export const token = (v) => typeof v === "string" && Buffer.byteLength(v) <= 4096 && !v.includes("\0")
|
|
10
11
|
&& !v.replaceAll("${SYNOPKG_PKGDEST}", "").replaceAll("${SYNOPKG_PKGVAR}", "").includes("${");
|
|
11
|
-
|
|
12
|
+
/** An application variable that cannot steer the loader, the shell or DSM. */
|
|
13
|
+
export const envKey = (k) => /^[A-Z][A-Z0-9_]{0,63}$/.test(k)
|
|
12
14
|
&& !/^(PATH|HOME|TMPDIR|ENV|BASH_ENV|SHELLOPTS|BASHOPTS|GODEBUG|GOTRACEBACK|LD_.*|DYLD_.*|SYNOPKG_.*)$/.test(k);
|
|
15
|
+
/** A relative path to a fixed executable, without `.` or `..` parts. */
|
|
16
|
+
export const relativeExec = (v) => typeof v === "string" && v.length <= 200
|
|
17
|
+
&& /^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_.-]+)*$/.test(v) && !v.split("/").some(p => p === "." || p === "..");
|
|
13
18
|
export function readDaemon(value, root, arches, fail) {
|
|
14
19
|
if (value === undefined)
|
|
15
20
|
return undefined;
|
|
16
21
|
const invalid = () => { fail({ code: "STANDALONE_DAEMON_INVALID", message: "daemon 只接受固定 exec、targets、args、env 與 timeouts;路徑須在專案內,環境不接受 loader/shell/system 變數。" }); return undefined; };
|
|
17
22
|
if (!record(value) || Object.keys(value).some(k => !fields.has(k)))
|
|
18
23
|
return invalid();
|
|
19
|
-
if (
|
|
20
|
-
|| value.exec.split("/").some(p => p === "." || p === "..") || value.exec.length > 200)
|
|
24
|
+
if (!relativeExec(value.exec))
|
|
21
25
|
return invalid();
|
|
22
26
|
if (!record(value.targets) || Object.keys(value.targets).length !== arches.length)
|
|
23
27
|
return invalid();
|
|
@@ -4,6 +4,7 @@ import { PASSWORD_CONFIRM, ROOT_PANEL, ROOT_RUNTIME, rootPanelCatalogs } from ".
|
|
|
4
4
|
import { HOST_ADAPTER_RUNTIME } from "./host-adapter.js";
|
|
5
5
|
import { permitsOrdinaryUsers } from "./api-permissions.js";
|
|
6
6
|
import { PORTAL_RUNTIME } from "./portal-window.js";
|
|
7
|
+
import { portalByExecutor } from "../build-spk/dsm-version.js";
|
|
7
8
|
import { uiTitleFields } from "../build-spk/ui-texts.js";
|
|
8
9
|
/** Same path the generated wrapper POSTs to, and the payload location of the CGI. */
|
|
9
10
|
export const CGI_PAYLOAD_PATH = "ui/cgi/api.cgi";
|
|
@@ -172,6 +173,15 @@ ${root ? root.definitions : ""}${manifest.portal ? renderPortalDefinition(ns) :
|
|
|
172
173
|
document.head.appendChild(js);
|
|
173
174
|
};
|
|
174
175
|
|
|
176
|
+
// DSM reopens a window at the size it was last left at, even one saved on a
|
|
177
|
+
// larger screen, and only moves a window that does not fit: keep it inside.
|
|
178
|
+
${ns}.fitDesktop = function (size) {
|
|
179
|
+
var taskbar = SYNO.SDS.TaskBar ? SYNO.SDS.TaskBar.height() : 0;
|
|
180
|
+
size.width = Math.min(size.width, Ext.lib.Dom.getViewWidth());
|
|
181
|
+
size.height = Math.min(size.height, Ext.lib.Dom.getViewHeight() - taskbar);
|
|
182
|
+
return size;
|
|
183
|
+
};
|
|
184
|
+
|
|
175
185
|
Ext.define(${JSON.stringify(`${ns}.Instance`)}, {
|
|
176
186
|
extend: "SYNO.SDS.AppInstance",
|
|
177
187
|
appWindowName: ${JSON.stringify(`${ns}.MainWindow`)}
|
|
@@ -180,10 +190,13 @@ Ext.define(${JSON.stringify(`${ns}.Instance`)}, {
|
|
|
180
190
|
Ext.define(${JSON.stringify(`${ns}.MainWindow`)}, {
|
|
181
191
|
extend: "SYNO.SDS.AppWindow",
|
|
182
192
|
constructor: function (config) {
|
|
183
|
-
this.callParent([Ext.apply({
|
|
193
|
+
this.callParent([${ns}.fitDesktop(Ext.apply({
|
|
184
194
|
width: 960, height: 640, minWidth: 700, minHeight: 420, layout: "fit",
|
|
185
195
|
${root ? root.html : `html: '<div class="dsmapp-root" style="height:100%"></div>'`}
|
|
186
|
-
}, config)]);
|
|
196
|
+
}, config))]);
|
|
197
|
+
},
|
|
198
|
+
getRestoreSizePos: function (config) {
|
|
199
|
+
return ${ns}.fitDesktop(Ext.apply({ width: config.width, height: config.height }, this.callParent(arguments)));
|
|
187
200
|
},
|
|
188
201
|
onOpen: function () {
|
|
189
202
|
this.callParent(arguments);
|
|
@@ -267,7 +280,7 @@ ${PORTAL_RUNTIME}
|
|
|
267
280
|
function renderPortalAccess(ns, manifest) {
|
|
268
281
|
return `${ns}.createPortalAccess({
|
|
269
282
|
appId: ${JSON.stringify(`${ns}.Instance`)},
|
|
270
|
-
defaultAlias: ${JSON.stringify(manifest.portal.alias)},
|
|
283
|
+
defaultAlias: ${JSON.stringify(manifest.portal.alias)},${portalByExecutor(manifest.os_min_ver) ? "\n entry: true," : ""}
|
|
271
284
|
isAdmin: function () { return ${ns}.sessionUser().isAdmin; },
|
|
272
285
|
send: ${ns}.webApi(me),
|
|
273
286
|
call: ${ns}.call,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ExecutorInspection } from "../runtime/executor-inspect.js";
|
|
2
2
|
import type { EntryMode } from "./manifest.js";
|
|
3
3
|
import { type UninstallCleanupItem } from "../runtime/uninstall-cleanup.js";
|
|
4
|
-
export type SpkKind = "native-go" | "binary" | "container" | "native-layout";
|
|
4
|
+
export type SpkKind = "native-go" | "native-rust" | "binary" | "container" | "native-layout";
|
|
5
5
|
export interface SpkInspection {
|
|
6
6
|
kind?: SpkKind;
|
|
7
7
|
package?: string;
|
|
@@ -6,10 +6,13 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import * as tar from "tar";
|
|
8
8
|
import { familyOfArchToken, NATIVE_ARCH_FAMILIES } from "../build-spk/dsm-arch.js";
|
|
9
|
-
import { CONTAINER_MIN_DSM, isDsmAtLeast, PORTAL_MIN_DSM } from "../build-spk/dsm-version.js";
|
|
9
|
+
import { CONTAINER_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, PORTAL_MIN_DSM, PORTAL_MIN_DSM_REASON } from "../build-spk/dsm-version.js";
|
|
10
10
|
import { ELF_MACHINE } from "./binary.js";
|
|
11
11
|
import { CGI_PAYLOAD_PATH, entryLauncherAccess } from "./dsm-window.js";
|
|
12
12
|
import { daemonConfig, readDaemon } from "./daemon.js";
|
|
13
|
+
import { ROOT_SCRIPT_HINTS } from "./root-ops.js";
|
|
14
|
+
import { DELETE_STATE_KEY } from "../build-spk/uninstall-wizard.js";
|
|
15
|
+
import { readUninstallRecord, stateItemTexts } from "./uninstall.js";
|
|
13
16
|
import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, backgroundUnitName, renderBackgroundUnit } from "./background.js";
|
|
14
17
|
import { renderStandaloneResource } from "./resource.js";
|
|
15
18
|
import { recoverSharedFolder, renderSharedFolderUpgradeWizardFiles, sharedFolderInstallSteps, SHARED_FOLDER_MARKER } from "./shared-folder.js";
|
|
@@ -23,7 +26,6 @@ import { PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH, renderPortalJson, renderP
|
|
|
23
26
|
import { isPortalAlias } from "../portal/alias.js";
|
|
24
27
|
import { defaultPackageIcons, MAX_ICON_BYTES } from "../build-spk/dsm-icons.js";
|
|
25
28
|
import { decodePng } from "../build-spk/png.js";
|
|
26
|
-
const ROOT_SCRIPT_HINTS = [/\bsudo\b/, /\bchown\s+(-\w+\s+)*root\b/, /chmod\s+[ugo]*\+s\b/, /chmod\s+[2467][0-7]{3}\b/];
|
|
27
29
|
/** Reads an SPK without installing it and reports how it would run on DSM. */
|
|
28
30
|
export async function inspectStandaloneSpk(spkPath) {
|
|
29
31
|
return inspectSpk(spkPath, "native-go");
|
|
@@ -55,7 +57,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
55
57
|
if (metadataText !== undefined) {
|
|
56
58
|
try {
|
|
57
59
|
const raw = JSON.parse(metadataText);
|
|
58
|
-
if (raw.schema !== 2 || !["go", "binary"].includes(raw.backend) || !Array.isArray(raw.capabilities) || raw.capabilities.some((name) => !["files", "jobs", "identity", "rootOps", "delegate"].includes(String(name))) || new Set(raw.capabilities).size !== raw.capabilities.length || (raw.entry !== undefined && !["admin-only", "app-privilege"].includes(raw.entry)))
|
|
60
|
+
if (raw.schema !== 2 || !["go", "rust", "binary"].includes(raw.backend) || !Array.isArray(raw.capabilities) || raw.capabilities.some((name) => !["files", "jobs", "identity", "rootOps", "delegate"].includes(String(name))) || new Set(raw.capabilities).size !== raw.capabilities.length || (raw.entry !== undefined && !["admin-only", "app-privilege"].includes(raw.entry)))
|
|
59
61
|
throw new Error();
|
|
60
62
|
metadata = raw;
|
|
61
63
|
}
|
|
@@ -63,17 +65,23 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
63
65
|
findings.push("sdk-package.json 不符合 schema 2 capability inventory。");
|
|
64
66
|
}
|
|
65
67
|
}
|
|
68
|
+
const uninstallHook = metadata.uninstall === undefined ? undefined : readUninstallRecord(metadata.uninstall, metadata.capabilities?.includes("rootOps") === true);
|
|
69
|
+
if (metadata.uninstall !== undefined && !uninstallHook)
|
|
70
|
+
findings.push("UNINSTALL_RECORD_INVALID sdk-package.json 的 uninstall 不是 synopkgland build 寫出的 uninstall 設定。");
|
|
66
71
|
const binaryBackend = metadata.backend === "binary";
|
|
67
|
-
|
|
68
|
-
const
|
|
72
|
+
// A strict standalone inspection names the language the CGI was built from.
|
|
73
|
+
const compiled = metadata.backend === "rust" ? "native-rust" : "native-go";
|
|
74
|
+
const kind = (expectedKind === "native-go" ? compiled : expectedKind) ?? (paths.has("compose.yaml") ? "container" : binaryBackend ? "binary" : compiled);
|
|
75
|
+
const compiledKind = kind === "native-go" || kind === "native-rust";
|
|
76
|
+
const standalone = compiledKind || kind === "binary";
|
|
69
77
|
const info = readInfo(join(dir, "INFO"), findings, kind);
|
|
70
78
|
await checkArch(dir, info, kind, paths.has(CGI_PAYLOAD_PATH), findings);
|
|
71
|
-
const background =
|
|
79
|
+
const background = compiledKind && paths.has(BACKGROUND_WORKER_PAYLOAD);
|
|
72
80
|
const sharedFolder = standalone ? recoverSharedFolder(readOptional(join(dir, "scripts/postinst"))) : undefined;
|
|
73
81
|
// A Compose App has no executor, so its SPK never carries a portal.
|
|
74
82
|
const portal = standalone ? await recoverPortal(dir, info, `SYNO.SDS.${info.package}.Instance`, paths, payload, findings) : undefined;
|
|
75
83
|
if (portal && info.os_min_ver && OS_MIN_VER.test(info.os_min_ver) && !isDsmAtLeast(info.os_min_ver, PORTAL_MIN_DSM)) {
|
|
76
|
-
findings.push(`INFO os_min_ver「${info.os_min_ver}」低於 portal 需要的 DSM ${PORTAL_MIN_DSM}
|
|
84
|
+
findings.push(`INFO os_min_ver「${info.os_min_ver}」低於 portal 需要的 DSM ${PORTAL_MIN_DSM};${PORTAL_MIN_DSM_REASON}`);
|
|
77
85
|
}
|
|
78
86
|
checkConf(dir, findings, kind, background, sharedFolder, info.package, portal);
|
|
79
87
|
checkScripts(dir, findings);
|
|
@@ -96,18 +104,23 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
96
104
|
executor = inspected.executor;
|
|
97
105
|
if (executor)
|
|
98
106
|
findings.push(...inspectExecutorAnchor(readFileSync(join(dir, "INFO"), "utf8"), executor.executorDigest));
|
|
107
|
+
if (executor?.policy.portal && info.os_min_ver && OS_MIN_VER.test(info.os_min_ver) && !isDsmAtLeast(info.os_min_ver, PORTAL_EXECUTOR_MIN_DSM)) {
|
|
108
|
+
findings.push(`INFO os_min_ver「${info.os_min_ver}」低於 executor 註冊 portal 需要的 DSM ${PORTAL_EXECUTOR_MIN_DSM}`);
|
|
109
|
+
}
|
|
110
|
+
if (executor && executor.policy.purge !== uninstallHook?.uninstall.purge.root)
|
|
111
|
+
findings.push(`UNINSTALL_ROOT_POLICY_MISMATCH executor policy 的 purge「${executor.policy.purge ?? ""}」與 uninstall.purge.root「${uninstallHook?.uninstall.purge.root ?? ""}」不同。`);
|
|
99
112
|
if (payload.find(p => p.path === "executor/executor")?.mode !== "0755")
|
|
100
113
|
findings.push("Executor must be executable.");
|
|
101
114
|
if (payload.find(p => p.path === "executor/lifecycle")?.mode !== "0755")
|
|
102
115
|
findings.push("Executor lifecycle helper must be executable.");
|
|
103
116
|
if (![...paths].some(path => path.startsWith("daemon/"))) {
|
|
104
|
-
const lifecycleManifest = { package: info.package, capabilities: { rootOps: true, ...(background ? { jobs: { worker: "sha256-v1" } } : {}) } };
|
|
117
|
+
const lifecycleManifest = { package: info.package, capabilities: { rootOps: true, ...(background ? { jobs: { worker: "sha256-v1" } } : {}) }, ...(uninstallHook ? { uninstall: uninstallHook.uninstall } : {}) };
|
|
105
118
|
for (const hook of ["preuninst", "preupgrade", "start-stop-status"]) {
|
|
106
119
|
if (readOptional(join(dir, "scripts", hook)) !== renderStandaloneScript(hook, lifecycleManifest))
|
|
107
120
|
findings.push(`Executor scripts/${hook} differs from fixed lifecycle.`);
|
|
108
121
|
}
|
|
109
122
|
}
|
|
110
|
-
for (const [name, text] of Object.entries(renderUninstallWizardFiles(info.package, "native", { executor: true, portal: Boolean(portal) }))) {
|
|
123
|
+
for (const [name, text] of Object.entries(renderUninstallWizardFiles(info.package, "native", { executor: true, portal: Boolean(portal), portalEntry: Boolean(executor?.policy.portal), ...uninstallWizardOptions(uninstallHook) }))) {
|
|
111
124
|
if (readOptional(join(dir, "WIZARD_UIFILES", name)) !== text)
|
|
112
125
|
findings.push(`Executor WIZARD_UIFILES/${name} is missing cleanup guidance.`);
|
|
113
126
|
}
|
|
@@ -132,8 +145,9 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
132
145
|
icon,
|
|
133
146
|
...(executor ? { executor } : {}),
|
|
134
147
|
...(portal ? { portal: { appId: portal.appId, alias: portal.alias } } : {}),
|
|
148
|
+
...(executor?.policy.portal ? { portal: { appId: executor.policy.app, alias: executor.policy.portal.alias } } : {}),
|
|
135
149
|
...(metadata.entry ? { entry: metadata.entry } : {}),
|
|
136
|
-
...(standalone ? { uninstallCleanup: uninstallCleanup(info.package, { executor: hasExecutor, portal: Boolean(portal) }) } : {}),
|
|
150
|
+
...(standalone ? { uninstallCleanup: uninstallCleanup(info.package, { executor: hasExecutor, portal: Boolean(portal), portalEntry: Boolean(executor?.policy.portal) }) } : {}),
|
|
137
151
|
package: info.package,
|
|
138
152
|
version: info.version,
|
|
139
153
|
arch: info.arch,
|
|
@@ -467,10 +481,10 @@ function checkScripts(dir, findings) {
|
|
|
467
481
|
const DATA_KEYS = ["pkgwizard_remove_additional", "delete_app_data"];
|
|
468
482
|
const DELETE_COMMAND = /\brm\s+-[a-zA-Z]*[rR]|\bfind\b[^\n]*(-delete|-exec\s+rm)/;
|
|
469
483
|
const UPGRADE_GUARD = /SYNOPKG_PKG_STATUS[^\n]*!=\s*"?UPGRADE"?/;
|
|
470
|
-
function uninstallBoxes(file) {
|
|
484
|
+
function uninstallBoxes(file, keys) {
|
|
471
485
|
try {
|
|
472
486
|
const steps = JSON.parse(readFileSync(file, "utf8"));
|
|
473
|
-
return steps.flatMap(step => step.items ?? []).flatMap(item => item.subitems ?? []).filter(box =>
|
|
487
|
+
return steps.flatMap(step => step.items ?? []).flatMap(item => item.subitems ?? []).filter(box => keys.includes(box.key));
|
|
474
488
|
}
|
|
475
489
|
catch {
|
|
476
490
|
return [];
|
|
@@ -482,6 +496,13 @@ function uninstallBoxes(file) {
|
|
|
482
496
|
* package's own `/volumeN/@app*` directories; the wizard and the script must
|
|
483
497
|
* agree on the box that asks for it.
|
|
484
498
|
*/
|
|
499
|
+
/** The wizard texts of the App's uninstall hook, as build rendered them. */
|
|
500
|
+
function uninstallWizardOptions(hook) {
|
|
501
|
+
if (!hook)
|
|
502
|
+
return {};
|
|
503
|
+
const { uninstall, catalogs } = hook;
|
|
504
|
+
return { rootPurge: Boolean(uninstall.purge.root), ...(uninstall.state ? { state: { items: (locale) => stateItemTexts(uninstall, catalogs, locale) } } : {}) };
|
|
505
|
+
}
|
|
485
506
|
function checkUninstall(dir, findings) {
|
|
486
507
|
const script = join(dir, "scripts", "postuninst");
|
|
487
508
|
if (!existsSync(script))
|
|
@@ -489,7 +510,8 @@ function checkUninstall(dir, findings) {
|
|
|
489
510
|
const body = readFileSync(script, "utf8");
|
|
490
511
|
const wizardDir = join(dir, "WIZARD_UIFILES");
|
|
491
512
|
const wizards = existsSync(wizardDir) ? readdirSync(wizardDir).filter(name => name === "uninstall_uifile" || name.startsWith("uninstall_uifile_")).sort() : [];
|
|
492
|
-
const boxes = wizards.flatMap(name => uninstallBoxes(join(wizardDir, name)).map(box => ({ ...box, name })));
|
|
513
|
+
const boxes = wizards.flatMap(name => uninstallBoxes(join(wizardDir, name), DATA_KEYS).map(box => ({ ...box, name })));
|
|
514
|
+
checkStateBoxes(dir, wizards.flatMap(name => uninstallBoxes(join(wizardDir, name), [DELETE_STATE_KEY]).map(box => ({ ...box, name }))), findings);
|
|
493
515
|
const deletes = DELETE_COMMAND.test(body);
|
|
494
516
|
if (deletes) {
|
|
495
517
|
if (!UPGRADE_GUARD.test(body))
|
|
@@ -510,6 +532,17 @@ function checkUninstall(dir, findings) {
|
|
|
510
532
|
findings.push(`UNINSTALL_WIZARD_NOT_HONOURED WIZARD_UIFILES/${box.name} 提供 ${box.key} 勾選框,但 scripts/postuninst 沒有讀它`);
|
|
511
533
|
}
|
|
512
534
|
}
|
|
535
|
+
/** The App's delete-state box keeps the state by default, and preuninst hands its answer to the App hook. */
|
|
536
|
+
function checkStateBoxes(dir, boxes, findings) {
|
|
537
|
+
const script = join(dir, "scripts", "preuninst");
|
|
538
|
+
const body = existsSync(script) ? readFileSync(script, "utf8") : "";
|
|
539
|
+
for (const box of boxes) {
|
|
540
|
+
if (box.defaultValue !== false)
|
|
541
|
+
findings.push(`UNINSTALL_STATE_DEFAULT_CHECKED WIZARD_UIFILES/${box.name} 的 ${box.key} 預設必須不勾(保留 App 狀態)`);
|
|
542
|
+
if (!body.includes(box.key))
|
|
543
|
+
findings.push(`UNINSTALL_STATE_NOT_HONOURED WIZARD_UIFILES/${box.name} 提供 ${box.key} 勾選框,但 scripts/preuninst 沒有讀它`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
513
546
|
export async function listPayload(packageTgz, findings) {
|
|
514
547
|
if (!existsSync(packageTgz)) {
|
|
515
548
|
findings.push("缺少 package.tgz");
|
|
@@ -4,6 +4,8 @@ import { renderSharedFolderScript } from "./shared-folder.js";
|
|
|
4
4
|
import { renderDelegateInstallDraft } from '../build-spk/delegate-scope.js';
|
|
5
5
|
import { renderPortalLifecycle } from "../portal/package.js";
|
|
6
6
|
import { nativePortal } from "./portal.js";
|
|
7
|
+
import { renderUninstallHook } from "./uninstall-hook.js";
|
|
8
|
+
const UNINSTALL_ONLY = '[ "${SYNOPKG_PKG_STATUS:-}" = UPGRADE ] || synopkgland_uninstall_hook\n';
|
|
7
9
|
/** Package-account lifecycle hooks for CGI, daemon and background packages. */
|
|
8
10
|
export function renderStandaloneScript(name, manifest) {
|
|
9
11
|
const portal = nativePortal(manifest);
|
|
@@ -43,12 +45,16 @@ function renderStandaloneScriptBody(name, manifest) {
|
|
|
43
45
|
const stopDaemon = manifest.daemon ? `"/var/packages/${manifest.package}/target/daemon/control" stop "\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}" "\${SYNOPKG_PKGVAR:-/var/packages/${manifest.package}/var}" || exit $?\n` : "";
|
|
44
46
|
return normal.replace("#!/bin/sh\n", `#!/bin/sh\n${workerStop}if [ "$1" = start ]; then\n${request("start")}fi\nif [ "$1" = stop ]; then\n${stopWorker}${stopDaemon}${request("stop")}${workerResult}fi\n`);
|
|
45
47
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return `#!/bin/sh\n${workerStop}${stopWorker}${
|
|
48
|
+
if (name === "preuninst")
|
|
49
|
+
return renderExecutorPreuninst(manifest, workerStop, stopWorker, workerResult, request);
|
|
50
|
+
return `#!/bin/sh\n${workerStop}${stopWorker}${request("stop")}${manifest.daemon ? renderDaemonScript(name, manifest).replace("#!/bin/sh\n", "") : workerResult}`;
|
|
49
51
|
}
|
|
50
52
|
if (name === "postuninst")
|
|
51
53
|
return renderPackagePostuninst(manifest.package);
|
|
54
|
+
if (name === "preuninst" && manifest.uninstall) {
|
|
55
|
+
const body = manifest.daemon ? renderDaemonPreuninst(manifest) : `${UNINSTALL_ONLY}exit 0\n`;
|
|
56
|
+
return `#!/bin/sh\n${renderUninstallHook(manifest)}${body}`;
|
|
57
|
+
}
|
|
52
58
|
if ((manifest.capabilities?.rootOps || manifest.capabilities?.delegate) && ["postinst", "postupgrade"].includes(name)) {
|
|
53
59
|
const normal = renderCapabilityScript(name, { ...manifest, capabilities: { ...manifest.capabilities, rootOps: undefined, delegate: undefined } });
|
|
54
60
|
return normal.replace("#!/bin/sh\n", `#!/bin/sh\n"\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}/executor/lifecycle" start --package ${manifest.package} || exit $?\n`);
|
|
@@ -84,7 +90,11 @@ function renderDelegatedDaemonScript(name, manifest) {
|
|
|
84
90
|
if (name !== 'start-stop-status') {
|
|
85
91
|
if (name === 'preupgrade')
|
|
86
92
|
return `#!/bin/sh\n${workerStop}${stop('stop', 'stop')}`;
|
|
87
|
-
|
|
93
|
+
const purge = manifest.uninstall
|
|
94
|
+
? stop(manifest.daemon?.uninstall_args ? 'uninstall' : 'stop', 'purge $synopkgland_purge_args')
|
|
95
|
+
.replace(executor('purge $synopkgland_purge_args'), () => `synopkgland_uninstall_hook\n${executor('purge $synopkgland_purge_args')}`)
|
|
96
|
+
: stop(manifest.daemon?.uninstall_args ? 'uninstall' : 'stop', 'purge');
|
|
97
|
+
return `#!/bin/sh\n${workerStop}${renderUninstallHook(manifest)}if [ "\${SYNOPKG_PKG_STATUS:-}" = UPGRADE ]; then\n${stop('stop', 'stop')}fi\n${purge}`;
|
|
88
98
|
}
|
|
89
99
|
return `#!/bin/sh
|
|
90
100
|
${workerStop}${renderUserManagerRecovery()}${renderBackgroundWorkerStart(pkg)}case "$1" in
|
|
@@ -143,3 +153,27 @@ case "$1" in
|
|
|
143
153
|
esac
|
|
144
154
|
`;
|
|
145
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* A real uninstall stops the daemon, runs the App hook, then asks the
|
|
158
|
+
* executor to purge; an upgrade only stops both.
|
|
159
|
+
*/
|
|
160
|
+
function renderExecutorPreuninst(manifest, workerStop, stopWorker, workerResult, request) {
|
|
161
|
+
const pkg = manifest.package;
|
|
162
|
+
const choose = 'action=purge\n[ "${SYNOPKG_PKG_STATUS:-}" != UPGRADE ] || action=stop\n';
|
|
163
|
+
const hook = manifest.uninstall ? '[ "$action" = stop ] || synopkgland_uninstall_hook\n' : "";
|
|
164
|
+
const purge = request(manifest.uninstall ? '"$action" $synopkgland_purge_args' : '"$action"');
|
|
165
|
+
if (!manifest.daemon)
|
|
166
|
+
return `#!/bin/sh\n${workerStop}${renderUninstallHook(manifest)}${stopWorker}${choose}${hook}${purge}${workerResult}`;
|
|
167
|
+
const control = `"/var/packages/${pkg}/target/daemon/control"`;
|
|
168
|
+
const paths = `"\${SYNOPKG_PKGDEST:-/var/packages/${pkg}/target}" "\${SYNOPKG_PKGVAR:-/var/packages/${pkg}/var}"`;
|
|
169
|
+
const daemonAction = manifest.daemon.uninstall_args ? 'daemon_action=stop\n[ "$action" = stop ] || daemon_action=uninstall\n' : "daemon_action=stop\n";
|
|
170
|
+
return `#!/bin/sh\n${workerStop}${renderUninstallHook(manifest)}${stopWorker}${choose}${daemonAction}daemon_result=0\n${control} "$daemon_action" ${paths} || daemon_result=$?\n${hook}${purge}exit "$daemon_result"\n`;
|
|
171
|
+
}
|
|
172
|
+
/** The daemon stops (or uninstalls) first; the App hook follows on a real uninstall. */
|
|
173
|
+
function renderDaemonPreuninst(manifest) {
|
|
174
|
+
const pkg = manifest.package;
|
|
175
|
+
const control = `"/var/packages/${pkg}/target/daemon/control"`;
|
|
176
|
+
const paths = `"\${SYNOPKG_PKGDEST:-/var/packages/${pkg}/target}" "\${SYNOPKG_PKGVAR:-/var/packages/${pkg}/var}"`;
|
|
177
|
+
return `if [ "\${SYNOPKG_PKG_STATUS:-}" = UPGRADE ]; then\n exec ${control} stop ${paths}\nfi\n`
|
|
178
|
+
+ `${control} ${manifest.daemon?.uninstall_args ? "uninstall" : "stop"} ${paths}\ndaemon_result=$?\nsynopkgland_uninstall_hook\nexit "$daemon_result"\n`;
|
|
179
|
+
}
|
|
@@ -6,10 +6,12 @@ import { type Catalogs, type InfoTranslations } from "./i18n.js";
|
|
|
6
6
|
export type { InfoText } from "./i18n.js";
|
|
7
7
|
import { type RootOp } from "./root-ops.js";
|
|
8
8
|
import { type Daemon } from "./daemon.js";
|
|
9
|
+
import { type Uninstall } from "./uninstall.js";
|
|
9
10
|
import { type Background } from "./background.js";
|
|
10
11
|
import { type SharedFolder } from "./shared-folder.js";
|
|
11
12
|
import { type ApiPermissions } from "./api-permissions.js";
|
|
12
13
|
import { type PortalDeclaration } from "../portal/alias.js";
|
|
14
|
+
import { type SdkBackend } from "../runtime/sdk-source.js";
|
|
13
15
|
/**
|
|
14
16
|
* `dsm-package.yaml`: a Standalone Package, i.e. an unsigned native SPK that
|
|
15
17
|
* runs as its own DSM package account with a CGI backend and a DSM window UI.
|
|
@@ -19,6 +21,12 @@ export type StandaloneBackend = {
|
|
|
19
21
|
kind: "go";
|
|
20
22
|
dir: string;
|
|
21
23
|
main: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "rust";
|
|
26
|
+
dir: string;
|
|
27
|
+
package: string;
|
|
28
|
+
bin: string;
|
|
29
|
+
worker: string;
|
|
22
30
|
} | {
|
|
23
31
|
kind: "binary";
|
|
24
32
|
files: Partial<Record<NativeArchFamily, string>>;
|
|
@@ -63,6 +71,8 @@ export interface StandaloneManifest {
|
|
|
63
71
|
tasks: ExecutorTask[];
|
|
64
72
|
};
|
|
65
73
|
daemon?: Daemon;
|
|
74
|
+
/** `uninstall`: the App's own purge hook and the state the wizard may delete. */
|
|
75
|
+
uninstall?: Uninstall;
|
|
66
76
|
/** `install.sharedFolder`: the Shared Folder the install wizard selects or `data-share` creates. */
|
|
67
77
|
sharedFolder?: SharedFolder;
|
|
68
78
|
/** Who may use the app at all; absent keeps the route policy alone. */
|
|
@@ -97,6 +107,8 @@ export type ManifestResult = {
|
|
|
97
107
|
};
|
|
98
108
|
export declare const KNOWN_FIELDS: Set<string>;
|
|
99
109
|
export declare function loadStandaloneManifest(root: string): ManifestResult;
|
|
110
|
+
/** Where an App's SDK version is declared: its Go or Rust backend; a prebuilt binary declares it in the project's go.mod. */
|
|
111
|
+
export declare function sdkBackend(m: Pick<StandaloneManifest, "backend" | "root">): SdkBackend;
|
|
100
112
|
/**
|
|
101
113
|
* The nearest directory at or above `start` that holds dsm-package.yaml,
|
|
102
114
|
* unless a Managed Profile project (synology-app.yaml) is found first.
|