@synopackageland/cli 0.25.0 → 0.25.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/error-registry.js +17 -2
- package/dist/standalone/daemon-ingress.d.ts +21 -0
- package/dist/standalone/daemon-ingress.js +61 -0
- package/dist/standalone/daemon.d.ts +26 -0
- package/dist/standalone/daemon.js +87 -4
- package/dist/standalone/inspect-spk.js +61 -9
- package/dist/standalone/lifecycle.js +18 -4
- package/dist/standalone/manifest.js +8 -0
- package/dist/standalone/resource.d.ts +4 -0
- package/dist/standalone/resource.js +5 -1
- package/dist/standalone/spk.js +12 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.25.2 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
- Use templates 0.25.1 with the native Rust lockfile pinned to the public SDK 0.25.0 commit. SDK/App support and executor/lifecycle digests are unchanged.
|
|
6
|
+
- Update SDK download and Go build diagnostics for anonymous access to the public repository.
|
|
7
|
+
|
|
8
|
+
## 0.25.1 (2026-10-22)
|
|
9
|
+
|
|
10
|
+
- Fix upgrades that first add a `daemon`: the generated `preupgrade` ran the old payload's missing `daemon/control` and DSM rolled back with error 4541. The hook now exits 0 when the outgoing payload has no `daemon/` (plain, executor and delegated-worker variants); inspect still accepts the released unguarded hook.
|
|
11
|
+
- Add `daemon.node` (`package: Node.js_v18|v20|v22`, optional `min_version`): the daemon's `exec` is a JavaScript entry run by the Package Center Node.js package through a generated launcher, with INFO `install_dep_packages`. The SPK bundles no Node. See docs/daemon.md for availability and dependency behaviour.
|
|
12
|
+
- Add `daemon.ingress` (`path`, optional `timeout`, default 3600 s): a same-origin path in DSM's nginx through web-config `nginx-static-config` `type: dsm`, proxied unbuffered to `/tmp/synopkgland-ingress-<package>.sock`, with nginx reloads in INFO. check requires os_min_ver 7.2-64570 or later (`STANDALONE_DAEMON_INGRESS_MIN_DSM`); inspect verifies the location, resource, INFO and daemon environment.
|
|
13
|
+
|
|
3
14
|
## 0.25.0 (2026-10-22)
|
|
4
15
|
|
|
5
16
|
- Add the opt-in `console` declaration for native Apps with `identity_gateway`: a native UI URL launcher and an administrator-only DSM console, selected by Package Center Open. Validate both entries and refuse incompatible launch declarations.
|
package/dist/error-registry.js
CHANGED
|
@@ -322,6 +322,21 @@ export const ERROR_REGISTRY = {
|
|
|
322
322
|
description: "The package daemon's Go supervisor could not be built.",
|
|
323
323
|
suggestion: "Install Go 1.21 or later (builds pin go1.26.10 and need it offline) and check the compiler stderr; the NAS does not need a Go toolchain.",
|
|
324
324
|
},
|
|
325
|
+
STANDALONE_DAEMON_NODE_INVALID: {
|
|
326
|
+
code: "STANDALONE_DAEMON_NODE_INVALID",
|
|
327
|
+
description: "daemon.node does not name a Package Center Node.js package and a version of its major.",
|
|
328
|
+
suggestion: "Set daemon.node.package to Node.js_v18, Node.js_v20 or Node.js_v22, and min_version (optional) to a version of that major such as 22.19.0.",
|
|
329
|
+
},
|
|
330
|
+
STANDALONE_DAEMON_INGRESS_INVALID: {
|
|
331
|
+
code: "STANDALONE_DAEMON_INGRESS_INVALID",
|
|
332
|
+
description: "daemon.ingress does not declare one same-origin path and a bounded proxy timeout.",
|
|
333
|
+
suggestion: "Set daemon.ingress.path to /<name>/ with lowercase letters, digits and -, not a DSM path, and timeout (optional) to 1–3600 seconds.",
|
|
334
|
+
},
|
|
335
|
+
STANDALONE_DAEMON_INGRESS_MIN_DSM: {
|
|
336
|
+
code: "STANDALONE_DAEMON_INGRESS_MIN_DSM",
|
|
337
|
+
description: "daemon.ingress uses DSM's web-config worker, which DSM 7.1 refuses in an unsigned package.",
|
|
338
|
+
suggestion: "Raise os_min_ver to 7.2-64570 or later, or remove daemon.ingress.",
|
|
339
|
+
},
|
|
325
340
|
STANDALONE_DAEMON_PAYLOAD_INVALID: {
|
|
326
341
|
code: "STANDALONE_DAEMON_PAYLOAD_INVALID",
|
|
327
342
|
description: "The daemon payload is not a bounded, link-free directory inside the project.",
|
|
@@ -525,7 +540,7 @@ export const ERROR_REGISTRY = {
|
|
|
525
540
|
STANDALONE_GO_BUILD_FAILED: {
|
|
526
541
|
code: "STANDALONE_GO_BUILD_FAILED",
|
|
527
542
|
description: "go build failed.",
|
|
528
|
-
suggestion: "Run go build ./... in the backend directory and fix the errors. If github.com/synopackageland/syno-pkg-land-sdk is unreachable,
|
|
543
|
+
suggestion: "Run go build ./... in the backend directory and fix the errors. If github.com/synopackageland/syno-pkg-land-sdk is unreachable, check network access to the configured Go proxy or the public GitHub repository.",
|
|
529
544
|
},
|
|
530
545
|
STANDALONE_PORTAL_INVALID: {
|
|
531
546
|
code: "STANDALONE_PORTAL_INVALID",
|
|
@@ -890,7 +905,7 @@ export const ERROR_REGISTRY = {
|
|
|
890
905
|
SDK_DOWNLOAD_FAILED: {
|
|
891
906
|
code: "SDK_DOWNLOAD_FAILED",
|
|
892
907
|
description: "go mod download cannot fetch the SDK version backend/go.mod requires.",
|
|
893
|
-
suggestion: "
|
|
908
|
+
suggestion: "Confirm the SDK version exists in the public repository and check network access to the configured Go proxy or github.com. Offline, run go mod download for that version on the same machine first.",
|
|
894
909
|
},
|
|
895
910
|
TEMPLATES_MISSING: {
|
|
896
911
|
code: "TEMPLATES_MISSING",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { StandaloneError } from "./manifest.js";
|
|
2
|
+
/** `daemon.ingress`: one path in DSM's own server blocks, proxied to the daemon. */
|
|
3
|
+
export interface DaemonIngress {
|
|
4
|
+
/** `/<name>/`, served on DSM's ports and custom domains, so same-origin with DSM. */
|
|
5
|
+
path: string;
|
|
6
|
+
/** nginx proxy_read_timeout and proxy_send_timeout in seconds. */
|
|
7
|
+
timeout: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const DAEMON_INGRESS_CONFIG = "daemon/ingress-nginx.conf";
|
|
10
|
+
export declare function readDaemonIngress(value: unknown, fail: (error: StandaloneError) => void): DaemonIngress | undefined;
|
|
11
|
+
/** DSM before 7.2 rejects an unsigned web-config, the worker this path uses. */
|
|
12
|
+
export declare function daemonIngressMinDsmProblem(osMinVer: string | undefined): string | undefined;
|
|
13
|
+
export declare const daemonIngressSocket: (pkg: string) => string;
|
|
14
|
+
/** What the daemon learns: where to listen and which path prefix it serves. */
|
|
15
|
+
export declare function ingressEnv(pkg: string, ingress: DaemonIngress): Record<string, string>;
|
|
16
|
+
export declare function daemonIngressResource(): Record<string, unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Included in DSM's own server blocks. Streaming responses (SSE) pass
|
|
19
|
+
* unbuffered; nginx replaces any client X-Real-IP with the peer address.
|
|
20
|
+
*/
|
|
21
|
+
export declare function renderDaemonIngressConfig(pkg: string, ingress: DaemonIngress): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { isDsmAtLeast, PORTAL_MIN_DSM } from "../build-spk/dsm-version.js";
|
|
2
|
+
import { RESERVED_PORTAL_ALIASES } from "../portal/alias.js";
|
|
3
|
+
export const DAEMON_INGRESS_CONFIG = "daemon/ingress-nginx.conf";
|
|
4
|
+
const PATH = /^\/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?\/$/;
|
|
5
|
+
const reserved = (path) => RESERVED_PORTAL_ALIASES.includes(path.slice(1, -1));
|
|
6
|
+
export function readDaemonIngress(value, fail) {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
const v = value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
10
|
+
const timeout = v?.timeout ?? 3600;
|
|
11
|
+
if (!v ||
|
|
12
|
+
Object.keys(v).some((k) => k !== "path" && k !== "timeout") ||
|
|
13
|
+
typeof v.path !== "string" ||
|
|
14
|
+
!PATH.test(v.path) ||
|
|
15
|
+
reserved(v.path) ||
|
|
16
|
+
!Number.isInteger(timeout) ||
|
|
17
|
+
timeout < 1 ||
|
|
18
|
+
timeout > 3600) {
|
|
19
|
+
fail({
|
|
20
|
+
code: "STANDALONE_DAEMON_INGRESS_INVALID",
|
|
21
|
+
message: `daemon.ingress needs path "/<name>/" (lowercase letters, digits and -, not a DSM path such as /webapi/) and an optional timeout of 1–3600 seconds.`,
|
|
22
|
+
});
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
return { path: v.path, timeout: timeout };
|
|
26
|
+
}
|
|
27
|
+
/** DSM before 7.2 rejects an unsigned web-config, the worker this path uses. */
|
|
28
|
+
export function daemonIngressMinDsmProblem(osMinVer) {
|
|
29
|
+
if (osMinVer && isDsmAtLeast(osMinVer, PORTAL_MIN_DSM))
|
|
30
|
+
return undefined;
|
|
31
|
+
return `daemon.ingress needs os_min_ver ${PORTAL_MIN_DSM} or later; os_min_ver is "${osMinVer ?? ""}". DSM 7.1 rejects the unsigned web-config worker that adds the path to DSM's nginx. Raise os_min_ver or remove daemon.ingress.`;
|
|
32
|
+
}
|
|
33
|
+
export const daemonIngressSocket = (pkg) => `/tmp/synopkgland-ingress-${pkg}.sock`;
|
|
34
|
+
/** What the daemon learns: where to listen and which path prefix it serves. */
|
|
35
|
+
export function ingressEnv(pkg, ingress) {
|
|
36
|
+
return { SYNOPKGLAND_INGRESS_PATH: ingress.path, SYNOPKGLAND_INGRESS_SOCKET: daemonIngressSocket(pkg) };
|
|
37
|
+
}
|
|
38
|
+
export function daemonIngressResource() {
|
|
39
|
+
return { "nginx-static-config": { enable: [{ type: "dsm", relpath: DAEMON_INGRESS_CONFIG }] } };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Included in DSM's own server blocks. Streaming responses (SSE) pass
|
|
43
|
+
* unbuffered; nginx replaces any client X-Real-IP with the peer address.
|
|
44
|
+
*/
|
|
45
|
+
export function renderDaemonIngressConfig(pkg, ingress) {
|
|
46
|
+
return `location ^~ ${ingress.path} {
|
|
47
|
+
proxy_pass http://unix:${daemonIngressSocket(pkg)}:;
|
|
48
|
+
proxy_http_version 1.1;
|
|
49
|
+
proxy_set_header Host $http_host;
|
|
50
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
51
|
+
proxy_set_header Connection "";
|
|
52
|
+
proxy_buffering off;
|
|
53
|
+
proxy_request_buffering off;
|
|
54
|
+
proxy_cache off;
|
|
55
|
+
gzip off;
|
|
56
|
+
proxy_read_timeout ${ingress.timeout}s;
|
|
57
|
+
proxy_send_timeout ${ingress.timeout}s;
|
|
58
|
+
client_max_body_size 8m;
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
2
|
+
import { NODE_PACKAGES } from "./bridge-backend.js";
|
|
3
|
+
import { type DaemonIngress } from "./daemon-ingress.js";
|
|
2
4
|
import type { CommandRunner } from "./build.js";
|
|
3
5
|
import type { StandaloneError, StandaloneManifest } from "./manifest.js";
|
|
4
6
|
export interface Daemon {
|
|
@@ -17,7 +19,26 @@ export interface Daemon {
|
|
|
17
19
|
status: number;
|
|
18
20
|
uninstall: number;
|
|
19
21
|
};
|
|
22
|
+
/** `exec` is a JavaScript entry run by the node of a Package Center Node.js package. */
|
|
23
|
+
node?: DaemonNode;
|
|
24
|
+
/** A same-origin DSM path that nginx proxies to the daemon's Unix socket. */
|
|
25
|
+
ingress?: DaemonIngress;
|
|
20
26
|
}
|
|
27
|
+
export interface DaemonNode {
|
|
28
|
+
package: (typeof NODE_PACKAGES)[number];
|
|
29
|
+
/** The lowest package version INFO install_dep_packages accepts. */
|
|
30
|
+
min_version?: string;
|
|
31
|
+
}
|
|
32
|
+
/** The CLI-generated launcher; the payload must not ship its own directory. */
|
|
33
|
+
export declare const NODE_LAUNCHER = "_synopkgland/node";
|
|
34
|
+
export declare const nodeBinary: (pkg: string) => string;
|
|
35
|
+
/** INFO install_dep_packages for a Node daemon. */
|
|
36
|
+
export declare function nodeDependency(node: DaemonNode): string;
|
|
37
|
+
/**
|
|
38
|
+
* Runs the dependency's node in place of the shell, so daemon/control
|
|
39
|
+
* supervises node itself. A removed dependency fails start with a clear line.
|
|
40
|
+
*/
|
|
41
|
+
export declare function renderNodeLauncher(node: DaemonNode): string;
|
|
21
42
|
/** A fixed argv or env value: only the package's own directories expand. */
|
|
22
43
|
export declare const token: (v: unknown) => v is string;
|
|
23
44
|
/** An application variable that cannot steer the loader, the shell or DSM. */
|
|
@@ -26,6 +47,11 @@ export declare const envKey: (k: string) => boolean;
|
|
|
26
47
|
export declare const relativeExec: (v: unknown) => v is string;
|
|
27
48
|
export declare function readDaemon(value: unknown, root: string, arches: NativeArchFamily[], fail: (error: StandaloneError) => void): Daemon | undefined;
|
|
28
49
|
export declare function daemonConfig(manifest: StandaloneManifest): string;
|
|
50
|
+
/**
|
|
51
|
+
* The argv and environment daemon/control runs: a Node daemon execs the
|
|
52
|
+
* launcher with its entry first, and ingress names the socket to listen on.
|
|
53
|
+
*/
|
|
54
|
+
export declare function runnableDaemon(pkg: string, daemon: Daemon): Daemon;
|
|
29
55
|
export declare function buildDaemonControl(manifest: StandaloneManifest, arch: NativeArchFamily, work: string, run: CommandRunner): Promise<string | undefined>;
|
|
30
56
|
export declare function copyDaemonPayload(manifest: StandaloneManifest, arch: NativeArchFamily, target: string, controller: string): void;
|
|
31
57
|
/** Copies a bounded, link-free package-account payload tree. */
|
|
@@ -1,14 +1,37 @@
|
|
|
1
1
|
import { operationBudgetSeconds, runtimeBudgetSdkSupported } from "../runtime/runtime-budgets.js";
|
|
2
2
|
import { goChildEnv } from "../runtime/go-env.js";
|
|
3
3
|
import { chmodSync, existsSync, copyFileSync, lstatSync, mkdirSync, readdirSync, realpathSync, writeFileSync, } from "node:fs";
|
|
4
|
-
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
5
|
import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem, } from "../build-spk/go-toolchain.js";
|
|
6
6
|
import { sdkSource, appSdkVersion } from "../runtime/sdk-source.js";
|
|
7
7
|
import { templateSdkVersion } from "../template-paths.js";
|
|
8
8
|
import { checkCgiBinary } from "./binary.js";
|
|
9
|
+
import { NODE_PACKAGES } from "./bridge-backend.js";
|
|
10
|
+
import { readDaemonIngress, ingressEnv } from "./daemon-ingress.js";
|
|
9
11
|
import { composeDaemon, composeNetwork } from "./compose.js";
|
|
10
12
|
import { gatewayDaemon } from "./identity-gateway.js";
|
|
11
13
|
import { StandaloneBuildError } from "./spk.js";
|
|
14
|
+
/** The CLI-generated launcher; the payload must not ship its own directory. */
|
|
15
|
+
export const NODE_LAUNCHER = "_synopkgland/node";
|
|
16
|
+
export const nodeBinary = (pkg) => `/var/packages/${pkg}/target/usr/local/bin/node`;
|
|
17
|
+
/** INFO install_dep_packages for a Node daemon. */
|
|
18
|
+
export function nodeDependency(node) {
|
|
19
|
+
return node.min_version ? `${node.package}>=${node.min_version}` : node.package;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Runs the dependency's node in place of the shell, so daemon/control
|
|
23
|
+
* supervises node itself. A removed dependency fails start with a clear line.
|
|
24
|
+
*/
|
|
25
|
+
export function renderNodeLauncher(node) {
|
|
26
|
+
return `#!/bin/sh
|
|
27
|
+
node=${nodeBinary(node.package)}
|
|
28
|
+
if [ ! -x "$node" ]; then
|
|
29
|
+
echo "${node.package} is not installed or is incomplete; install it in Package Center." >&2
|
|
30
|
+
exit 127
|
|
31
|
+
fi
|
|
32
|
+
exec "$node" "$@"
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
12
35
|
const fields = new Set([
|
|
13
36
|
"exec",
|
|
14
37
|
"targets",
|
|
@@ -16,6 +39,8 @@ const fields = new Set([
|
|
|
16
39
|
"env",
|
|
17
40
|
"timeouts",
|
|
18
41
|
"uninstall_args",
|
|
42
|
+
"node",
|
|
43
|
+
"ingress",
|
|
19
44
|
]);
|
|
20
45
|
const record = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
21
46
|
/** A fixed argv or env value: only the package's own directories expand. */
|
|
@@ -34,6 +59,8 @@ export const relativeExec = (v) => typeof v === "string" &&
|
|
|
34
59
|
v.length <= 200 &&
|
|
35
60
|
/^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_.-]+)*$/.test(v) &&
|
|
36
61
|
!v.split("/").some((p) => p === "." || p === "..");
|
|
62
|
+
/** A Node entry such as `server.mjs` or `dist/main.js`, inside the payload. */
|
|
63
|
+
const relativeEntry = (v) => typeof v === "string" && relativeExec(`_/${v}`);
|
|
37
64
|
export function readDaemon(value, root, arches, fail) {
|
|
38
65
|
if (value === undefined)
|
|
39
66
|
return undefined;
|
|
@@ -46,7 +73,7 @@ export function readDaemon(value, root, arches, fail) {
|
|
|
46
73
|
};
|
|
47
74
|
if (!record(value) || Object.keys(value).some((k) => !fields.has(k)))
|
|
48
75
|
return invalid();
|
|
49
|
-
if (!relativeExec(value.exec))
|
|
76
|
+
if (!(value.node === undefined ? relativeExec(value.exec) : relativeEntry(value.exec)))
|
|
50
77
|
return invalid();
|
|
51
78
|
if (!record(value.targets) ||
|
|
52
79
|
Object.keys(value.targets).length !== arches.length)
|
|
@@ -83,6 +110,14 @@ export function readDaemon(value, root, arches, fail) {
|
|
|
83
110
|
const timeouts = readLifecycleTimeouts(value.timeouts, fail);
|
|
84
111
|
if (!timeouts)
|
|
85
112
|
return undefined;
|
|
113
|
+
const node = readDaemonNode(value.node, fail);
|
|
114
|
+
if (value.node !== undefined && !node)
|
|
115
|
+
return undefined;
|
|
116
|
+
const ingress = readDaemonIngress(value.ingress, fail);
|
|
117
|
+
if (value.ingress !== undefined && !ingress)
|
|
118
|
+
return undefined;
|
|
119
|
+
if (ingress && Object.keys(env).some((key) => key in ingressEnv("", ingress)))
|
|
120
|
+
return invalid();
|
|
86
121
|
return {
|
|
87
122
|
exec: value.exec,
|
|
88
123
|
targets,
|
|
@@ -92,8 +127,31 @@ export function readDaemon(value, root, arches, fail) {
|
|
|
92
127
|
.sort()
|
|
93
128
|
.map((key) => [key, env[key]])),
|
|
94
129
|
timeouts: timeouts,
|
|
130
|
+
...(node ? { node } : {}),
|
|
131
|
+
...(ingress ? { ingress } : {}),
|
|
95
132
|
};
|
|
96
133
|
}
|
|
134
|
+
function readDaemonNode(value, fail) {
|
|
135
|
+
if (value === undefined)
|
|
136
|
+
return undefined;
|
|
137
|
+
const pkg = record(value) ? value.package : undefined;
|
|
138
|
+
const min = record(value) ? value.min_version : undefined;
|
|
139
|
+
const major = typeof pkg === "string" ? pkg.replace("Node.js_v", "") : "";
|
|
140
|
+
if (!record(value) ||
|
|
141
|
+
Object.keys(value).some((k) => k !== "package" && k !== "min_version") ||
|
|
142
|
+
!NODE_PACKAGES.includes(pkg) ||
|
|
143
|
+
(min !== undefined &&
|
|
144
|
+
(typeof min !== "string" ||
|
|
145
|
+
!/^[0-9]{1,3}(\.[0-9]{1,4}){0,2}$/.test(min) ||
|
|
146
|
+
min.split(".")[0] !== major))) {
|
|
147
|
+
fail({
|
|
148
|
+
code: "STANDALONE_DAEMON_NODE_INVALID",
|
|
149
|
+
message: `daemon.node needs package (${NODE_PACKAGES.join(", ")}) and an optional min_version of that major, such as "22.19.0".`,
|
|
150
|
+
});
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
return { package: pkg, ...(min ? { min_version: min } : {}) };
|
|
154
|
+
}
|
|
97
155
|
function daemonSdkVersion(manifest) {
|
|
98
156
|
return manifest.identityGateway?.sdk ??
|
|
99
157
|
(manifest.backend.kind === "go" ? appSdkVersion(manifest.backend) : undefined) ??
|
|
@@ -104,7 +162,7 @@ export function daemonConfig(manifest) {
|
|
|
104
162
|
? composeDaemon(manifest)
|
|
105
163
|
: manifest.identityGateway
|
|
106
164
|
? gatewayDaemon(manifest.daemon)
|
|
107
|
-
: manifest.daemon;
|
|
165
|
+
: runnableDaemon(manifest.package, manifest.daemon);
|
|
108
166
|
return (JSON.stringify({
|
|
109
167
|
package: manifest.package,
|
|
110
168
|
exec,
|
|
@@ -117,6 +175,20 @@ export function daemonConfig(manifest) {
|
|
|
117
175
|
...(manifest.compose ? { network: composeNetwork(manifest) } : {}),
|
|
118
176
|
}) + "\n");
|
|
119
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* The argv and environment daemon/control runs: a Node daemon execs the
|
|
180
|
+
* launcher with its entry first, and ingress names the socket to listen on.
|
|
181
|
+
*/
|
|
182
|
+
export function runnableDaemon(pkg, daemon) {
|
|
183
|
+
const entry = daemon.node && [`\${SYNOPKG_PKGDEST}/daemon/payload/${daemon.exec}`];
|
|
184
|
+
const env = daemon.ingress ? { ...daemon.env, ...ingressEnv(pkg, daemon.ingress) } : daemon.env;
|
|
185
|
+
return {
|
|
186
|
+
...daemon,
|
|
187
|
+
...(entry ? { exec: NODE_LAUNCHER, args: [...entry, ...daemon.args] } : {}),
|
|
188
|
+
...(entry && daemon.uninstall_args ? { uninstall_args: [...entry, ...daemon.uninstall_args] } : {}),
|
|
189
|
+
env: Object.fromEntries(Object.keys(env).sort().map((key) => [key, env[key]])),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
120
192
|
export async function buildDaemonControl(manifest, arch, work, run) {
|
|
121
193
|
if (!manifest.daemon && !(manifest.compose && manifest.identityGateway))
|
|
122
194
|
return undefined;
|
|
@@ -151,7 +223,18 @@ export function copyDaemonPayload(manifest, arch, target, controller) {
|
|
|
151
223
|
copyPayloadTree(manifest.root, source, join(target, "payload"));
|
|
152
224
|
else
|
|
153
225
|
mkdirSync(join(target, "payload"), { recursive: true });
|
|
154
|
-
if (!manifest.compose) {
|
|
226
|
+
if (!manifest.compose && manifest.daemon?.node) {
|
|
227
|
+
const exec = join(source, daemon.exec);
|
|
228
|
+
if (!existsSync(exec) || !lstatSync(exec).isFile())
|
|
229
|
+
throw new StandaloneBuildError("STANDALONE_DAEMON_PAYLOAD_INVALID", `daemon.exec must name the Node entry file inside the payload: ${exec}`);
|
|
230
|
+
if (existsSync(join(source, NODE_LAUNCHER.split("/")[0])))
|
|
231
|
+
throw new StandaloneBuildError("STANDALONE_DAEMON_PAYLOAD_INVALID", `The daemon payload must not contain ${NODE_LAUNCHER.split("/")[0]}/; the CLI writes its Node launcher there.`);
|
|
232
|
+
mkdirSync(join(target, "payload", dirname(NODE_LAUNCHER)), { recursive: true });
|
|
233
|
+
chmodSync(join(target, "payload", dirname(NODE_LAUNCHER)), 0o755);
|
|
234
|
+
writeFileSync(join(target, "payload", NODE_LAUNCHER), renderNodeLauncher(manifest.daemon.node));
|
|
235
|
+
chmodSync(join(target, "payload", NODE_LAUNCHER), 0o755);
|
|
236
|
+
}
|
|
237
|
+
else if (!manifest.compose) {
|
|
155
238
|
const exec = join(source, daemon.exec);
|
|
156
239
|
const problem = checkCgiBinary(manifest.root, exec, arch);
|
|
157
240
|
if (problem)
|
|
@@ -21,7 +21,8 @@ import { familyOfArchToken, NATIVE_ARCH_FAMILIES, } from "../build-spk/dsm-arch.
|
|
|
21
21
|
import { CONTAINER_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, PORTAL_MIN_DSM, PORTAL_MIN_DSM_REASON, } from "../build-spk/dsm-version.js";
|
|
22
22
|
import { ELF_MACHINE } from "./binary.js";
|
|
23
23
|
import { CGI_PAYLOAD_PATH, entryLauncherAccess } from "./dsm-window.js";
|
|
24
|
-
import { daemonConfig, readDaemon, readLifecycleTimeouts } from "./daemon.js";
|
|
24
|
+
import { daemonConfig, readDaemon, readLifecycleTimeouts, renderNodeLauncher, nodeDependency, NODE_LAUNCHER } from "./daemon.js";
|
|
25
|
+
import { DAEMON_INGRESS_CONFIG, daemonIngressMinDsmProblem, readDaemonIngress, renderDaemonIngressConfig } from "./daemon-ingress.js";
|
|
25
26
|
import { ROOT_SCRIPT_HINTS } from "./root-ops.js";
|
|
26
27
|
import { DELETE_STATE_KEY } from "../build-spk/uninstall-wizard.js";
|
|
27
28
|
import { readUninstallRecord, stateItemTexts } from "./uninstall.js";
|
|
@@ -155,12 +156,14 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
155
156
|
const combined = metadata.compose === undefined
|
|
156
157
|
? undefined
|
|
157
158
|
: await recoverCompose(dir, metadata.compose, findings);
|
|
158
|
-
const
|
|
159
|
+
const daemonNode = recoverDaemonNode(metadata.daemon_node, findings);
|
|
160
|
+
const info = readInfo(join(dir, "INFO"), findings, kind, Boolean(combined), daemonNode);
|
|
159
161
|
if (combined &&
|
|
160
162
|
(!info.os_min_ver || !isDsmAtLeast(info.os_min_ver, CONTAINER_MIN_DSM)))
|
|
161
163
|
findings.push("compose identity_gateway requires DSM >= 7.2.1");
|
|
162
164
|
await checkArch(dir, info, kind, paths.has(CGI_PAYLOAD_PATH), findings);
|
|
163
165
|
const nativeGateway = await recoverNativeGateway(dir, metadata.native_gateway, info, findings);
|
|
166
|
+
const daemonIngress = await recoverDaemonIngress(dir, metadata.daemon_ingress, info, paths, findings);
|
|
164
167
|
const background = compiledKind && paths.has(BACKGROUND_WORKER_PAYLOAD);
|
|
165
168
|
// Before any fixed-lifecycle comparison: those see the SDK steps alone.
|
|
166
169
|
const appScripts = standalone || kind === "container"
|
|
@@ -179,7 +182,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
179
182
|
!isDsmAtLeast(info.os_min_ver, PORTAL_MIN_DSM)) {
|
|
180
183
|
findings.push(`INFO os_min_ver "${info.os_min_ver}" is below the DSM ${PORTAL_MIN_DSM} a portal needs; ${PORTAL_MIN_DSM_REASON}`);
|
|
181
184
|
}
|
|
182
|
-
checkConf(dir, findings, kind, background, sharedFolder, info.package, portal, combined?.compose, nativeGateway, info.os_min_ver);
|
|
185
|
+
checkConf(dir, findings, kind, background, sharedFolder, info.package, portal, combined?.compose, nativeGateway, info.os_min_ver, daemonIngress);
|
|
183
186
|
checkScripts(dir, findings);
|
|
184
187
|
checkUninstall(dir, findings);
|
|
185
188
|
if (kind === "container")
|
|
@@ -207,7 +210,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
207
210
|
if (standalone && portal)
|
|
208
211
|
findings.push(...inspectPortalInfo(info, hasExecutor));
|
|
209
212
|
const daemon = standalone
|
|
210
|
-
? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined, nativeGateway, uninstallHook?.uninstall)
|
|
213
|
+
? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined, nativeGateway, uninstallHook?.uninstall, { ingress: daemonIngress, node: daemonNode })
|
|
211
214
|
: undefined;
|
|
212
215
|
if (background)
|
|
213
216
|
checkBackground(dir, info, payload, findings, hasExecutor, daemon, kind !== "native-go", metadata.lifecycleTimeouts, nativeGateway);
|
|
@@ -847,7 +850,7 @@ async function recoverCompose(dir, value, findings) {
|
|
|
847
850
|
return undefined;
|
|
848
851
|
}
|
|
849
852
|
}
|
|
850
|
-
async function checkDaemon(dir, info, paths, payload, findings, executor, background, combined, nativeGateway, uninstall) {
|
|
853
|
+
async function checkDaemon(dir, info, paths, payload, findings, executor, background, combined, nativeGateway, uninstall, extras = {}) {
|
|
851
854
|
if (![...paths].some((path) => path.startsWith("daemon/")))
|
|
852
855
|
return;
|
|
853
856
|
if (info.precheckstartstop !== "yes")
|
|
@@ -861,7 +864,7 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
861
864
|
await tar.x({
|
|
862
865
|
file: join(dir, "package.tgz"),
|
|
863
866
|
cwd: unpacked,
|
|
864
|
-
filter: (path) => path.replace(/^\.\//, "")
|
|
867
|
+
filter: (path) => ["daemon/config.json", `daemon/payload/${NODE_LAUNCHER}`].includes(path.replace(/^\.\//, "")),
|
|
865
868
|
});
|
|
866
869
|
try {
|
|
867
870
|
const config = JSON.parse(readFileSync(join(unpacked, "daemon/config.json"), "utf8"));
|
|
@@ -887,6 +890,14 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
887
890
|
}, dir, ["x86_64"], () => { });
|
|
888
891
|
if (!daemon)
|
|
889
892
|
throw new Error("invalid config");
|
|
893
|
+
// The packaged config already carries the launcher argv; only the launcher itself is checked here.
|
|
894
|
+
if (extras.node &&
|
|
895
|
+
(daemon.exec !== NODE_LAUNCHER ||
|
|
896
|
+
!daemon.args[0]?.startsWith("${SYNOPKG_PKGDEST}/daemon/payload/") ||
|
|
897
|
+
readOptional(join(unpacked, "daemon/payload", NODE_LAUNCHER)) !== renderNodeLauncher(extras.node)))
|
|
898
|
+
throw new Error("Node launcher differs from its build declaration");
|
|
899
|
+
if (extras.ingress)
|
|
900
|
+
daemon.ingress = extras.ingress;
|
|
890
901
|
if (config.operation_seconds !== undefined && (!Number.isInteger(config.operation_seconds) || config.operation_seconds < 1 || config.operation_seconds > 3600))
|
|
891
902
|
throw new Error('invalid operation budget');
|
|
892
903
|
if (combined &&
|
|
@@ -955,7 +966,7 @@ function archivedInfoTranslations(info) {
|
|
|
955
966
|
}
|
|
956
967
|
return translations;
|
|
957
968
|
}
|
|
958
|
-
function readInfo(path, findings, kind, compose = false) {
|
|
969
|
+
function readInfo(path, findings, kind, compose = false, daemonNode) {
|
|
959
970
|
if (!existsSync(path)) {
|
|
960
971
|
findings.push("INFO is missing");
|
|
961
972
|
return {};
|
|
@@ -984,7 +995,10 @@ function readInfo(path, findings, kind, compose = false) {
|
|
|
984
995
|
: kind === "native-node"
|
|
985
996
|
? /^Node\.js_v[0-9]{2}([><=].*)?$/
|
|
986
997
|
: undefined;
|
|
987
|
-
if (info.install_dep_packages
|
|
998
|
+
if (daemonNode && info.install_dep_packages !== nodeDependency(daemonNode))
|
|
999
|
+
findings.push(`daemon.node requires INFO install_dep_packages="${nodeDependency(daemonNode)}"`);
|
|
1000
|
+
else if (!daemonNode &&
|
|
1001
|
+
info.install_dep_packages &&
|
|
988
1002
|
!(allowed &&
|
|
989
1003
|
info.install_dep_packages.split(":").every((dep) => allowed.test(dep)))) {
|
|
990
1004
|
findings.push(`INFO depends on other packages: ${info.install_dep_packages}`);
|
|
@@ -1088,7 +1102,44 @@ async function recoverNativeGateway(dir, value, info, findings) {
|
|
|
1088
1102
|
return undefined;
|
|
1089
1103
|
}
|
|
1090
1104
|
}
|
|
1091
|
-
function
|
|
1105
|
+
function recoverDaemonNode(value, findings) {
|
|
1106
|
+
if (value === undefined)
|
|
1107
|
+
return undefined;
|
|
1108
|
+
const daemon = readDaemon({ exec: "x", targets: {}, node: value }, tmpdir(), [], () => { });
|
|
1109
|
+
if (!daemon?.node || !isDeepStrictEqual(daemon.node, value)) {
|
|
1110
|
+
findings.push("DAEMON_NODE_INVALID sdk-package.json daemon_node is not a Node.js package declaration build writes");
|
|
1111
|
+
return undefined;
|
|
1112
|
+
}
|
|
1113
|
+
return daemon.node;
|
|
1114
|
+
}
|
|
1115
|
+
async function recoverDaemonIngress(dir, value, info, paths, findings) {
|
|
1116
|
+
if (value === undefined)
|
|
1117
|
+
return undefined;
|
|
1118
|
+
try {
|
|
1119
|
+
const ingress = readDaemonIngress(value, () => { });
|
|
1120
|
+
if (!ingress || !isDeepStrictEqual(ingress, value))
|
|
1121
|
+
throw new Error("invalid ingress record");
|
|
1122
|
+
const minDsm = daemonIngressMinDsmProblem(info.os_min_ver);
|
|
1123
|
+
if (minDsm)
|
|
1124
|
+
throw new Error(minDsm);
|
|
1125
|
+
if (!paths.has("daemon/config.json"))
|
|
1126
|
+
throw new Error("ingress needs the package daemon");
|
|
1127
|
+
const root = join(dir, "daemon-ingress-inspection");
|
|
1128
|
+
mkdirSync(root);
|
|
1129
|
+
await tar.x({ file: join(dir, "package.tgz"), cwd: root, filter: (path) => path.replace(/^\.\//, "") === DAEMON_INGRESS_CONFIG });
|
|
1130
|
+
if (readOptional(join(root, DAEMON_INGRESS_CONFIG)) !== renderDaemonIngressConfig(info.package, ingress))
|
|
1131
|
+
throw new Error("ingress location differs from its fixed proxy");
|
|
1132
|
+
for (const key of ["instuninst_restart_services", "startstop_restart_services"])
|
|
1133
|
+
if (!(info[key] ?? "").split(/\s+/).includes("nginx.service"))
|
|
1134
|
+
throw new Error(`ingress requires INFO ${key}`);
|
|
1135
|
+
return ingress;
|
|
1136
|
+
}
|
|
1137
|
+
catch (e) {
|
|
1138
|
+
findings.push(`DAEMON_INGRESS_INVALID ${e.message}`);
|
|
1139
|
+
return undefined;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal, compose, nativeGateway, osMinVer, daemonIngress) {
|
|
1092
1143
|
const confDir = join(dir, "conf");
|
|
1093
1144
|
const entries = existsSync(confDir) ? readdirSync(confDir) : [];
|
|
1094
1145
|
const nativeResource = kind !== "container"
|
|
@@ -1101,6 +1152,7 @@ function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal, c
|
|
|
1101
1152
|
? { jobs: { worker: "sha256-v1" } }
|
|
1102
1153
|
: {},
|
|
1103
1154
|
...(sharedFolder ? { sharedFolder } : {}),
|
|
1155
|
+
...(daemonIngress ? { daemon: { ingress: daemonIngress } } : {}),
|
|
1104
1156
|
}, portal)
|
|
1105
1157
|
: undefined;
|
|
1106
1158
|
if (nativeResource !== undefined &&
|
|
@@ -42,7 +42,10 @@ function standaloneScriptVariants(name, manifest, expected) {
|
|
|
42
42
|
const diagnostic = renderNativeGatewayDiagnostic(manifest, name);
|
|
43
43
|
const wait = renderNativeGatewayPortWait(manifest, name);
|
|
44
44
|
const diagnosticOnly = wait ? expected.replace(wait, '') : expected;
|
|
45
|
-
|
|
45
|
+
const variants = [expected, ...(wait ? [diagnosticOnly] : []), ...(diagnostic ? [diagnosticOnly.replace(diagnostic, '')] : [])];
|
|
46
|
+
// Released packages stop the daemon in preupgrade without the first-daemon guard.
|
|
47
|
+
const unguarded = (v) => v.replace(daemonGuard(manifest.package), '').replace(delegatedDaemonGuard(manifest.package), '');
|
|
48
|
+
return name === 'preupgrade' ? [...new Set([...variants, ...variants.map(unguarded)])] : variants;
|
|
46
49
|
}
|
|
47
50
|
function renderStandaloneScriptInternal(name, manifest) {
|
|
48
51
|
if (manifest.compose) {
|
|
@@ -212,11 +215,11 @@ function renderDelegatedDaemonScript(name, manifest) {
|
|
|
212
215
|
const worker = (action) => `/usr/syno/bin/synosystemctl ${action} ${backgroundUnitName(pkg)}`;
|
|
213
216
|
// Try every owner even on failure; never leave protected ingress open merely
|
|
214
217
|
// because a producer could not stop. A partial teardown remains a failure.
|
|
215
|
-
const stop = (daemonAction, executorAction, executorStop = executor(executorAction)) => `result=0\nstop_background_worker || result=1\n${daemon} ${daemonAction} ${paths} || result=1\n${executorStop} || result=1\nexit "$result"\n`;
|
|
218
|
+
const stop = (daemonAction, executorAction, executorStop = executor(executorAction), guard = "") => `result=0\nstop_background_worker || result=1\n${guard}${daemon} ${daemonAction} ${paths} || result=1\n${executorStop} || result=1\nexit "$result"\n`;
|
|
216
219
|
const workerStop = renderBackgroundWorkerStop(pkg);
|
|
217
220
|
if (name !== "start-stop-status") {
|
|
218
221
|
if (name === "preupgrade")
|
|
219
|
-
return `#!/bin/sh\n${renderExecutorUpgradeStop(pkg)}${workerStop}${stop("stop", "stop", UPGRADE_STOP)}`;
|
|
222
|
+
return `#!/bin/sh\n${renderExecutorUpgradeStop(pkg)}${workerStop}${stop("stop", "stop", UPGRADE_STOP, delegatedDaemonGuard(pkg))}`;
|
|
220
223
|
const purge = manifest.uninstall
|
|
221
224
|
? stop(manifest.daemon?.uninstall_args ? "uninstall" : "stop", "purge $synopkgland_purge_args").replace(executor("purge $synopkgland_purge_args"), () => `synopkgland_uninstall_hook\n${executor("purge $synopkgland_purge_args")}`)
|
|
222
225
|
: stop(manifest.daemon?.uninstall_args ? "uninstall" : "stop", "purge");
|
|
@@ -273,6 +276,17 @@ function renderExecutorUpgradeStop(pkg) {
|
|
|
273
276
|
}
|
|
274
277
|
`;
|
|
275
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* preupgrade runs before DSM replaces the payload, so it sees the outgoing
|
|
281
|
+
* package. Only a payload without any daemon/ is an upgrade that introduces
|
|
282
|
+
* the daemon; a partial or symlinked daemon/ still stops or fails closed.
|
|
283
|
+
*/
|
|
284
|
+
const daemonAbsent = (pkg) => {
|
|
285
|
+
const dir = `"\${SYNOPKG_PKGDEST:-/var/packages/${pkg}/target}/daemon"`;
|
|
286
|
+
return `[ ! -e ${dir} ] && [ ! -L ${dir} ]`;
|
|
287
|
+
};
|
|
288
|
+
const daemonGuard = (pkg) => `if ${daemonAbsent(pkg)}; then exit 0; fi\n`;
|
|
289
|
+
const delegatedDaemonGuard = (pkg) => `${daemonAbsent(pkg)} || `;
|
|
276
290
|
function renderDaemonScript(name, manifest) {
|
|
277
291
|
const pkg = manifest.package;
|
|
278
292
|
const control = `"/var/packages/${pkg}/target/daemon/control"`;
|
|
@@ -283,7 +297,7 @@ function renderDaemonScript(name, manifest) {
|
|
|
283
297
|
const uninstall = name === "preuninst" && manifest.daemon?.uninstall_args;
|
|
284
298
|
const upgrade = uninstall
|
|
285
299
|
? `if [ "\${SYNOPKG_PKG_STATUS:-}" = "UPGRADE" ]; then\n exec ${control} stop ${paths}\nfi\n`
|
|
286
|
-
: "";
|
|
300
|
+
: name === "preupgrade" ? daemonGuard(pkg) : "";
|
|
287
301
|
return `#!/bin/sh\n${upgrade}exec ${control} ${uninstall ? "uninstall" : "stop"} ${paths}\n`;
|
|
288
302
|
}
|
|
289
303
|
if (name !== "start-stop-status")
|
|
@@ -11,6 +11,7 @@ import { checkGlossary, loadCatalogs, readInfoTranslations, } from "./i18n.js";
|
|
|
11
11
|
import { loadRootOps, readRootSettings, rootBinaryProblem, ROOT_OPS_DIR, } from "./root-ops.js";
|
|
12
12
|
import { readComposeApp } from "./compose.js";
|
|
13
13
|
import { readDaemon, readLifecycleTimeouts } from "./daemon.js";
|
|
14
|
+
import { daemonIngressMinDsmProblem } from "./daemon-ingress.js";
|
|
14
15
|
import { readIdentityGateway, } from "./identity-gateway.js";
|
|
15
16
|
import { readUninstall } from "./uninstall.js";
|
|
16
17
|
import { readAppScripts } from "./app-scripts.js";
|
|
@@ -277,6 +278,13 @@ function readManifest(raw, c) {
|
|
|
277
278
|
c.fail("COMPOSE_WEB_INVALID", "Compose upstream is derived from the SDK Compose ingress endpoint; remove identity_gateway.upstream.");
|
|
278
279
|
// A failed Compose plan already explains the invalid upstream declaration.
|
|
279
280
|
const identityGateway = raw.compose !== undefined && !compose ? undefined : readIdentityGateway(gatewayValue, daemon || (compose ? { targets: compose.targets } : undefined), (e) => c.fail(e.code, e.message), false, Boolean(compose?.plan.ingress));
|
|
281
|
+
if (daemon?.ingress) {
|
|
282
|
+
const problem = daemonIngressMinDsmProblem(osMinVer);
|
|
283
|
+
if (problem)
|
|
284
|
+
c.fail("STANDALONE_DAEMON_INGRESS_MIN_DSM", problem);
|
|
285
|
+
}
|
|
286
|
+
if ((daemon?.ingress || daemon?.node) && raw.identity_gateway !== undefined)
|
|
287
|
+
c.fail("STANDALONE_DAEMON_INVALID", "daemon.node and daemon.ingress run the App's own daemon; identity_gateway runs its gateway as the daemon. Use one or the other.");
|
|
280
288
|
if (compose && identityGateway && compose.sdk !== identityGateway.sdk)
|
|
281
289
|
c.fail("STANDALONE_IDENTITY_GATEWAY_INVALID", "compose and identity_gateway must use the same SDK release.");
|
|
282
290
|
if (compose &&
|
|
@@ -3,6 +3,7 @@ import type { Capabilities } from "./manifest.js";
|
|
|
3
3
|
import { type SharedFolder } from "./shared-folder.js";
|
|
4
4
|
import { type PortalPackage } from "../portal/package.js";
|
|
5
5
|
import { type NativeGatewayPlan } from './native-gateway.js';
|
|
6
|
+
import { type DaemonIngress } from "./daemon-ingress.js";
|
|
6
7
|
/**
|
|
7
8
|
* `conf/resource` for a native package: only DSM's official `data-share`,
|
|
8
9
|
* `systemd-user-unit` and `web-config` workers, in a fixed key order.
|
|
@@ -15,4 +16,7 @@ export declare function renderStandaloneResource(m: {
|
|
|
15
16
|
capabilities: Capabilities;
|
|
16
17
|
compose?: ComposeApp;
|
|
17
18
|
sharedFolder?: SharedFolder;
|
|
19
|
+
daemon?: {
|
|
20
|
+
ingress?: DaemonIngress;
|
|
21
|
+
};
|
|
18
22
|
}, portal?: PortalPackage): string | undefined;
|
|
@@ -2,6 +2,7 @@ import { renderDockerProjectResource } from "../build-spk/resource-worker.js";
|
|
|
2
2
|
import { sharedFolderResource } from "./shared-folder.js";
|
|
3
3
|
import { portalResource } from "../portal/package.js";
|
|
4
4
|
import { nativeGatewayResource } from './native-gateway.js';
|
|
5
|
+
import { daemonIngressResource } from "./daemon-ingress.js";
|
|
5
6
|
/**
|
|
6
7
|
* `conf/resource` for a native package: only DSM's official `data-share`,
|
|
7
8
|
* `systemd-user-unit` and `web-config` workers, in a fixed key order.
|
|
@@ -9,7 +10,10 @@ import { nativeGatewayResource } from './native-gateway.js';
|
|
|
9
10
|
*/
|
|
10
11
|
export function renderStandaloneResource(m, portal) {
|
|
11
12
|
const dataShare = m.sharedFolder && sharedFolderResource(m.package, m.sharedFolder);
|
|
12
|
-
const
|
|
13
|
+
const nginx = [nativeGatewayResource(m), m.daemon?.ingress && daemonIngressResource()]
|
|
14
|
+
.flatMap((r) => r?.["nginx-static-config"]?.enable ?? []);
|
|
15
|
+
const webConfig = { ...(portal ? portalResource()['web-config'] : {}),
|
|
16
|
+
...(nginx.length ? { 'nginx-static-config': { enable: nginx } } : {}) };
|
|
13
17
|
const resource = {
|
|
14
18
|
...(m.compose?.plan.devices?.gpu && !m.compose.plan.mounts_blocked ? { "video-driver": {} } : {}),
|
|
15
19
|
...(m.compose && !m.compose.plan.mounts_blocked
|
package/dist/standalone/spk.js
CHANGED
|
@@ -29,7 +29,8 @@ import { copyAppTree } from "./bridge-app.js";
|
|
|
29
29
|
import { isBridgeBackend } from "./bridge-backend.js";
|
|
30
30
|
import { hostScriptName, renderHostAdapterScript } from "./host-adapter.js";
|
|
31
31
|
import { writeComposePayload } from "./compose.js";
|
|
32
|
-
import { copyDaemonPayload } from "./daemon.js";
|
|
32
|
+
import { copyDaemonPayload, nodeDependency } from "./daemon.js";
|
|
33
|
+
import { DAEMON_INGRESS_CONFIG, renderDaemonIngressConfig } from "./daemon-ingress.js";
|
|
33
34
|
import { GATEWAY_CGI_PATH, GATEWAY_CONFIG_PATH, GATEWAY_EXEC, identityGatewayConfig, identityGatewayDeclaration, } from "./identity-gateway.js";
|
|
34
35
|
import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, backgroundUnitName, renderBackgroundUnit, BRIDGE_WORKER_ARGS, } from "./background.js";
|
|
35
36
|
import { renderStandaloneResource } from "./resource.js";
|
|
@@ -104,6 +105,8 @@ export async function assembleStandaloneSpk(options) {
|
|
|
104
105
|
if (!options.daemonControl)
|
|
105
106
|
throw new StandaloneBuildError("STANDALONE_DAEMON_ARTIFACT_REQUIRED", "daemon controller artifact required.");
|
|
106
107
|
copyDaemonPayload(manifest, options.arch, join(staging, "package", "daemon"), options.daemonControl);
|
|
108
|
+
if (manifest.daemon?.ingress)
|
|
109
|
+
writeFile(join(staging, "package", DAEMON_INGRESS_CONFIG), renderDaemonIngressConfig(manifest.package, manifest.daemon.ingress));
|
|
107
110
|
}
|
|
108
111
|
if (manifest.identityGateway) {
|
|
109
112
|
if (!options.identityGateway)
|
|
@@ -230,6 +233,8 @@ export async function assembleStandaloneSpk(options) {
|
|
|
230
233
|
backend: manifest.backend.kind,
|
|
231
234
|
...(gatewayAdmissionPolicy(manifest.identityGateway, manifest.entry) ? { provider_only_oidc: gatewayAdmissionPolicy(manifest.identityGateway, manifest.entry) } : {}),
|
|
232
235
|
...(nativeGatewaySupported(manifest) ? { native_gateway: { sdk: manifest.identityGateway.sdk, listen: manifest.identityGateway.listen } } : {}),
|
|
236
|
+
...(manifest.daemon?.ingress ? { daemon_ingress: manifest.daemon.ingress } : {}),
|
|
237
|
+
...(manifest.daemon?.node ? { daemon_node: manifest.daemon.node } : {}),
|
|
233
238
|
launch: resolvedSdkLaunch(manifest),
|
|
234
239
|
capabilities: Object.keys(manifest.capabilities).sort(),
|
|
235
240
|
...(manifest.compose
|
|
@@ -360,6 +365,8 @@ export function renderInfo(manifest, arch, checksum, executorDigest) {
|
|
|
360
365
|
const backend = manifest.backend;
|
|
361
366
|
if (isBridgeBackend(backend) && backend.node)
|
|
362
367
|
fields.push(["install_dep_packages", backend.node.package]);
|
|
368
|
+
if (manifest.daemon?.node)
|
|
369
|
+
fields.push(["install_dep_packages", nodeDependency(manifest.daemon.node)]);
|
|
363
370
|
fields.push(...metadataInfoFields(manifest.metadata));
|
|
364
371
|
// Background packages need prestart: it runs before DSM's systemd-user-unit acquire.
|
|
365
372
|
if (manifest.daemon || manifest.compose || manifest.capabilities.jobs)
|
|
@@ -371,9 +378,11 @@ export function renderInfo(manifest, arch, checksum, executorDigest) {
|
|
|
371
378
|
if (text?.description)
|
|
372
379
|
fields.push([`description_${DSM_LANGS[locale]}`, text.description]);
|
|
373
380
|
}
|
|
374
|
-
|
|
381
|
+
// DSM reloads nginx on start and stop so the location follows the package state.
|
|
382
|
+
const nginx = nativeGatewaySupported(manifest) || Boolean(manifest.daemon?.ingress);
|
|
383
|
+
if (nginx)
|
|
375
384
|
fields.push(['startstop_restart_services', 'nginx.service']);
|
|
376
|
-
return portalInfoFields(fields, Boolean(manifest.portal) ||
|
|
385
|
+
return portalInfoFields(fields, Boolean(manifest.portal) || nginx).map(([key, value]) => `${key}=${quote(value)}\n`).join("");
|
|
377
386
|
}
|
|
378
387
|
function copyUiTree(source, target) {
|
|
379
388
|
for (const entry of ["app.js", "app.css"]) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.2",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@napi-rs/keyring": "^2.1.1",
|
|
30
|
-
"@synopackageland/templates": "0.25.
|
|
30
|
+
"@synopackageland/templates": "0.25.1",
|
|
31
31
|
"ajv": "^8.17.1",
|
|
32
32
|
"commander": "^13.1.0",
|
|
33
33
|
"json-canonicalize": "^3.0.1",
|