@synopackageland/cli 0.23.0 → 0.23.1
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 +5 -0
- package/dist/runtime/executor-digests.json +11 -0
- package/dist/runtime/sdk-source.js +1 -1
- package/dist/standalone/inspect-spk.js +28 -23
- package/dist/standalone/lifecycle.d.ts +2 -0
- package/dist/standalone/lifecycle.js +13 -2
- package/dist/standalone/native-gateway.d.ts +4 -0
- package/dist/standalone/native-gateway.js +86 -0
- package/package.json +1 -1
- package/skills/reference/diagnose.md +48 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.23.1 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
- Build SDK/App 0.23.1 with its verified immutable executor/v0.20.0 digests; retain support for SDK/App 0.23.0. This enables the SDK HTTP transfer error-status fix without changing executor or lifecycle payloads.
|
|
6
|
+
- Include the native HTTPS shutdown/start port-release fix and recognize its exact diagnostic prefix when inspecting Portal and background-job lifecycle scripts. Keep templates 0.23.0 and the existing UI pin.
|
|
7
|
+
|
|
3
8
|
## 0.23.0 (2026-10-21)
|
|
4
9
|
|
|
5
10
|
- Validate the explicit OIDC `browser_redirect` contract before packaging and pin templates/SDK 0.23.0.
|
|
@@ -9,5 +9,16 @@
|
|
|
9
9
|
"armv7": "256fb348b275da7ac5728a3cb7cb7c65660de0d5c2c59430ab4f5ade1b0a3681"
|
|
10
10
|
},
|
|
11
11
|
"goToolchain": "go1.26.9"
|
|
12
|
+
},
|
|
13
|
+
"0.23.1": {
|
|
14
|
+
"x86_64": "e27cfeb85fed221b06f9c58166f02c0f701a724308cd17c96a43a843a0b5c4aa",
|
|
15
|
+
"aarch64": "908db00cbd37d072f6ffc339e7bca24da5efb8cddd4befcbad02c7398beb7191",
|
|
16
|
+
"armv7": "a0f35d62ec80a1bf3fb49f5e2d1a5f7576eda569a6fd0695084cec6cb8e100bc",
|
|
17
|
+
"lifecycle": {
|
|
18
|
+
"x86_64": "9bb53301670b43cdcd6e21cebac99ab7abd82962d874f32423a81e6cb49d4972",
|
|
19
|
+
"aarch64": "38fe873c86eb102c850baa1f304114a83a31d1df72defd22792bc37610fec9bb",
|
|
20
|
+
"armv7": "256fb348b275da7ac5728a3cb7cb7c65660de0d5c2c59430ab4f5ade1b0a3681"
|
|
21
|
+
},
|
|
22
|
+
"goToolchain": "go1.26.9"
|
|
12
23
|
}
|
|
13
24
|
}
|
|
@@ -76,7 +76,7 @@ function goMod(backendDir) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
/** SDK releases that reuse an immutable executor release with identical payload bytes. */
|
|
79
|
-
const EXECUTOR_RELEASES = { '0.21.0': '0.20.0', '0.21.1': '0.20.0', '0.22.0': '0.20.0', '0.23.0': '0.20.0' };
|
|
79
|
+
const EXECUTOR_RELEASES = { '0.21.0': '0.20.0', '0.21.1': '0.20.0', '0.22.0': '0.20.0', '0.23.0': '0.20.0', '0.23.1': '0.20.0' };
|
|
80
80
|
/**
|
|
81
81
|
* The module directories of one SDK version. SYNOPKGLAND_SDK_DIR names a
|
|
82
82
|
* local sdk checkout (development and gates only); otherwise modules come from
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readInstallSettings, settingsInstallSteps, settingsConfig, renderSettingsHook } from './install-settings.js';
|
|
2
2
|
import { nativePortal } from "./portal.js";
|
|
3
|
-
import { nativeGatewaySupported, renderNativeGatewayConfig, renderNativeGatewayPrepare, NATIVE_GATEWAY_CONFIG, gatewayEntryPath } from './native-gateway.js';
|
|
3
|
+
import { nativeGatewaySupported, renderNativeGatewayConfig, renderNativeGatewayDiagnostic, renderNativeGatewayPrepare, NATIVE_GATEWAY_CONFIG, gatewayEntryPath } from './native-gateway.js';
|
|
4
4
|
import { inspectPortalInfo } from "../portal/info.js";
|
|
5
5
|
import { isDeepStrictEqual } from "node:util";
|
|
6
6
|
import { inspectApiDescription } from "./api-description-inspect.js";
|
|
@@ -36,7 +36,7 @@ import { renderNativePrivilege } from "../build-spk/native-lifecycle.js";
|
|
|
36
36
|
import { renderUiConfig } from "./dsm-window.js";
|
|
37
37
|
import { identityGatewayConfig, gatewayAdmissionReport, gatewayAdmissionPolicy, readIdentityGateway, } from "./identity-gateway.js";
|
|
38
38
|
import { checkCgiBinary } from "./binary.js";
|
|
39
|
-
import { renderStandaloneScript } from "./lifecycle.js";
|
|
39
|
+
import { matchesStandaloneScript, renderStandaloneScript } from "./lifecycle.js";
|
|
40
40
|
import { listPortExposures, loadCompose } from "../compose/analyze.js";
|
|
41
41
|
import { uninstallCleanup, } from "../runtime/uninstall-cleanup.js";
|
|
42
42
|
import { PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH, renderPortalJson, renderPortalLifecycle, renderPortalMustache, renderPortalWizardStep, } from "../portal/package.js";
|
|
@@ -209,7 +209,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
209
209
|
? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined, nativeGateway, uninstallHook?.uninstall)
|
|
210
210
|
: undefined;
|
|
211
211
|
if (background)
|
|
212
|
-
checkBackground(dir, info, payload, findings, hasExecutor, daemon, kind !== "native-go", metadata.lifecycleTimeouts);
|
|
212
|
+
checkBackground(dir, info, payload, findings, hasExecutor, daemon, kind !== "native-go", metadata.lifecycleTimeouts, nativeGateway);
|
|
213
213
|
let executor;
|
|
214
214
|
if ((metadata.capabilities ?? []).some((name) => name === "rootOps" || name === "delegate" || name === "webapi") ||
|
|
215
215
|
paths.has("executor/executor")) {
|
|
@@ -701,8 +701,7 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
701
701
|
"postupgrade",
|
|
702
702
|
"start-stop-status",
|
|
703
703
|
])
|
|
704
|
-
if (readOptional(join(dir, "scripts", hook))
|
|
705
|
-
renderStandaloneScript(hook, manifest))
|
|
704
|
+
if (!matchesStandaloneScript(hook, manifest, readOptional(join(dir, "scripts", hook))))
|
|
706
705
|
throw new Error(`unexpected scripts/${hook}`);
|
|
707
706
|
if (appScripts.length)
|
|
708
707
|
base.appScripts = appScripts;
|
|
@@ -897,8 +896,7 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
897
896
|
if (readFileSync(join(unpacked, "daemon/config.json"), "utf8") !== JSON.stringify(expectedConfig) + '\n')
|
|
898
897
|
throw new Error("non-canonical daemon config");
|
|
899
898
|
for (const hook of ["start-stop-status", "preupgrade", "preuninst"]) {
|
|
900
|
-
if (readFileSync(join(dir, "scripts", hook), "utf8")
|
|
901
|
-
renderStandaloneScript(hook, nativeGateway ? { ...manifest, os_min_ver: info.os_min_ver, identityGateway: nativeGateway } : manifest))
|
|
899
|
+
if (!matchesStandaloneScript(hook, nativeGateway ? { ...manifest, os_min_ver: info.os_min_ver, identityGateway: nativeGateway } : manifest, readFileSync(join(dir, "scripts", hook), "utf8")))
|
|
902
900
|
findings.push(`daemon scripts/${hook} does not match the fixed lifecycle`);
|
|
903
901
|
}
|
|
904
902
|
return daemon;
|
|
@@ -1120,7 +1118,7 @@ function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal, c
|
|
|
1120
1118
|
}
|
|
1121
1119
|
}
|
|
1122
1120
|
/** The opt-in package user unit must be exactly what the generator writes. */
|
|
1123
|
-
function checkBackground(dir, info, payload, findings, executor, daemon, bridge = false, lifecycleTimeouts) {
|
|
1121
|
+
function checkBackground(dir, info, payload, findings, executor, daemon, bridge = false, lifecycleTimeouts, nativeGateway) {
|
|
1124
1122
|
const pkg = info.package;
|
|
1125
1123
|
if (!pkg)
|
|
1126
1124
|
return;
|
|
@@ -1141,16 +1139,17 @@ function checkBackground(dir, info, payload, findings, executor, daemon, bridge
|
|
|
1141
1139
|
renderBackgroundUnit(pkg, bridge ? BRIDGE_WORKER_ARGS : [], daemon?.timeouts.stop ?? lifecycleTimeouts?.stop ?? 30)) {
|
|
1142
1140
|
findings.push(`${BACKGROUND_UNIT_DIR} must contain only the fixed ${unit} (runs as the package account, no User=, no root)`);
|
|
1143
1141
|
}
|
|
1144
|
-
if (
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
},
|
|
1153
|
-
}
|
|
1142
|
+
if (!matchesStandaloneScript("start-stop-status", {
|
|
1143
|
+
package: pkg,
|
|
1144
|
+
os_min_ver: info.os_min_ver,
|
|
1145
|
+
...(nativeGateway ? { identityGateway: nativeGateway } : {}),
|
|
1146
|
+
lifecycleTimeouts,
|
|
1147
|
+
...(daemon ? { daemon } : {}),
|
|
1148
|
+
capabilities: {
|
|
1149
|
+
jobs: { worker: "sha256-v1" },
|
|
1150
|
+
...(executor ? { rootOps: true } : {}),
|
|
1151
|
+
},
|
|
1152
|
+
}, read("scripts/start-stop-status")))
|
|
1154
1153
|
findings.push("background scripts/start-stop-status does not match the fixed synosystemctl lifecycle");
|
|
1155
1154
|
}
|
|
1156
1155
|
/**
|
|
@@ -1204,16 +1203,14 @@ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal,
|
|
|
1204
1203
|
findings.push(`WIZARD_UIFILES/${name} is not a wizard file produced by ${source}`);
|
|
1205
1204
|
if (selected) {
|
|
1206
1205
|
for (const hook of ["preinst", "postinst"]) {
|
|
1207
|
-
if (readOptional(join(dir, "scripts", hook))
|
|
1208
|
-
renderStandaloneScript(hook, lifecycleManifest))
|
|
1206
|
+
if (!matchesStandaloneScript(hook, lifecycleManifest, readOptional(join(dir, "scripts", hook))))
|
|
1209
1207
|
findings.push(`scripts/${hook} does not match the fixed install-selected draft handling`);
|
|
1210
1208
|
}
|
|
1211
1209
|
}
|
|
1212
1210
|
if (!folder && !portal)
|
|
1213
1211
|
return;
|
|
1214
1212
|
for (const hook of ["postinst", "postupgrade"]) {
|
|
1215
|
-
if (readOptional(join(dir, "scripts", hook))
|
|
1216
|
-
renderStandaloneScript(hook, lifecycleManifest)) {
|
|
1213
|
+
if (!matchesStandaloneScript(hook, lifecycleManifest, readOptional(join(dir, "scripts", hook)))) {
|
|
1217
1214
|
findings.push(`scripts/${hook} does not match the fixed script declared by ${folder ? "install.sharedFolder" : "portal"}`);
|
|
1218
1215
|
}
|
|
1219
1216
|
}
|
|
@@ -1313,7 +1310,15 @@ async function recoverPortal(dir, info, appId, paths, payload, findings, nativeG
|
|
|
1313
1310
|
identityGateway: nativeGateway,
|
|
1314
1311
|
})
|
|
1315
1312
|
: "";
|
|
1316
|
-
|
|
1313
|
+
const diagnostic = renderNativeGatewayDiagnostic({
|
|
1314
|
+
package: info.package,
|
|
1315
|
+
os_min_ver: info.os_min_ver,
|
|
1316
|
+
identityGateway: nativeGateway,
|
|
1317
|
+
}, hook);
|
|
1318
|
+
const script = readOptional(join(dir, "scripts", hook));
|
|
1319
|
+
// The fixed native diagnostics precede preparation in current scripts;
|
|
1320
|
+
// released scripts can still start directly with preparation.
|
|
1321
|
+
if (![diagnostic, ""].some(prefix => script?.startsWith(`#!/bin/sh\n${prefix}${prepare}${renderPortalLifecycle(hook, portal)}`)))
|
|
1317
1322
|
throw new Error(hook);
|
|
1318
1323
|
}
|
|
1319
1324
|
return portal;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { StandaloneManifest } from "./manifest.js";
|
|
2
2
|
/** Package-account lifecycle hooks for CGI, daemon and background packages. */
|
|
3
3
|
export declare function renderStandaloneScript(name: string, manifest: StandaloneManifest): string;
|
|
4
|
+
/** Accept the exact current, diagnostic-only, and released lifecycle variants. */
|
|
5
|
+
export declare function matchesStandaloneScript(name: string, manifest: StandaloneManifest, actual: string | undefined): boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { renderSettingsHook } from './install-settings.js';
|
|
2
2
|
import { renderComposeRetainedData } from "../build-spk/compose-retained-data.js";
|
|
3
|
-
import { renderNativeGatewayPrepare } from './native-gateway.js';
|
|
3
|
+
import { renderNativeGatewayDiagnostic, renderNativeGatewayPortWait, renderNativeGatewayPrepare } from './native-gateway.js';
|
|
4
4
|
import { renderComposeDiagnosis } from "../build-spk/compose-diagnosis.js";
|
|
5
5
|
import { composeDaemon, renderComposeInstall } from "./compose.js";
|
|
6
6
|
import { renderBackgroundControl, renderBackgroundWorkerStart, renderBackgroundWorkerStop, renderUserManagerRecovery, backgroundUnitName, } from "./background.js";
|
|
@@ -16,12 +16,23 @@ const UNINSTALL_ONLY = '[ "${SYNOPKG_PKG_STATUS:-}" = UPGRADE ] || synopkgland_u
|
|
|
16
16
|
export function renderStandaloneScript(name, manifest) {
|
|
17
17
|
const script = renderStandaloneScriptInternal(name, manifest);
|
|
18
18
|
const prepared = ['postinst', 'postupgrade'].includes(name) ? renderNativeGatewayPrepare(manifest) : '';
|
|
19
|
-
const prefix = renderSettingsHook(name, manifest) + prepared;
|
|
19
|
+
const prefix = renderNativeGatewayDiagnostic(manifest, name) + renderNativeGatewayPortWait(manifest, name) + renderSettingsHook(name, manifest) + prepared;
|
|
20
20
|
const sdk = prefix ? script.replace('#!/bin/sh\n', () => '#!/bin/sh\n' + prefix) : script;
|
|
21
21
|
return manifest.appScripts?.some((app) => app === name)
|
|
22
22
|
? composeAppScript(name, sdk)
|
|
23
23
|
: sdk;
|
|
24
24
|
}
|
|
25
|
+
/** Accept the exact current, diagnostic-only, and released lifecycle variants. */
|
|
26
|
+
export function matchesStandaloneScript(name, manifest, actual) {
|
|
27
|
+
const expected = renderStandaloneScript(name, manifest);
|
|
28
|
+
if (actual === expected)
|
|
29
|
+
return true;
|
|
30
|
+
const diagnostic = renderNativeGatewayDiagnostic(manifest, name);
|
|
31
|
+
const wait = renderNativeGatewayPortWait(manifest, name);
|
|
32
|
+
const diagnosticOnly = wait ? expected.replace(wait, '') : expected;
|
|
33
|
+
return (wait !== '' && actual === diagnosticOnly) ||
|
|
34
|
+
(diagnostic !== '' && actual === diagnosticOnly.replace(diagnostic, ''));
|
|
35
|
+
}
|
|
25
36
|
function renderStandaloneScriptInternal(name, manifest) {
|
|
26
37
|
if (manifest.compose) {
|
|
27
38
|
const native = {
|
|
@@ -8,6 +8,10 @@ type Target = {
|
|
|
8
8
|
export declare const NATIVE_GATEWAY_CONFIG = "sso/native-nginx.conf";
|
|
9
9
|
export declare function nativeGatewaySdk(sdk: string): boolean;
|
|
10
10
|
export declare function nativeGatewaySupported(m: Target): boolean;
|
|
11
|
+
/** Observe the boundary before DSM acquires web-config, without claiming port ownership. */
|
|
12
|
+
export declare function renderNativeGatewayDiagnostic(m: Target, hook: string): string;
|
|
13
|
+
/** Give a retiring nginx listener a bounded window before DSM acquires web-config. */
|
|
14
|
+
export declare function renderNativeGatewayPortWait(m: Target, hook: string): string;
|
|
11
15
|
export declare function gatewayEntryPath(m: Target): string;
|
|
12
16
|
export declare function legacyGatewayNotice(): string;
|
|
13
17
|
export declare function nativeGatewayResource(m: Target): Record<string, unknown> | undefined;
|
|
@@ -10,6 +10,92 @@ export function nativeGatewaySupported(m) {
|
|
|
10
10
|
return Boolean(m.identityGateway && nativeGatewaySdk(m.identityGateway.sdk)
|
|
11
11
|
&& m.os_min_ver && isDsmAtLeast(m.os_min_ver, PORTAL_MIN_DSM));
|
|
12
12
|
}
|
|
13
|
+
/** Observe the boundary before DSM acquires web-config, without claiming port ownership. */
|
|
14
|
+
export function renderNativeGatewayDiagnostic(m, hook) {
|
|
15
|
+
if (!nativeGatewaySupported(m) || !['start-stop-status', 'postinst', 'postupgrade', 'preupgrade', 'preuninst'].includes(hook))
|
|
16
|
+
return '';
|
|
17
|
+
const ports = [...new Set([m.identityGateway.listen.https, m.identityGateway.listen.http].filter((port) => port !== undefined))];
|
|
18
|
+
const action = hook === 'start-stop-status'
|
|
19
|
+
? ` case "\${1:-}" in prestart|prestop|start|stop|restart) action="$1" ;; *) exit 0 ;; esac\n`
|
|
20
|
+
: ' action=hook\n';
|
|
21
|
+
// Only fixed phase names and declared public ports reach syslog. In particular,
|
|
22
|
+
// never log argv, environment dumps, request/session data or a guessed owner.
|
|
23
|
+
return `synopkgland_native_diagnostic() (
|
|
24
|
+
command -v logger >/dev/null 2>&1 || exit 0
|
|
25
|
+
${action} case "\${SYNOPKG_PKG_STATUS:-}" in
|
|
26
|
+
INSTALL|UPGRADE|UNINSTALL|START|STOP) reason="$SYNOPKG_PKG_STATUS" ;;
|
|
27
|
+
'') reason=NONE ;;
|
|
28
|
+
*) reason=UNKNOWN ;;
|
|
29
|
+
esac
|
|
30
|
+
stamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ') || stamp=unknown
|
|
31
|
+
socket_state() {
|
|
32
|
+
if [ ! -r "$1" ]; then echo unknown; return; fi
|
|
33
|
+
awk -v port="$2" '$4 == "0A" && toupper($2) ~ (":" port "$") { found=1 } END { print found ? "listening" : "clear" }' "$1" || echo unknown
|
|
34
|
+
}
|
|
35
|
+
${ports.map(port => ` tcp4=$(socket_state /proc/net/tcp ${port.toString(16).toUpperCase().padStart(4, '0')})
|
|
36
|
+
tcp6=$(socket_state /proc/net/tcp6 ${port.toString(16).toUpperCase().padStart(4, '0')})
|
|
37
|
+
logger -p user.warning -t synopkgland "NATIVE_GATEWAY_LIFECYCLE at=$stamp package=${m.package} hook=${hook} action=$action reason=$reason port=${port} tcp4=$tcp4 tcp6=$tcp6" || :
|
|
38
|
+
`).join('')})
|
|
39
|
+
synopkgland_native_diagnostic "$@" >/dev/null 2>&1 || :
|
|
40
|
+
`;
|
|
41
|
+
}
|
|
42
|
+
/** Give a retiring nginx listener a bounded window before DSM acquires web-config. */
|
|
43
|
+
export function renderNativeGatewayPortWait(m, hook) {
|
|
44
|
+
if (hook !== 'start-stop-status' || !nativeGatewaySupported(m))
|
|
45
|
+
return '';
|
|
46
|
+
const ports = [...new Set([m.identityGateway.listen.https, m.identityGateway.listen.http].filter((port) => port !== undefined))];
|
|
47
|
+
return `synopkgland_native_port_wait() (
|
|
48
|
+
[ "\${1:-}" = prestart ] || exit 0
|
|
49
|
+
socket_state() {
|
|
50
|
+
if [ ! -r "$1" ]; then echo unknown; return; fi
|
|
51
|
+
awk -v port="$2" '$4 == "0A" && toupper($2) ~ (":" port "$") { found=1 } END { print found ? "listening" : "clear" }' "$1" || echo unknown
|
|
52
|
+
}
|
|
53
|
+
remaining=30
|
|
54
|
+
polls=0
|
|
55
|
+
monotonic_cs() {
|
|
56
|
+
read uptime unused < /proc/uptime || return 1
|
|
57
|
+
whole=\${uptime%.*}
|
|
58
|
+
fraction=\${uptime#*.}
|
|
59
|
+
case "$whole" in ''|*[!0-9]*) return 1 ;; esac
|
|
60
|
+
case "$fraction" in [0-9][0-9]) : ;; *) return 1 ;; esac
|
|
61
|
+
echo $((whole * 100 + 1$fraction - 100))
|
|
62
|
+
}
|
|
63
|
+
started=$(monotonic_cs) || started=
|
|
64
|
+
deadline=$((\${started:-0} + 1500))
|
|
65
|
+
while :; do
|
|
66
|
+
busy=0
|
|
67
|
+
unknown=0
|
|
68
|
+
for port in ${ports.map(port => port.toString(16).toUpperCase().padStart(4, '0')).join(' ')}; do
|
|
69
|
+
for table in /proc/net/tcp /proc/net/tcp6; do
|
|
70
|
+
case "$(socket_state "$table" "$port")" in
|
|
71
|
+
listening) busy=1 ;;
|
|
72
|
+
clear) : ;;
|
|
73
|
+
*) unknown=1 ;;
|
|
74
|
+
esac
|
|
75
|
+
done
|
|
76
|
+
done
|
|
77
|
+
if [ "$busy" -eq 0 ]; then
|
|
78
|
+
outcome=clear
|
|
79
|
+
[ "$unknown" -eq 0 ] || outcome=unknown
|
|
80
|
+
break
|
|
81
|
+
fi
|
|
82
|
+
if [ "$remaining" -eq 0 ]; then outcome=timeout; break; fi
|
|
83
|
+
now=$(monotonic_cs) || now=
|
|
84
|
+
if [ -z "$started" ] || [ -z "$now" ]; then outcome=unavailable; break; fi
|
|
85
|
+
left=$((deadline - now))
|
|
86
|
+
if [ "$left" -le 0 ]; then outcome=timeout; break; fi
|
|
87
|
+
delay=0.5
|
|
88
|
+
if [ "$left" -lt 50 ]; then delay=$(printf '0.%02d' "$left"); fi
|
|
89
|
+
sleep "$delay" || { outcome=unavailable; break; }
|
|
90
|
+
remaining=$((remaining - 1))
|
|
91
|
+
polls=$((polls + 1))
|
|
92
|
+
done
|
|
93
|
+
stamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ') || stamp=unknown
|
|
94
|
+
logger -p user.warning -t synopkgland "NATIVE_GATEWAY_PORT_WAIT at=$stamp package=${m.package} action=prestart ports=${ports.join(',')} outcome=$outcome polls=$polls" || :
|
|
95
|
+
)
|
|
96
|
+
synopkgland_native_port_wait "$@" >/dev/null 2>&1 || :
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
13
99
|
export function gatewayEntryPath(m) {
|
|
14
100
|
return `/webman/3rdparty/${m.package}/sso/open.${m.identityGateway && !nativeGatewaySdk(m.identityGateway.sdk) ? 'html' : 'cgi'}`;
|
|
15
101
|
}
|
package/package.json
CHANGED
|
@@ -5,3 +5,51 @@ Start with check; inspect the built SPK. For DSM broken / 150 inspect Package Ce
|
|
|
5
5
|
The independent container path uses resource-worker lifecycle. Unsupported Compose settings fail before build; there is no alternate installer. COMPOSE_NUMERIC_COMMAND_ARG_FORBIDDEN means DSM may strip quoting from 純數字 command 元素. Use a single shell command when appropriate, for example `command: ["sh","-c","httpd -f -p 18333 -h /srv"]`.
|
|
6
6
|
|
|
7
7
|
Never work around a failed permission check by running the package as root. Native CGI authentication and root-operation administrator approval are separate checks.
|
|
8
|
+
|
|
9
|
+
## Native HTTPS fails before the App starts
|
|
10
|
+
|
|
11
|
+
For a package using DSM-managed native gateway HTTPS (including Compose Apps),
|
|
12
|
+
`start_failed` can occur after `start-stop-status prestart` succeeds but before
|
|
13
|
+
DSM invokes `start`. Check `/var/log/packages/<package>.log`,
|
|
14
|
+
`/var/log/synopkg.log`, `/var/log/synopkgmgr.log` and `/var/log/messages` at the
|
|
15
|
+
same timestamp. `Failed to acquire startup worker` (272) plus
|
|
16
|
+
`Runtime port <port> conflict for nginx` identifies the DSM `web-config` resource
|
|
17
|
+
boundary. Increasing the daemon or Compose startup timeout cannot repair a
|
|
18
|
+
failure that occurs before the daemon controller runs.
|
|
19
|
+
|
|
20
|
+
New builds emit `NATIVE_GATEWAY_LIFECYCLE` records to local syslog at SDK hook entry
|
|
21
|
+
for prestart, prestop, start, stop, restart and the install/upgrade/removal hooks.
|
|
22
|
+
The record contains a UTC timestamp, package, fixed hook/action/reason, declared
|
|
23
|
+
public port, and IPv4/IPv6 TCP listener presence from `/proc/net/tcp{,6}`:
|
|
24
|
+
`listening`, `clear`, or `unknown`. It records no usernames, environment dumps,
|
|
25
|
+
credentials or application arguments. Records use `user.warning` because DSM
|
|
26
|
+
may filter notice-level messages; these records are observations, not error verdicts. Status polls are silent. Missing logger or
|
|
27
|
+
unreadable proc data does not change the lifecycle result.
|
|
28
|
+
|
|
29
|
+
Before the existing SDK and App prestart steps, native gateway builds also
|
|
30
|
+
check all declared public ports for IPv4/IPv6 TCP listeners. While any is
|
|
31
|
+
listening, one shared monotonic 15-second deadline includes probe time, with at
|
|
32
|
+
most thirty sleeps of up to 0.5 seconds. The final sleep uses only the remaining
|
|
33
|
+
budget; scheduling and the final local probe can add slight overhead. No listener
|
|
34
|
+
means no sleep. Unknown
|
|
35
|
+
probe results do not themselves trigger a wait. `NATIVE_GATEWAY_PORT_WAIT`
|
|
36
|
+
records the outcome (`clear`, `timeout`, `unknown`, or `unavailable`) and the
|
|
37
|
+
number of sleeps. Timeout, logger failure, or an unavailable sleep command
|
|
38
|
+
continues to the original prestart steps and preserves their exit semantics;
|
|
39
|
+
DSM remains responsible for acquiring the resource and reporting a conflict.
|
|
40
|
+
This is an observation-only mitigation, without signalling processes or a root
|
|
41
|
+
helper. Inspect accepts the exact current scripts, the prior diagnostic-only
|
|
42
|
+
scripts, and the original released scripts; edited waits or SDK steps fail.
|
|
43
|
+
|
|
44
|
+
These observations do not identify the listener's owner or reserve the port.
|
|
45
|
+
They do not prove that a listener persisted until DSM's subsequent resource
|
|
46
|
+
check, and a missing record may mean logging was unavailable. Correlate them
|
|
47
|
+
with DSM's own hook begin/end records. During an authorized reproduction, also
|
|
48
|
+
capture a timestamped `netstat -lntp` (administrator access may be required),
|
|
49
|
+
nginx process/config state, package state and daemon logs before attempting
|
|
50
|
+
recovery. Keep system logs private and redact them before sharing. A later
|
|
51
|
+
native Start success proves recovery, not why the previous attempt failed.
|
|
52
|
+
|
|
53
|
+
Do not restart nginx, change DSM port assignments, kill unrelated listeners,
|
|
54
|
+
or add automatic package-start retries to hide an unproven conflict. Rebuild
|
|
55
|
+
an App to obtain the diagnostic hooks; existing installed packages are unchanged.
|