@synopackageland/cli 0.17.4 → 0.17.5
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 +15 -1
- package/README.md +27 -1
- package/dist/build-spk/compose-retained-data.d.ts +11 -0
- package/dist/build-spk/compose-retained-data.js +34 -0
- package/dist/build-spk/delete-app-data.d.ts +1 -1
- package/dist/build-spk/delete-app-data.js +1 -1
- package/dist/build-spk/uninstall-wizard.d.ts +2 -0
- package/dist/build-spk/uninstall-wizard.js +2 -1
- package/dist/build-spk/wizard-messages.d.ts +22 -0
- package/dist/build-spk/wizard-messages.js +22 -0
- package/dist/ctl/cli.js +10 -6
- package/dist/ctl/flows.js +1 -1
- package/dist/ctl/service.d.ts +4 -1
- package/dist/ctl/service.js +20 -6
- package/dist/ctl/transfers.d.ts +1 -1
- package/dist/ctl/transfers.js +5 -2
- package/dist/ctl/transport.d.ts +2 -1
- package/dist/ctl/transport.js +7 -3
- package/dist/runtime/compose-data.d.ts +10 -0
- package/dist/runtime/compose-data.js +20 -0
- package/dist/runtime/executor-digests.json +11 -0
- package/dist/runtime/executor-policy.d.ts +25 -0
- package/dist/runtime/executor-policy.js +10 -1
- package/dist/runtime/runtime-budgets.d.ts +24 -0
- package/dist/runtime/runtime-budgets.js +13 -0
- package/dist/standalone/background.d.ts +3 -3
- package/dist/standalone/background.js +12 -11
- package/dist/standalone/compose.d.ts +3 -1
- package/dist/standalone/compose.js +8 -1
- package/dist/standalone/daemon.d.ts +4 -0
- package/dist/standalone/daemon.js +36 -23
- package/dist/standalone/dsm-window.js +2 -1
- package/dist/standalone/identity-gateway.js +9 -1
- package/dist/standalone/inspect-spk.js +87 -15
- package/dist/standalone/lifecycle.js +16 -9
- package/dist/standalone/manifest.d.ts +1 -0
- package/dist/standalone/manifest.js +8 -5
- package/dist/standalone/native-gateway.d.ts +17 -0
- package/dist/standalone/native-gateway.js +115 -0
- package/dist/standalone/resource.d.ts +3 -0
- package/dist/standalone/resource.js +3 -1
- package/dist/standalone/spk.js +14 -3
- package/dist/standalone/uninstall-hook.js +21 -8
- package/dist/standalone/uninstall.d.ts +1 -1
- package/dist/standalone/uninstall.js +2 -2
- package/package.json +2 -2
- package/skills/reference/compose.md +31 -0
- package/skills/reference/create.md +1 -1
- package/skills/reference/lifecycle.md +25 -0
|
@@ -9,7 +9,7 @@ import { isNativeArchFamily, } from "../build-spk/dsm-arch.js";
|
|
|
9
9
|
import { checkGlossary, loadCatalogs, readInfoTranslations, } from "./i18n.js";
|
|
10
10
|
import { loadRootOps, readRootSettings, rootBinaryProblem, ROOT_OPS_DIR, } from "./root-ops.js";
|
|
11
11
|
import { readComposeApp } from "./compose.js";
|
|
12
|
-
import { readDaemon } from "./daemon.js";
|
|
12
|
+
import { readDaemon, readLifecycleTimeouts } from "./daemon.js";
|
|
13
13
|
import { readIdentityGateway, } from "./identity-gateway.js";
|
|
14
14
|
import { readUninstall } from "./uninstall.js";
|
|
15
15
|
import { readAppScripts } from "./app-scripts.js";
|
|
@@ -231,8 +231,10 @@ function readManifest(raw, c) {
|
|
|
231
231
|
c.fail("STANDALONE_API_PERMISSIONS_INVALID", `backend/${API_PERMISSIONS_FILE[backend.kind]} must define the centralized API policy: ${String(error)}`);
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
const timeoutOnly = isRecord(raw.daemon) && Object.keys(raw.daemon).length === 1 && 'timeouts' in raw.daemon;
|
|
235
|
+
const lifecycleTimeouts = timeoutOnly ? readLifecycleTimeouts(raw.daemon.timeouts, (e) => c.fail(e.code, e.message)) : undefined;
|
|
234
236
|
const daemon = arches &&
|
|
235
|
-
readDaemon(raw.daemon, c.root, arches, (e) => c.fail(e.code, e.message));
|
|
237
|
+
readDaemon(timeoutOnly ? undefined : raw.daemon, c.root, arches, (e) => c.fail(e.code, e.message));
|
|
236
238
|
const gatewayRaw = isRecord(raw.identity_gateway) ? raw.identity_gateway : {};
|
|
237
239
|
if (raw.compose !== undefined && isRecord(raw.ctl) && raw.ctl.enabled === true)
|
|
238
240
|
c.fail("CTL_UNSUPPORTED_BACKEND", "Compose does not support automatic ctl descriptions; disable ctl.");
|
|
@@ -246,7 +248,7 @@ function readManifest(raw, c) {
|
|
|
246
248
|
composeMounts: capabilities.composeMounts,
|
|
247
249
|
});
|
|
248
250
|
if (raw.compose !== undefined &&
|
|
249
|
-
(raw.daemon !== undefined ||
|
|
251
|
+
((raw.daemon !== undefined && !timeoutOnly) ||
|
|
250
252
|
Object.keys(capabilities).some((k) => k !== "composeMounts") ||
|
|
251
253
|
!osMinVer ||
|
|
252
254
|
!isDsmAtLeast(osMinVer, "7.2.1-69057")))
|
|
@@ -280,7 +282,7 @@ function readManifest(raw, c) {
|
|
|
280
282
|
s.port === identityGateway.oidc?.issuer_port))))
|
|
281
283
|
c.fail("STANDALONE_IDENTITY_GATEWAY_INVALID", "compose service ports must contain upstream and not overlap gateway or issuer ports.");
|
|
282
284
|
if (capabilities.jobs &&
|
|
283
|
-
raw.daemon !== undefined &&
|
|
285
|
+
raw.daemon !== undefined && !timeoutOnly &&
|
|
284
286
|
capabilities.delegate?.lifetime !== "until-revoked") {
|
|
285
287
|
c.fail("CAPABILITY_COMBINATION_INVALID", "daemon can only coexist with an explicit until-revoked delegate fixed worker; plain capabilities.jobs stays mutually exclusive.");
|
|
286
288
|
}
|
|
@@ -299,7 +301,7 @@ function readManifest(raw, c) {
|
|
|
299
301
|
delegate.destinations?.length)) {
|
|
300
302
|
c.fail("CAPABILITY_UNSUPPORTED", `For backend.runtime: ${backend.kind}, capabilities.delegate supports only operations: [files.moveFromStaging], destinations and lifetime: until-revoked.`);
|
|
301
303
|
}
|
|
302
|
-
if (raw.daemon !== undefined)
|
|
304
|
+
if (raw.daemon !== undefined && !timeoutOnly)
|
|
303
305
|
c.fail("CAPABILITY_UNSUPPORTED", `backend.runtime: ${backend.kind} does not support daemon; a v1 App runs once per request (exec mode).`);
|
|
304
306
|
if (permissions)
|
|
305
307
|
checkBridgeMethods(backend, permissions, c);
|
|
@@ -438,6 +440,7 @@ function readManifest(raw, c) {
|
|
|
438
440
|
...(rootOps ? { rootOps } : {}),
|
|
439
441
|
...(rootPrograms ? { rootPrograms } : {}),
|
|
440
442
|
...(daemon ? { daemon } : {}),
|
|
443
|
+
...(lifecycleTimeouts ? { lifecycleTimeouts } : {}),
|
|
441
444
|
...(identityGateway ? { identityGateway } : {}),
|
|
442
445
|
...(compose ? { compose } : {}),
|
|
443
446
|
...(uninstall ? { uninstall } : {}),
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IdentityGateway } from './identity-gateway.js';
|
|
2
|
+
export type NativeGatewayPlan = Pick<IdentityGateway, 'sdk' | 'listen'>;
|
|
3
|
+
type Target = {
|
|
4
|
+
package: string;
|
|
5
|
+
os_min_ver?: string;
|
|
6
|
+
identityGateway?: NativeGatewayPlan;
|
|
7
|
+
};
|
|
8
|
+
export declare const NATIVE_GATEWAY_CONFIG = "sso/native-nginx.conf";
|
|
9
|
+
export declare function nativeGatewaySdk(sdk: string): boolean;
|
|
10
|
+
export declare function nativeGatewaySupported(m: Target): boolean;
|
|
11
|
+
export declare function gatewayEntryPath(m: Target): string;
|
|
12
|
+
export declare function legacyGatewayNotice(): string;
|
|
13
|
+
export declare function nativeGatewayResource(m: Target): Record<string, unknown> | undefined;
|
|
14
|
+
export declare function renderNativeGatewayConfig(m: Target): string;
|
|
15
|
+
/** Runs after DSM creates pkgvar, before package start activates the nginx resource. */
|
|
16
|
+
export declare function renderNativeGatewayPrepare(m: Target): string;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { isDsmAtLeast, PORTAL_MIN_DSM } from '../build-spk/dsm-version.js';
|
|
2
|
+
export const NATIVE_GATEWAY_CONFIG = 'sso/native-nginx.conf';
|
|
3
|
+
// Earlier SDK releases do not understand native_ingress. Keep their JSON ABI
|
|
4
|
+
// and put an explanation in front of their existing direct-port launcher.
|
|
5
|
+
export function nativeGatewaySdk(sdk) {
|
|
6
|
+
const [major, minor, patch] = sdk.split('.').map(Number);
|
|
7
|
+
return major > 0 || minor > 17 || minor === 17 && patch >= 5;
|
|
8
|
+
}
|
|
9
|
+
export function nativeGatewaySupported(m) {
|
|
10
|
+
return Boolean(m.identityGateway && nativeGatewaySdk(m.identityGateway.sdk)
|
|
11
|
+
&& m.os_min_ver && isDsmAtLeast(m.os_min_ver, PORTAL_MIN_DSM));
|
|
12
|
+
}
|
|
13
|
+
export function gatewayEntryPath(m) {
|
|
14
|
+
return `/webman/3rdparty/${m.package}/sso/open.${m.identityGateway && !nativeGatewaySdk(m.identityGateway.sdk) ? 'html' : 'cgi'}`;
|
|
15
|
+
}
|
|
16
|
+
export function legacyGatewayNotice() {
|
|
17
|
+
return '<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src \'none\'; frame-ancestors \'none\'"><title>Direct App connection</title><p>DSM-managed HTTPS is unavailable for this App. The direct connection may show a certificate warning or use unencrypted HTTP.</p><a href="open.cgi">Continue to the direct connection</a></html>\n';
|
|
18
|
+
}
|
|
19
|
+
export function nativeGatewayResource(m) {
|
|
20
|
+
if (!nativeGatewaySupported(m))
|
|
21
|
+
return undefined;
|
|
22
|
+
const g = m.identityGateway, https = g.listen.https ?? g.listen.http;
|
|
23
|
+
const ports = [https, ...(g.listen.http && g.listen.http !== https ? [g.listen.http] : [])];
|
|
24
|
+
return { 'nginx-static-config': { enable: [{ type: 'server', relpath: NATIVE_GATEWAY_CONFIG,
|
|
25
|
+
ports: ports.map(port => ({ port, protocol: 'tcp', schema: port === https ? 'https' : 'http' })) }] } };
|
|
26
|
+
}
|
|
27
|
+
export function renderNativeGatewayConfig(m) {
|
|
28
|
+
if (!nativeGatewaySupported(m))
|
|
29
|
+
return '';
|
|
30
|
+
const g = m.identityGateway, port = g.listen.https ?? g.listen.http;
|
|
31
|
+
const state = `/var/packages/${m.package}/var/sso`;
|
|
32
|
+
// Public ssl_certificate is deliberately inherited from DSM's http context.
|
|
33
|
+
// Only nginx's master reads the package's private upstream key. No DSM key
|
|
34
|
+
// is read or copied by the package, and no secret appears in this config.
|
|
35
|
+
return `server {
|
|
36
|
+
listen ${port} ssl;
|
|
37
|
+
listen [::]:${port} ssl;
|
|
38
|
+
server_name _;
|
|
39
|
+
client_max_body_size 0;
|
|
40
|
+
location / {
|
|
41
|
+
proxy_pass https://unix:/tmp/synopkgland-gateway-${m.package}.sock:;
|
|
42
|
+
proxy_http_version 1.1;
|
|
43
|
+
proxy_ssl_verify on;
|
|
44
|
+
proxy_ssl_verify_depth 1;
|
|
45
|
+
proxy_ssl_server_name on;
|
|
46
|
+
proxy_ssl_name gateway.invalid;
|
|
47
|
+
proxy_ssl_trusted_certificate ${state}/native-cert.pem;
|
|
48
|
+
proxy_ssl_certificate ${state}/native-cert.pem;
|
|
49
|
+
proxy_ssl_certificate_key ${state}/native-key.pem;
|
|
50
|
+
proxy_set_header Host $http_host;
|
|
51
|
+
proxy_set_header X-Synopkgland-Client-IP $remote_addr;
|
|
52
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
53
|
+
proxy_set_header Connection $http_connection;
|
|
54
|
+
proxy_request_buffering off;
|
|
55
|
+
proxy_buffering off;
|
|
56
|
+
proxy_read_timeout 3600s;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
${g.listen.http && g.listen.http !== port ? `server {
|
|
60
|
+
listen ${g.listen.http};
|
|
61
|
+
listen [::]:${g.listen.http};
|
|
62
|
+
server_name _;
|
|
63
|
+
return 308 https://$host:${port}$request_uri;
|
|
64
|
+
}
|
|
65
|
+
` : ''}`;
|
|
66
|
+
}
|
|
67
|
+
/** Runs after DSM creates pkgvar, before package start activates the nginx resource. */
|
|
68
|
+
export function renderNativeGatewayPrepare(m) {
|
|
69
|
+
if (!nativeGatewaySupported(m))
|
|
70
|
+
return '';
|
|
71
|
+
return `synopkgland_native_keys() (
|
|
72
|
+
set -eu
|
|
73
|
+
umask 077
|
|
74
|
+
v="\${SYNOPKG_PKGVAR:-/var/packages/${m.package}/var}/sso"
|
|
75
|
+
if [ -L "$v" ]; then echo 'Native HTTPS state must not be a symlink.' >&2; exit 1; fi
|
|
76
|
+
mkdir -p "$v"
|
|
77
|
+
if [ "$(stat -c %a "$v")" != 700 ]; then echo 'Native HTTPS state must have mode 0700.' >&2; exit 1; fi
|
|
78
|
+
c="$v/native-cert.pem"; k="$v/native-key.pem"
|
|
79
|
+
if [ -e "$c" ] || [ -L "$c" ] || [ -e "$k" ] || [ -L "$k" ]; then
|
|
80
|
+
for f in "$c" "$k"; do
|
|
81
|
+
if [ -L "$f" ] || [ ! -f "$f" ] || [ "$(stat -c %a "$f")" != 600 ]; then
|
|
82
|
+
echo 'Native HTTPS requires a complete private keypair with mode 0600.' >&2; exit 1
|
|
83
|
+
fi
|
|
84
|
+
done
|
|
85
|
+
/usr/bin/openssl x509 -in "$c" -noout -checkend 0 >/dev/null || exit 1
|
|
86
|
+
cp=$(/usr/bin/openssl x509 -in "$c" -pubkey -noout | /usr/bin/openssl sha256)
|
|
87
|
+
kp=$(/usr/bin/openssl pkey -in "$k" -pubout | /usr/bin/openssl sha256)
|
|
88
|
+
[ "$cp" = "$kp" ] || { echo 'Native HTTPS keypair does not match.' >&2; exit 1; }
|
|
89
|
+
exit 0
|
|
90
|
+
fi
|
|
91
|
+
t=$(mktemp -d "$v/.native-tls.XXXXXX")
|
|
92
|
+
trap 'rm -rf "$t"' EXIT HUP INT TERM
|
|
93
|
+
cat > "$t/openssl.cnf" <<'NATIVE_TLS_CONFIG'
|
|
94
|
+
[req]
|
|
95
|
+
distinguished_name=subject
|
|
96
|
+
x509_extensions=extensions
|
|
97
|
+
prompt=no
|
|
98
|
+
[subject]
|
|
99
|
+
CN=gateway.invalid
|
|
100
|
+
[extensions]
|
|
101
|
+
basicConstraints=critical,CA:TRUE
|
|
102
|
+
keyUsage=critical,digitalSignature,keyEncipherment,keyCertSign
|
|
103
|
+
extendedKeyUsage=serverAuth,clientAuth
|
|
104
|
+
subjectAltName=DNS:gateway.invalid
|
|
105
|
+
NATIVE_TLS_CONFIG
|
|
106
|
+
/usr/bin/openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -config "$t/openssl.cnf" -keyout "$t/key.pem" -out "$t/cert.pem" >/dev/null 2>&1 || {
|
|
107
|
+
echo 'Could not prepare the private native HTTPS connection.' >&2; exit 1;
|
|
108
|
+
}
|
|
109
|
+
chmod 600 "$t/key.pem" "$t/cert.pem"
|
|
110
|
+
mv "$t/key.pem" "$k"
|
|
111
|
+
mv "$t/cert.pem" "$c"
|
|
112
|
+
)
|
|
113
|
+
synopkgland_native_keys || exit $?
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { ComposeApp } from "./compose.js";
|
|
|
2
2
|
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
|
+
import { type NativeGatewayPlan } from './native-gateway.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,6 +10,8 @@ import { type PortalPackage } from "../portal/package.js";
|
|
|
9
10
|
*/
|
|
10
11
|
export declare function renderStandaloneResource(m: {
|
|
11
12
|
package: string;
|
|
13
|
+
os_min_ver?: string;
|
|
14
|
+
identityGateway?: NativeGatewayPlan;
|
|
12
15
|
capabilities: Capabilities;
|
|
13
16
|
compose?: ComposeApp;
|
|
14
17
|
sharedFolder?: SharedFolder;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { nativeGatewayResource } from './native-gateway.js';
|
|
4
5
|
/**
|
|
5
6
|
* `conf/resource` for a native package: only DSM's official `data-share`,
|
|
6
7
|
* `systemd-user-unit` and `web-config` workers, in a fixed key order.
|
|
@@ -8,13 +9,14 @@ import { portalResource } from "../portal/package.js";
|
|
|
8
9
|
*/
|
|
9
10
|
export function renderStandaloneResource(m, portal) {
|
|
10
11
|
const dataShare = m.sharedFolder && sharedFolderResource(m.package, m.sharedFolder);
|
|
12
|
+
const webConfig = { ...(portal ? portalResource()['web-config'] : {}), ...nativeGatewayResource(m) };
|
|
11
13
|
const resource = {
|
|
12
14
|
...(m.compose && !m.compose.plan.mounts_blocked
|
|
13
15
|
? renderDockerProjectResource(m.package, "compose", false, m.compose.plan.project_name)
|
|
14
16
|
: {}),
|
|
15
17
|
...(dataShare ? { "data-share": dataShare } : {}),
|
|
16
18
|
...(m.capabilities.jobs ? { "systemd-user-unit": {} } : {}),
|
|
17
|
-
...(
|
|
19
|
+
...(Object.keys(webConfig).length ? { 'web-config': webConfig } : {}),
|
|
18
20
|
};
|
|
19
21
|
return Object.keys(resource).length > 0
|
|
20
22
|
? JSON.stringify(resource)
|
package/dist/standalone/spk.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nativeGatewaySupported, nativeGatewaySdk, renderNativeGatewayConfig, NATIVE_GATEWAY_CONFIG, legacyGatewayNotice, gatewayEntryPath } from './native-gateway.js';
|
|
1
2
|
import { portalInfoFields } from "../portal/info.js";
|
|
2
3
|
import { sdkSource } from "../runtime/sdk-source.js";
|
|
3
4
|
import { resolvedSdkLaunch } from "../contract/app-launch.js";
|
|
@@ -116,6 +117,10 @@ export async function assembleStandaloneSpk(options) {
|
|
|
116
117
|
chmodSync(target, EXEC_MODE);
|
|
117
118
|
}
|
|
118
119
|
writeFile(join(staging, "package", GATEWAY_CONFIG_PATH), identityGatewayConfig(manifest));
|
|
120
|
+
if (nativeGatewaySupported(manifest))
|
|
121
|
+
writeFile(join(staging, 'package', NATIVE_GATEWAY_CONFIG), renderNativeGatewayConfig(manifest));
|
|
122
|
+
if (!nativeGatewaySdk(manifest.identityGateway.sdk))
|
|
123
|
+
writeFile(join(staging, 'package/ui/sso/open.html'), legacyGatewayNotice());
|
|
119
124
|
}
|
|
120
125
|
if (manifest.capabilities.jobs) {
|
|
121
126
|
if (!options.backgroundWorker)
|
|
@@ -208,8 +213,11 @@ export async function assembleStandaloneSpk(options) {
|
|
|
208
213
|
.digest("hex");
|
|
209
214
|
writeFile(join(staging, "sdk-package.json"), JSON.stringify({
|
|
210
215
|
schema: 2,
|
|
216
|
+
...(manifest.lifecycleTimeouts ? { lifecycleTimeouts: manifest.lifecycleTimeouts } : {}),
|
|
217
|
+
...("timeoutSeconds" in manifest.backend && manifest.backend.timeoutSeconds ? { operationSeconds: manifest.backend.timeoutSeconds } : {}),
|
|
211
218
|
...(options.apiDescription ? { ctl: { hash: options.apiDescription.sha256, registry: options.apiRegistry } } : {}),
|
|
212
219
|
backend: manifest.backend.kind,
|
|
220
|
+
...(nativeGatewaySupported(manifest) ? { native_gateway: { sdk: manifest.identityGateway.sdk, listen: manifest.identityGateway.listen } } : {}),
|
|
213
221
|
launch: resolvedSdkLaunch(manifest),
|
|
214
222
|
capabilities: Object.keys(manifest.capabilities).sort(),
|
|
215
223
|
...(manifest.compose
|
|
@@ -236,7 +244,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
236
244
|
writeFile(join(staging, "conf", "resource"), resource);
|
|
237
245
|
if (manifest.capabilities.jobs) {
|
|
238
246
|
mkdirSync(join(staging, BACKGROUND_UNIT_DIR), { recursive: true });
|
|
239
|
-
writeFile(join(staging, BACKGROUND_UNIT_DIR, backgroundUnitName(manifest.package)), renderBackgroundUnit(manifest.package, isBridgeBackend(manifest.backend) ? BRIDGE_WORKER_ARGS : []));
|
|
247
|
+
writeFile(join(staging, BACKGROUND_UNIT_DIR, backgroundUnitName(manifest.package)), renderBackgroundUnit(manifest.package, isBridgeBackend(manifest.backend) ? BRIDGE_WORKER_ARGS : [], manifest.daemon?.timeouts.stop ?? manifest.lifecycleTimeouts?.stop ?? 30));
|
|
240
248
|
}
|
|
241
249
|
mkdirSync(join(staging, "scripts"));
|
|
242
250
|
for (const name of LIFECYCLE_SCRIPTS) {
|
|
@@ -265,6 +273,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
265
273
|
? renderSharedFolderUpgradeWizardFiles(manifest.package, folder)
|
|
266
274
|
: {}),
|
|
267
275
|
...renderUninstallWizardFiles(manifest.package, "native", {
|
|
276
|
+
composeData: manifest.compose?.plan.app_data,
|
|
268
277
|
executor: Boolean(options.executor),
|
|
269
278
|
portal: Boolean(portal),
|
|
270
279
|
portalEntry: Boolean(manifest.portal && portalByExecutor(manifest.os_min_ver)),
|
|
@@ -324,7 +333,7 @@ export function renderInfo(manifest, arch, checksum, executorDigest) {
|
|
|
324
333
|
if (manifest.identityGateway)
|
|
325
334
|
fields.push([
|
|
326
335
|
"adminurl",
|
|
327
|
-
|
|
336
|
+
gatewayEntryPath(manifest),
|
|
328
337
|
]);
|
|
329
338
|
}
|
|
330
339
|
const backend = manifest.backend;
|
|
@@ -341,7 +350,9 @@ export function renderInfo(manifest, arch, checksum, executorDigest) {
|
|
|
341
350
|
if (text?.description)
|
|
342
351
|
fields.push([`description_${DSM_LANGS[locale]}`, text.description]);
|
|
343
352
|
}
|
|
344
|
-
|
|
353
|
+
if (nativeGatewaySupported(manifest))
|
|
354
|
+
fields.push(['startstop_restart_services', 'nginx.service']);
|
|
355
|
+
return portalInfoFields(fields, Boolean(manifest.portal) || nativeGatewaySupported(manifest)).map(([key, value]) => `${key}=${quote(value)}\n`).join("");
|
|
345
356
|
}
|
|
346
357
|
function copyUiTree(source, target) {
|
|
347
358
|
for (const entry of ["app.js", "app.css"]) {
|
|
@@ -22,6 +22,7 @@ export function renderUninstallHook(manifest) {
|
|
|
22
22
|
if (!purge)
|
|
23
23
|
return "";
|
|
24
24
|
const pkg = manifest.package;
|
|
25
|
+
const reserve = Math.min(5, Math.max(1, Math.floor(purge.timeout / 4)));
|
|
25
26
|
const env = [
|
|
26
27
|
"PATH=/usr/bin:/bin", 'HOME="$hook_var"', 'TMPDIR="$hook_var/tmp"',
|
|
27
28
|
...Object.entries(purge.env).map(([key, value]) => `${key}=${word(value)}`),
|
|
@@ -47,7 +48,11 @@ ${manifest.uninstall?.state ? wizardFlag("delete_state", ["pkgwizard_delete_stat
|
|
|
47
48
|
# DSM's bash logs a failed kill to syslog even with 2>/dev/null, so a
|
|
48
49
|
# finished hook is looked up in /proc wherever there is one.
|
|
49
50
|
hook_running() {
|
|
50
|
-
if [ -d /proc/self ]; then
|
|
51
|
+
if [ -d /proc/self ]; then
|
|
52
|
+
[ -r "/proc/$hook_pid/stat" ] || return 1
|
|
53
|
+
case "$(sed 's/.*) //' "/proc/$hook_pid/stat" 2>/dev/null)" in Z*|X*|'') return 1 ;; esac
|
|
54
|
+
return 0
|
|
55
|
+
else kill -0 "$hook_pid" 2>/dev/null; fi
|
|
51
56
|
}
|
|
52
57
|
# A process in /proc whose group is the hook's ("pid (comm) state ppid pgrp ...").
|
|
53
58
|
hook_group_running() {
|
|
@@ -62,19 +67,27 @@ ${manifest.uninstall?.state ? wizardFlag("delete_state", ["pkgwizard_delete_stat
|
|
|
62
67
|
kill -"$1" "$hook_pid"
|
|
63
68
|
fi
|
|
64
69
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
hook_now() { if [ -r /proc/uptime ]; then awk '{print int($1)}' /proc/uptime; else date +%s; fi; }
|
|
71
|
+
hook_until=$(( $(hook_now) + ${purge.timeout} ))
|
|
72
|
+
hook_work_until=$((hook_until - ${reserve}))
|
|
73
|
+
while hook_running && [ "$(hook_now)" -lt "$hook_work_until" ]; do
|
|
74
|
+
sleep 0.1
|
|
69
75
|
done
|
|
70
76
|
hook_failure=
|
|
71
77
|
if hook_running; then
|
|
72
78
|
hook_signal TERM
|
|
73
|
-
|
|
79
|
+
hook_grace_until=$((hook_until - 1))
|
|
80
|
+
while hook_group_running && [ "$(hook_now)" -lt "$hook_grace_until" ]; do sleep 0.1; done
|
|
74
81
|
hook_signal KILL
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
while hook_running && [ "$(hook_now)" -lt "$hook_until" ]; do sleep 0.1; done
|
|
83
|
+
if hook_running || hook_group_running; then
|
|
84
|
+
echo cleanup-pending >"$hook_var/uninstall-hook.pending"
|
|
85
|
+
else
|
|
86
|
+
wait "$hook_pid" 2>/dev/null
|
|
87
|
+
fi
|
|
88
|
+
hook_failure="timeout after ${purge.timeout} s; cleanup may be pending"
|
|
77
89
|
else
|
|
90
|
+
rm -f "$hook_var/uninstall-hook.pending"
|
|
78
91
|
wait "$hook_pid"
|
|
79
92
|
hook_status=$?
|
|
80
93
|
[ "$hook_status" -eq 0 ] || hook_failure="exit $hook_status"
|
|
@@ -19,7 +19,7 @@ export interface UninstallPurge {
|
|
|
19
19
|
exec: string;
|
|
20
20
|
args: string[];
|
|
21
21
|
env: Record<string, string>;
|
|
22
|
-
/** Seconds, 2..
|
|
22
|
+
/** Seconds, 2..3600. */
|
|
23
23
|
timeout: number;
|
|
24
24
|
/** A root/<name>.sh the executor runs at uninstall, only while its approval is live. */
|
|
25
25
|
root?: string;
|
|
@@ -16,7 +16,7 @@ export function readUninstall(value, context, fail) {
|
|
|
16
16
|
if (value === undefined)
|
|
17
17
|
return undefined;
|
|
18
18
|
const invalid = () => {
|
|
19
|
-
fail({ code: "UNINSTALL_INVALID", message: "uninstall accepts only purge {exec, args, env, timeout, root} and state {items: [{key}]}; exec must be a fixed executable inside the package (not in executor/ or root/), and timeout is an integer from 2 to
|
|
19
|
+
fail({ code: "UNINSTALL_INVALID", message: "uninstall accepts only purge {exec, args, env, timeout, root} and state {items: [{key}]}; exec must be a fixed executable inside the package (not in executor/ or root/), and timeout is an integer from 2 to 3600 seconds." });
|
|
20
20
|
return undefined;
|
|
21
21
|
};
|
|
22
22
|
if (!record(value) || Object.keys(value).some(k => !FIELDS.has(k)))
|
|
@@ -54,7 +54,7 @@ function readPurge(value, context, fail) {
|
|
|
54
54
|
|| Object.entries(env).some(([k, v]) => !envKey(k) || k.startsWith("SYNOPKGLAND_") || !token(v)))
|
|
55
55
|
return "invalid";
|
|
56
56
|
const timeout = value.timeout ?? 60;
|
|
57
|
-
if (!Number.isInteger(timeout) || timeout < 2 || timeout >
|
|
57
|
+
if (!Number.isInteger(timeout) || timeout < 2 || timeout > 3600)
|
|
58
58
|
return "invalid";
|
|
59
59
|
const root = value.root;
|
|
60
60
|
if (root !== undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.5",
|
|
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.17.
|
|
30
|
+
"@synopackageland/templates": "0.17.8",
|
|
31
31
|
"ajv": "^8.17.1",
|
|
32
32
|
"commander": "^13.1.0",
|
|
33
33
|
"json-canonicalize": "^3.0.1",
|
|
@@ -39,6 +39,12 @@ scripts run as the package account; containers are managed by Container Manager.
|
|
|
39
39
|
|
|
40
40
|
Set `compose.startup_timeout_seconds: 600` for a slow cold start or migration.
|
|
41
41
|
Omitting it uses 120 seconds; only integers from 2 through 900 are accepted.
|
|
42
|
+
Lock waiting, status, readiness, stability and handoff share that deadline; there
|
|
43
|
+
is no extra fixed handoff allowance. The cap is a policy bound, not a measured
|
|
44
|
+
DSM lifecycle limit. `daemon: {timeouts: {stop: 120, status: 15, uninstall: 300}}`
|
|
45
|
+
configures the other lifecycle phases without declaring a daemon payload.
|
|
46
|
+
A transient health failure closes new ingress while the running App gets one
|
|
47
|
+
operation-budget recovery window; authority or generation failures stop at once.
|
|
42
48
|
Use a matching SDK that implements the field and rebuild the App; older planners
|
|
43
49
|
are rejected for explicit budgets. The SDK plan drives generated daemon start
|
|
44
50
|
and supplies every container with `SYNOPKGLAND_STARTUP_BUDGET_SECONDS`, overriding
|
|
@@ -386,3 +392,28 @@ Do not silently switch networking, add Docker socket access, run an App hook as
|
|
|
386
392
|
root or delete DSM configuration to claim compatibility.
|
|
387
393
|
|
|
388
394
|
Compose web health is required before check/build; init has no guessed endpoint or status. Select both `--health-path` and `--health-status`, or edit `compose.web.health`. Readiness does not follow redirects. Static check is not an HTTP or arbitrary-UID/storage preflight. For official-image upstream packaging and DSM login, read [upstream-fork.md](upstream-fork.md).
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
## Named volumes and uninstall
|
|
398
|
+
|
|
399
|
+
The SDK planner and lock inventory all current project-owned named volumes as
|
|
400
|
+
App data, with exact names. Compose alone never selects an executor or root
|
|
401
|
+
permissions. The package account does not delete named volumes or access Docker.
|
|
402
|
+
|
|
403
|
+
Before confirmation, the uninstall wizard states that container data volumes
|
|
404
|
+
are kept even when delete-data is selected, and lists the current exact names.
|
|
405
|
+
Only exact `SYNOPKG_PKG_STATUS=UNINSTALL` with an affirmative
|
|
406
|
+
`pkgwizard_remove_additional` (or supported mirror/legacy alias) clears the
|
|
407
|
+
package data/config/home contents the package account can clear. Upgrade, stop,
|
|
408
|
+
unknown status and keep preserve data. Unsafe paths and remnants are reported.
|
|
409
|
+
|
|
410
|
+
On delete-data uninstall with a nonempty volume inventory, generated postuninst
|
|
411
|
+
appends nine-language native text through `SYNOPKG_TEMP_LOGFILE`, preserving
|
|
412
|
+
existing text. It says plainly the listed volumes were NOT deleted and ends
|
|
413
|
+
with `COMPOSE_DATA_RETAINED`. The list is the current planner/lock inventory,
|
|
414
|
+
not a runtime existence check; volumes retired by older versions may also remain.
|
|
415
|
+
No full-data-deletion success is claimed and no unverified deletion path is given.
|
|
416
|
+
Administrator cleanup procedures, executor-tier Apps and storage redesign are
|
|
417
|
+
out of scope. Final DSM native UI acceptance remains required.
|
|
418
|
+
|
|
419
|
+
See the SDK's [Compose App data contract](https://github.com/synopackageland/syno-pkg-land-sdk/blob/main/docs/compose-app-data.md).
|
|
@@ -113,7 +113,7 @@ root:
|
|
|
113
113
|
migrate: {} # 沒有 run、job:只作為 start 步驟
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
start、stop 步驟與 `uninstall.purge.root` 都以 root、無 session 執行,只在管理員核准有效時跑;這些程式不能宣告 run 或 job(session 要用時以另一個名稱設定,可指向同一份內容)。start 步驟在新版本第一次啟動、executor 開 socket
|
|
116
|
+
start、stop 步驟與 `uninstall.purge.root` 都以 root、無 session 執行,只在管理員核准有效時跑;這些程式不能宣告 run 或 job(session 要用時以另一個名稱設定,可指向同一份內容)。start 步驟在新版本第一次啟動、executor 開 socket 並回讀核准任務之後執行,預設最多 60 秒;若宣告 lifecycle budget,則從核准的 start 剩餘預算扣除收尾餘裕;執行期間 executor 只回應 `status`(`state: "starting"`),其他請求回可重試的 `EXECUTOR_STARTING`,DSM 視窗的核准會等它結束,狀態讀取則立即回 `executor_starting`、由 SDK 面板自行重試,失敗只寫進 executor log,不擋啟動;同一版本不重跑,程式要能重複執行。結果由 executor `status` 的 `start_step` 讀取(`pending`、`ok`、`failed`,含 exit 與 output)。DSM 的 start 只寫入要求就返回,所以 `dsm-scripts/postinst`、`postupgrade`、`start` 看不到它的結果;root 的清理或遷移放 `root.start`,非特權的收尾放 `dsm-scripts/`。宣告 `root.start` 會改變 policy digest,需要管理員重新核准;沒有宣告的 App policy 不變。需要 SDK 0.10.3 以上。
|
|
117
117
|
|
|
118
118
|
schema 是 executor 的封閉子集(每個 object 都要 `additionalProperties: false`,key `[a-z0-9_]`,pattern 為 RE2)。這些設定需要支援它們的 SDK executor(`EXECUTOR_ROOT_PROGRAMS_UNSUPPORTED`)。preupgrade 呼叫 `executor/lifecycle stop --upgrade`;舊 SDK 的 helper 不認得此旗標時改用一般 stop。
|
|
119
119
|
|
|
@@ -108,3 +108,28 @@ A silent upgrade has no wizard values; use a default.
|
|
|
108
108
|
change they made.
|
|
109
109
|
- An upgrade runs the old package's preuninst and postuninst; check
|
|
110
110
|
`SYNOPKG_PKG_STATUS` before deleting anything.
|
|
111
|
+
|
|
112
|
+
## Work budgets
|
|
113
|
+
|
|
114
|
+
Use existing `daemon.timeouts` for start/stop/status/uninstall (defaults
|
|
115
|
+
30/30/5/60 seconds). Start/stop/uninstall accept 2–3600 seconds and status accepts
|
|
116
|
+
1–3600. A `daemon` containing only `timeouts` declares budgets without an
|
|
117
|
+
executable, including background and Compose Apps; Compose start still uses
|
|
118
|
+
`compose.startup_timeout_seconds` (120, 2–900). Background user-manager recovery
|
|
119
|
+
uses the start allowance and its generated package user unit uses the stop
|
|
120
|
+
allowance with `KillMode=control-group`.
|
|
121
|
+
|
|
122
|
+
`backend.timeout_seconds` owns request/operation work. Binary and Compose Apps
|
|
123
|
+
without a CGI request setting can enlarge the shared operation allowance through
|
|
124
|
+
`daemon.timeouts.status` above its 60-second default. Generated gateway and
|
|
125
|
+
approved executor configurations inherit it; nested calls use remaining time and
|
|
126
|
+
leave response reserve. Budget changes in the protected policy require renewed
|
|
127
|
+
approval and do not change token/session/grant lifetimes.
|
|
128
|
+
|
|
129
|
+
`uninstall.purge.timeout` accepts 2–3600 seconds and includes termination reserve.
|
|
130
|
+
A timeout is logged and may leave `uninstall-hook.pending`; the uninstall may
|
|
131
|
+
continue. The executor helper's JSON state distinguishes `accepted`,
|
|
132
|
+
`cleanup-complete` and `cleanup-pending`. An exit of zero acknowledges the retained
|
|
133
|
+
request and does not prove cleanup finished. Unconfirmed cleanup retains protected
|
|
134
|
+
state for a later retry. DSM's outer install/uninstall deadline is not established
|
|
135
|
+
by these settings.
|