openship 0.6.1 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/dist/{bare-IWS2QHJ3.js → bare-OXJVW3AK.js} +11 -9
- package/dist/{chunk-VZ2ZHPS2.js → chunk-2YM3NI2E.js} +3 -9
- package/dist/chunk-4I2BM2DT.js +68 -0
- package/dist/{chunk-2NZSUXOB.js → chunk-5DMWSTOM.js} +310 -1459
- package/dist/{chunk-3FYIXVX5.js → chunk-77QDF6UD.js} +581 -122
- package/dist/{chunk-IOT5ZX23.js → chunk-A4GXYE4L.js} +449 -383
- package/dist/chunk-CBLD2HS5.js +428 -0
- package/dist/chunk-ELGRMZOW.js +25 -0
- package/dist/{chunk-TWJCLD4W.js → chunk-FE7ZZQSE.js} +5 -5
- package/dist/{chunk-3NZICQHF.js → chunk-HKRQXPTI.js} +31 -8
- package/dist/{chunk-IXM5245F.js → chunk-LFE5KQ7Y.js} +553 -538
- package/dist/{chunk-VDDMADOF.js → chunk-LH2PLRQO.js} +16 -2
- package/dist/{chunk-EMXI4UCU.js → chunk-LUKYPVJ4.js} +29 -10
- package/dist/{chunk-UC7YIEHT.js → chunk-MLYSEZQB.js} +352 -47
- package/dist/chunk-NK5N5LXN.js +1681 -0
- package/dist/{chunk-RRJJGVKE.js → chunk-OBPKYXQL.js} +75 -17
- package/dist/{chunk-6ODUBKUB.js → chunk-PCPVTH6M.js} +78 -88
- package/dist/chunk-Q7WGTQQ5.js +88 -0
- package/dist/{chunk-EW6KB2J7.js → chunk-QVW76WDL.js} +440 -74
- package/dist/{chunk-YIPMDGG6.js → chunk-RSR7PFIQ.js} +79 -23
- package/dist/{chunk-3LDWWY7U.js → chunk-SM26HDS2.js} +41 -7
- package/dist/{chunk-OTLSJYAC.js → chunk-UJ6JZM65.js} +5185 -341
- package/dist/chunk-UVNMQM7W.js +444 -0
- package/dist/chunk-VTGPEC6B.js +308 -0
- package/dist/chunk-WVAML4H6.js +2145 -0
- package/dist/{chunk-JIMUYEKN.js → chunk-YIMOCZBZ.js} +1 -1
- package/dist/chunk-YMMQRE5J.js +292 -0
- package/dist/cloud-75VIT4KV.js +37 -0
- package/dist/{detect-ALF4GXRX.js → detect-OIE5V3VZ.js} +3 -3
- package/dist/{docker-QECQJ5P4.js → docker-VTR2GK5Y.js} +16 -8
- package/dist/{docker-edge-executor-62N64MGS.js → docker-edge-executor-22IBXNYJ.js} +5 -5
- package/dist/{edge-import-SAYTRJLO.js → edge-import-WJUB4M7F.js} +27 -23
- package/dist/edge-preflight-GBNCPNVU.js +42 -0
- package/dist/{ensure-container-edge-CFSCPGZK.js → ensure-container-edge-KPI2CVXJ.js} +9 -8
- package/dist/{executor-PT6UEJ5M.js → executor-7LXXZPBR.js} +15 -5
- package/dist/host-channel-preflight-KE6OONYK.js +50 -0
- package/dist/index.js +3783 -4221
- package/dist/{nginx-YOEEFBTE.js → nginx-KUFMLMAC.js} +13 -5
- package/dist/node-bootstrap.js +241 -0
- package/dist/node-entry.js +52 -1
- package/dist/{openresty-lua-WGBO2M3Z.js → openresty-lua-ATTH35BE.js} +8 -2
- package/dist/privilege-I4W3ZNZK.js +19 -0
- package/dist/server/engine/samples/amavisd/amavisd.conf +25 -0
- package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +4 -1
- package/dist/server/engine/samples/postfix/master.cf +14 -0
- package/dist/server/index.js +77659 -61909
- package/dist/server/migrations/0098_instance_product_mode.sql +12 -0
- package/dist/server/migrations/0099_mail_webmail_project.sql +24 -0
- package/dist/server/migrations/0100_server_ssh_private_key.sql +11 -0
- package/dist/server/migrations/0101_mail_resume_step.sql +12 -0
- package/dist/server/migrations/0102_project_workload_axes.sql +19 -0
- package/dist/server/migrations/0103_dns_credential.sql +41 -0
- package/dist/server/migrations/0104_instance_host_control.sql +15 -0
- package/dist/server/migrations/0105_mail_inbound_rules.sql +80 -0
- package/dist/server/migrations/0106_mcp_call_tracking.sql +23 -0
- package/dist/server/migrations/0107_build_minute_metering_indexes.sql +20 -0
- package/dist/server/migrations/0108_credential.sql +68 -0
- package/dist/server/migrations/meta/_journal.json +77 -0
- package/dist/{setup-M2ZDGH3O.js → setup-XGLGUUFW.js} +10 -9
- package/dist/ssh-client-JBMA7SN4.js +28 -0
- package/dist/up-K4BV7466.js +57 -0
- package/package.json +4 -2
- package/dist/chunk-IW2UKBVF.js +0 -144
- package/dist/chunk-ZCFVHA25.js +0 -89
- package/dist/cloud-M2YLWEXO.js +0 -31
- /package/dist/{dist-es-JGBPABJI.js → dist-es-AXFRS7PS.js} +0 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
|
|
3
|
+
import { createRequire as __ospCreateRequire } from "node:module";
|
|
4
|
+
const require = __ospCreateRequire(import.meta.url);
|
|
5
|
+
import {
|
|
6
|
+
sq
|
|
7
|
+
} from "./chunk-ZFJXPCQP.js";
|
|
8
|
+
import {
|
|
9
|
+
assessHostSupport,
|
|
10
|
+
classifyDistro,
|
|
11
|
+
formatDuration,
|
|
12
|
+
isRemoteConnectionError,
|
|
13
|
+
parseOsRelease,
|
|
14
|
+
parseSystemArch,
|
|
15
|
+
parseSystemOs,
|
|
16
|
+
safeErrorMessage,
|
|
17
|
+
systemDebug
|
|
18
|
+
} from "./chunk-UJ6JZM65.js";
|
|
19
|
+
|
|
20
|
+
// ../../packages/adapters/src/system/elevated-executor.ts
|
|
21
|
+
import { randomBytes } from "crypto";
|
|
22
|
+
var INSTALL_ENV = "DEBIAN_FRONTEND=noninteractive DPKG_FORCE=confnew";
|
|
23
|
+
function elevateCommand(command) {
|
|
24
|
+
return `sudo -n sh -c ${sq(`export ${INSTALL_ENV}; ${command}`)}`;
|
|
25
|
+
}
|
|
26
|
+
var EXECUTOR_DELEGATE = /* @__PURE__ */ Symbol.for("openship.executor.delegate");
|
|
27
|
+
function dirOf(path) {
|
|
28
|
+
const idx = path.lastIndexOf("/");
|
|
29
|
+
if (idx < 0) return ".";
|
|
30
|
+
return idx === 0 ? "/" : path.slice(0, idx);
|
|
31
|
+
}
|
|
32
|
+
function elevatedExecutor(inner) {
|
|
33
|
+
const readFileElevated = async (path) => {
|
|
34
|
+
try {
|
|
35
|
+
return await inner.readFile(path);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
try {
|
|
38
|
+
return await inner.exec(elevateCommand(`cat ${sq(path)}`));
|
|
39
|
+
} catch {
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const existsElevated = async (path) => {
|
|
45
|
+
if (await inner.exists(path).catch(() => false)) return true;
|
|
46
|
+
return await inner.exec(elevateCommand(`test -e ${sq(path)}`)).then(() => true).catch(() => false);
|
|
47
|
+
};
|
|
48
|
+
const writeFileElevated = async (path, content) => {
|
|
49
|
+
const stage = `/tmp/.openship-elev-${randomBytes(12).toString("hex")}`;
|
|
50
|
+
await inner.exec(`mkdir -m 700 ${sq(stage)}`);
|
|
51
|
+
try {
|
|
52
|
+
const staged = `${stage}/payload`;
|
|
53
|
+
await inner.writeFile(staged, content);
|
|
54
|
+
await inner.exec(
|
|
55
|
+
elevateCommand(
|
|
56
|
+
`mkdir -p ${sq(dirOf(path))} && chown 0:0 ${sq(staged)} && mv -f ${sq(staged)} ${sq(path)}`
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
} finally {
|
|
60
|
+
await inner.exec(`rm -rf ${sq(stage)}`).catch(() => void 0);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const overrides = {
|
|
64
|
+
exec: (command, opts) => inner.exec(elevateCommand(command), opts),
|
|
65
|
+
streamExec: (command, onLog, opts) => inner.streamExec(elevateCommand(command), onLog, opts),
|
|
66
|
+
writeFile: writeFileElevated,
|
|
67
|
+
readFile: readFileElevated,
|
|
68
|
+
exists: existsElevated,
|
|
69
|
+
// Elevated for the same reason as writeFile: the vhost/conf dirs are root-owned,
|
|
70
|
+
// and the Proxy would otherwise forward this to the inner executor unelevated —
|
|
71
|
+
// EACCES immediately after an elevated write succeeded.
|
|
72
|
+
rename: async (from, to) => {
|
|
73
|
+
await inner.exec(elevateCommand(`mv -f ${sq(from)} ${sq(to)}`));
|
|
74
|
+
},
|
|
75
|
+
mkdir: async (path) => {
|
|
76
|
+
await inner.exec(elevateCommand(`mkdir -p ${sq(path)}`));
|
|
77
|
+
},
|
|
78
|
+
rm: async (path) => {
|
|
79
|
+
await inner.exec(elevateCommand(`rm -rf ${sq(path)}`));
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return new Proxy(inner, {
|
|
83
|
+
get(target, prop) {
|
|
84
|
+
if (prop === EXECUTOR_DELEGATE) return target;
|
|
85
|
+
if (Object.prototype.hasOwnProperty.call(overrides, prop)) {
|
|
86
|
+
return overrides[prop];
|
|
87
|
+
}
|
|
88
|
+
const value = Reflect.get(target, prop, target);
|
|
89
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async function ensureOwnedDir(executor, path) {
|
|
94
|
+
const quoted = sq(path);
|
|
95
|
+
try {
|
|
96
|
+
await executor.exec(`mkdir -p ${quoted} && [ -w ${quoted} ]`);
|
|
97
|
+
return;
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
const { isRoot, canSudo } = await resolveEnvironment(executor);
|
|
101
|
+
if (isRoot) {
|
|
102
|
+
await executor.exec(`mkdir -p ${quoted}`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!canSudo) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Cannot create ${path} on the target server: the deploy user cannot write there and has no passwordless sudo. Run these once on the server, then redeploy:
|
|
108
|
+
sudo mkdir -p ${path}
|
|
109
|
+
sudo chown -R "$(id -un)" ${path}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const loginUser = await executor.exec("id -un").then((out) => out.trim()).catch(() => "");
|
|
113
|
+
const owner = loginUser ? sq(loginUser) : '"$SUDO_USER"';
|
|
114
|
+
await executor.exec(
|
|
115
|
+
elevateCommand(
|
|
116
|
+
[
|
|
117
|
+
// Without it the `;`-joined steps fall through a failed mkdir, and the
|
|
118
|
+
// chown's exit status — not the mkdir's — is what the caller sees. That
|
|
119
|
+
// reported a permission failure as success.
|
|
120
|
+
"set -e",
|
|
121
|
+
`p=${quoted}`,
|
|
122
|
+
"d=$p",
|
|
123
|
+
"top=",
|
|
124
|
+
'while [ ! -d "$d" ]; do top=$d; d=$(dirname "$d"); done',
|
|
125
|
+
'mkdir -p "$p"',
|
|
126
|
+
// Expansion, not `[ -z "$top" ] && top=…`: that AND-list returns 1 whenever
|
|
127
|
+
// $top is already set, which under `set -e` would exit before the chown.
|
|
128
|
+
`chown -R ${owner} "\${top:-$p}"`
|
|
129
|
+
].join("; ")
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ../../packages/adapters/src/system/environment.ts
|
|
135
|
+
var ENVIRONMENT_PROBE_SCRIPT = `echo "opsh_begin=1"
|
|
136
|
+
# Appended, never replaced: \`ufw\`, \`nft\` and \`iptables\` live in /usr/sbin, which is off
|
|
137
|
+
# a non-root user's PATH on Debian \u2014 so "no firewall installed" was sometimes just "not on
|
|
138
|
+
# this user's PATH", and the box came back \`unknown\`.
|
|
139
|
+
PATH="$PATH:/usr/sbin:/sbin"
|
|
140
|
+
echo "opsh_os=$(uname -s 2>/dev/null || true)"
|
|
141
|
+
echo "opsh_arch=$(uname -m 2>/dev/null || true)"
|
|
142
|
+
echo "opsh_uid=$(id -u 2>/dev/null || true)"
|
|
143
|
+
echo "opsh_user=$(id -un 2>/dev/null || true)"
|
|
144
|
+
echo "opsh_home=$HOME"
|
|
145
|
+
|
|
146
|
+
OSR=/etc/os-release
|
|
147
|
+
[ -r "$OSR" ] || OSR=/usr/lib/os-release
|
|
148
|
+
# awk and not sed: \`print\` always terminates its record, while sed reproduces the input
|
|
149
|
+
# byte for byte \u2014 and an os-release with no trailing newline glued the next \`echo\` onto
|
|
150
|
+
# the last forwarded line, so \`opsh_pm\` vanished and a working apt host was refused for
|
|
151
|
+
# having no package manager, brackets intact and nothing to flag it.
|
|
152
|
+
[ -r "$OSR" ] && awk '{ print "opsh_osr:" $0 }' "$OSR" 2>/dev/null || true
|
|
153
|
+
|
|
154
|
+
PM=none
|
|
155
|
+
if command -v apt-get >/dev/null 2>&1; then PM=apt
|
|
156
|
+
elif command -v dnf >/dev/null 2>&1; then PM=dnf
|
|
157
|
+
elif command -v yum >/dev/null 2>&1; then PM=yum
|
|
158
|
+
elif command -v apk >/dev/null 2>&1; then PM=apk
|
|
159
|
+
elif command -v brew >/dev/null 2>&1; then PM=brew
|
|
160
|
+
fi
|
|
161
|
+
echo "opsh_pm=$PM"
|
|
162
|
+
|
|
163
|
+
SM=none
|
|
164
|
+
if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then SM=systemd
|
|
165
|
+
elif command -v rc-service >/dev/null 2>&1; then SM=openrc
|
|
166
|
+
elif command -v launchctl >/dev/null 2>&1; then SM=launchd
|
|
167
|
+
fi
|
|
168
|
+
echo "opsh_sm=$SM"
|
|
169
|
+
|
|
170
|
+
SUDO=n
|
|
171
|
+
if [ "$(id -u 2>/dev/null)" != 0 ]; then
|
|
172
|
+
sudo -n true >/dev/null 2>&1 && SUDO=y
|
|
173
|
+
fi
|
|
174
|
+
echo "opsh_sudo=$SUDO"
|
|
175
|
+
|
|
176
|
+
# Established BEFORE the firewall probes, because reading a ruleset is privileged:
|
|
177
|
+
# \`iptables -S\` and \`firewall-cmd --state\` refuse outright for an unprivileged user, and a
|
|
178
|
+
# refused read used to come back as "no firewall" \u2014 the one answer that makes us skip
|
|
179
|
+
# opening a port. \`-n\` so a box without passwordless sudo fails instead of waiting for a
|
|
180
|
+
# password on a channel with no TTY, and every elevated command below is a READ.
|
|
181
|
+
PRIV=
|
|
182
|
+
[ "$SUDO" = y ] && PRIV="sudo -n"
|
|
183
|
+
|
|
184
|
+
HAS_UFW=n; HAS_FWD=n; HAS_NFT=n; HAS_IPT=n
|
|
185
|
+
command -v ufw >/dev/null 2>&1 && HAS_UFW=y
|
|
186
|
+
command -v firewall-cmd >/dev/null 2>&1 && HAS_FWD=y
|
|
187
|
+
command -v nft >/dev/null 2>&1 && HAS_NFT=y
|
|
188
|
+
command -v iptables >/dev/null 2>&1 && HAS_IPT=y
|
|
189
|
+
FW=unknown
|
|
190
|
+
if [ "$HAS_UFW" = y ]; then
|
|
191
|
+
case "$($PRIV ufw status 2>/dev/null || true)" in *"Status: active"*) FW=ufw ;; esac
|
|
192
|
+
if [ "$FW" = unknown ] && grep -qiE "^[[:space:]]*ENABLED[[:space:]]*=[[:space:]]*yes" /etc/ufw/ufw.conf 2>/dev/null; then FW=ufw; fi
|
|
193
|
+
fi
|
|
194
|
+
if [ "$FW" = unknown ] && [ "$HAS_FWD" = y ]; then
|
|
195
|
+
case "$($PRIV firewall-cmd --state 2>/dev/null || true)" in *running*) FW=firewalld ;; esac
|
|
196
|
+
fi
|
|
197
|
+
if [ "$FW" = unknown ] && [ "$HAS_NFT" = y ]; then
|
|
198
|
+
case "$($PRIV nft list tables 2>/dev/null || true)" in *"table inet filter"*) FW=nftables ;; esac
|
|
199
|
+
fi
|
|
200
|
+
if [ "$FW" = unknown ] && [ "$HAS_IPT" = y ]; then
|
|
201
|
+
IPT="$($PRIV iptables -S INPUT 2>/dev/null || true)"
|
|
202
|
+
case "$IPT" in
|
|
203
|
+
*"-P INPUT DROP"*) FW=iptables ;;
|
|
204
|
+
# A permissive POLICY does not mean a permissive CHAIN. RHEL's stock ruleset and every
|
|
205
|
+
# hand-written iptables-persistent file end INPUT in a catch-all reject with the policy
|
|
206
|
+
# left at ACCEPT, and reading the policy alone reported those hosts as unfirewalled \u2014
|
|
207
|
+
# so nothing opened a port and the service came up unreachable. Any terminal drop counts:
|
|
208
|
+
# a single banned IP over-reports slightly, and over-reporting only costs a warning
|
|
209
|
+
# while under-reporting costs the port. (There is deliberately no \`-P INPUT REJECT\`
|
|
210
|
+
# arm: netfilter has no such built-in policy, so the old one was unreachable.)
|
|
211
|
+
*"-P INPUT "*)
|
|
212
|
+
if printf '%s\\n' "$IPT" | grep -qE '^-A INPUT .*-j (DROP|REJECT)'; then FW=iptables; else FW=none; fi
|
|
213
|
+
;;
|
|
214
|
+
esac
|
|
215
|
+
fi
|
|
216
|
+
if [ "$FW" = unknown ] && [ "$HAS_UFW$HAS_FWD$HAS_NFT$HAS_IPT" = nnnn ]; then FW=none; fi
|
|
217
|
+
echo "opsh_fw=$FW"
|
|
218
|
+
|
|
219
|
+
LIBC=unknown
|
|
220
|
+
if command -v ldd >/dev/null 2>&1; then
|
|
221
|
+
case "$(ldd --version 2>&1 || true)" in
|
|
222
|
+
*musl*) LIBC=musl ;;
|
|
223
|
+
*GNU*|*GLIBC*|*glibc*) LIBC=glibc ;;
|
|
224
|
+
esac
|
|
225
|
+
fi
|
|
226
|
+
echo "opsh_libc=$LIBC"
|
|
227
|
+
|
|
228
|
+
SEL=absent
|
|
229
|
+
if command -v getenforce >/dev/null 2>&1; then
|
|
230
|
+
SEL=disabled
|
|
231
|
+
case "$(getenforce 2>/dev/null || true)" in
|
|
232
|
+
Enforcing*) SEL=enforcing ;;
|
|
233
|
+
Permissive*) SEL=permissive ;;
|
|
234
|
+
esac
|
|
235
|
+
fi
|
|
236
|
+
echo "opsh_selinux=$SEL"
|
|
237
|
+
|
|
238
|
+
CT=n
|
|
239
|
+
[ -f /.dockerenv ] && CT=y
|
|
240
|
+
[ -f /run/.containerenv ] && CT=y
|
|
241
|
+
if [ "$CT" = n ] && grep -qaE "(docker|lxc|containerd|kubepods|libpod)" /proc/1/cgroup 2>/dev/null; then CT=y; fi
|
|
242
|
+
if [ "$CT" = n ] && command -v systemd-detect-virt >/dev/null 2>&1; then
|
|
243
|
+
systemd-detect-virt --container --quiet >/dev/null 2>&1 && CT=y
|
|
244
|
+
fi
|
|
245
|
+
echo "opsh_container=$CT"
|
|
246
|
+
|
|
247
|
+
echo "opsh_end=1"
|
|
248
|
+
exit 0`;
|
|
249
|
+
var ENVIRONMENT_PROBE_TIMEOUT_MS = 2e4;
|
|
250
|
+
var MAX_INTERCEPT_CHARS = 400;
|
|
251
|
+
var ENVIRONMENT_PROFILE_TTL_MS = 5 * 6e4;
|
|
252
|
+
function profileIsFresh(measuredAt) {
|
|
253
|
+
return Date.now() - measuredAt < ENVIRONMENT_PROFILE_TTL_MS;
|
|
254
|
+
}
|
|
255
|
+
var profileCache = /* @__PURE__ */ new WeakMap();
|
|
256
|
+
function isUnansweredProbe(message) {
|
|
257
|
+
const text = message.trim().toLowerCase();
|
|
258
|
+
if (text.includes("timed out") || text.includes("without an exit status")) return true;
|
|
259
|
+
return EXIT_STATUS_ONLY.test(text);
|
|
260
|
+
}
|
|
261
|
+
var EXIT_STATUS_ONLY = /^exit code (?:-?\d+|null|undefined)$/;
|
|
262
|
+
var SILENT_PROBE = "the probe produced no output at all";
|
|
263
|
+
async function runProbe(executor) {
|
|
264
|
+
const startedAt = Date.now();
|
|
265
|
+
systemDebug("environment", "probe:start");
|
|
266
|
+
try {
|
|
267
|
+
const text = await executor.exec(ENVIRONMENT_PROBE_SCRIPT, {
|
|
268
|
+
timeout: ENVIRONMENT_PROBE_TIMEOUT_MS
|
|
269
|
+
});
|
|
270
|
+
systemDebug("environment", `probe:ok (${formatDuration(startedAt)}) ${text.length}b`);
|
|
271
|
+
return { text, failed: false, unanswered: false };
|
|
272
|
+
} catch (err) {
|
|
273
|
+
if (isRemoteConnectionError(err)) {
|
|
274
|
+
systemDebug(
|
|
275
|
+
"environment",
|
|
276
|
+
`probe:abort (${formatDuration(startedAt)}) ${safeErrorMessage(err)}`
|
|
277
|
+
);
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
280
|
+
const message = safeErrorMessage(err);
|
|
281
|
+
systemDebug("environment", `probe:fail (${formatDuration(startedAt)}) ${message}`);
|
|
282
|
+
return { text: message, failed: true, unanswered: isUnansweredProbe(message) };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function members(all) {
|
|
286
|
+
return Object.keys(all);
|
|
287
|
+
}
|
|
288
|
+
var PACKAGE_MANAGERS = members({
|
|
289
|
+
apt: true,
|
|
290
|
+
dnf: true,
|
|
291
|
+
yum: true,
|
|
292
|
+
apk: true,
|
|
293
|
+
brew: true,
|
|
294
|
+
none: true
|
|
295
|
+
});
|
|
296
|
+
var SERVICE_MANAGERS = members({
|
|
297
|
+
systemd: true,
|
|
298
|
+
openrc: true,
|
|
299
|
+
launchd: true,
|
|
300
|
+
none: true
|
|
301
|
+
});
|
|
302
|
+
var FIREWALLS = members({
|
|
303
|
+
ufw: true,
|
|
304
|
+
firewalld: true,
|
|
305
|
+
nftables: true,
|
|
306
|
+
iptables: true,
|
|
307
|
+
none: true,
|
|
308
|
+
unknown: true
|
|
309
|
+
});
|
|
310
|
+
var LIBCS = members({ glibc: true, musl: true, unknown: true });
|
|
311
|
+
var SELINUX_STATES = members({
|
|
312
|
+
enforcing: true,
|
|
313
|
+
permissive: true,
|
|
314
|
+
disabled: true,
|
|
315
|
+
absent: true
|
|
316
|
+
});
|
|
317
|
+
function oneOf(value, allowed, fallback) {
|
|
318
|
+
const found = allowed.find((member) => member === value?.trim());
|
|
319
|
+
return found ?? fallback;
|
|
320
|
+
}
|
|
321
|
+
function splitProbeOutput(text) {
|
|
322
|
+
const keys = {};
|
|
323
|
+
const osReleaseLines = [];
|
|
324
|
+
for (const line of text.split("\n")) {
|
|
325
|
+
if (line.startsWith("opsh_osr:")) {
|
|
326
|
+
osReleaseLines.push(line.slice("opsh_osr:".length));
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (!line.startsWith("opsh_")) continue;
|
|
330
|
+
const eq = line.indexOf("=");
|
|
331
|
+
if (eq <= 0) continue;
|
|
332
|
+
keys[line.slice(0, eq)] = line.slice(eq + 1).trimEnd();
|
|
333
|
+
}
|
|
334
|
+
return { keys, osRelease: osReleaseLines.join("\n") };
|
|
335
|
+
}
|
|
336
|
+
function summarizeInterception(text) {
|
|
337
|
+
const collapsed = text.split("\n").map((line) => line.trim()).filter(Boolean).join(" ");
|
|
338
|
+
if (!collapsed) return null;
|
|
339
|
+
return collapsed.length > MAX_INTERCEPT_CHARS ? `${collapsed.slice(0, MAX_INTERCEPT_CHARS)}\u2026` : collapsed;
|
|
340
|
+
}
|
|
341
|
+
function parseEnvironmentProbe(probe) {
|
|
342
|
+
const { keys, osRelease } = splitProbeOutput(probe.text);
|
|
343
|
+
const complete = keys.opsh_begin === "1" && keys.opsh_end === "1";
|
|
344
|
+
const said = complete ? null : summarizeInterception(probe.text);
|
|
345
|
+
const unanswered = !complete && (probe.unanswered || said === null);
|
|
346
|
+
const forcedCommand = unanswered ? null : said;
|
|
347
|
+
const probeError = unanswered ? said ?? SILENT_PROBE : null;
|
|
348
|
+
const os = complete ? parseSystemOs(keys.opsh_os) : "unknown";
|
|
349
|
+
const osRaw = complete ? keys.opsh_os?.trim() || null : null;
|
|
350
|
+
const archRaw = complete ? keys.opsh_arch?.trim() || null : null;
|
|
351
|
+
const arch = complete ? parseSystemArch(keys.opsh_arch) : "unknown";
|
|
352
|
+
const osReleaseFields = parseOsRelease(osRelease);
|
|
353
|
+
const distroId = osReleaseFields.ID ?? null;
|
|
354
|
+
const { distro, family } = classifyDistro(distroId, osReleaseFields.ID_LIKE);
|
|
355
|
+
const isRoot = keys.opsh_uid?.trim() === "0";
|
|
356
|
+
const loginUser = keys.opsh_user?.trim() || (isRoot ? "root" : "");
|
|
357
|
+
const home = keys.opsh_home?.trim() || (isRoot ? "/root" : loginUser ? `/home/${loginUser}` : "");
|
|
358
|
+
const packageManager = oneOf(keys.opsh_pm, PACKAGE_MANAGERS, "none");
|
|
359
|
+
const profile = {
|
|
360
|
+
os,
|
|
361
|
+
osRaw,
|
|
362
|
+
arch,
|
|
363
|
+
archRaw,
|
|
364
|
+
// `distro` stays null off Linux, where the concept doesn't apply — the existing
|
|
365
|
+
// contract every call site was written against.
|
|
366
|
+
distro: os === "linux" ? distro : null,
|
|
367
|
+
distroFamily: os === "linux" ? family : "unknown",
|
|
368
|
+
distroId,
|
|
369
|
+
versionId: osReleaseFields.VERSION_ID ?? null,
|
|
370
|
+
prettyName: osReleaseFields.PRETTY_NAME ?? null,
|
|
371
|
+
packageManager,
|
|
372
|
+
serviceManager: oneOf(keys.opsh_sm, SERVICE_MANAGERS, "none"),
|
|
373
|
+
firewall: oneOf(keys.opsh_fw, FIREWALLS, "unknown"),
|
|
374
|
+
libc: oneOf(keys.opsh_libc, LIBCS, "unknown"),
|
|
375
|
+
selinux: oneOf(keys.opsh_selinux, SELINUX_STATES, "absent"),
|
|
376
|
+
isContainer: keys.opsh_container?.trim() === "y",
|
|
377
|
+
isRoot,
|
|
378
|
+
// Meaningless when root, and the script never probes sudo in that case — the
|
|
379
|
+
// deliberate `false` the previous detector also returned.
|
|
380
|
+
canSudo: !isRoot && keys.opsh_sudo?.trim() === "y",
|
|
381
|
+
loginUser,
|
|
382
|
+
home,
|
|
383
|
+
forcedCommand,
|
|
384
|
+
probeError,
|
|
385
|
+
supported: false,
|
|
386
|
+
unsupportedReason: null
|
|
387
|
+
};
|
|
388
|
+
return { ...profile, ...assessHostSupport(profile) };
|
|
389
|
+
}
|
|
390
|
+
function cacheKeyFor(executor) {
|
|
391
|
+
let root = executor;
|
|
392
|
+
for (let hops = 0; hops < 8; hops += 1) {
|
|
393
|
+
const delegate = root[EXECUTOR_DELEGATE];
|
|
394
|
+
if (!delegate || delegate === root) break;
|
|
395
|
+
root = delegate;
|
|
396
|
+
}
|
|
397
|
+
return root;
|
|
398
|
+
}
|
|
399
|
+
async function resolveEnvironment(executor) {
|
|
400
|
+
const key = cacheKeyFor(executor);
|
|
401
|
+
const cached = profileCache.get(key);
|
|
402
|
+
if (cached && profileIsFresh(cached.measuredAt)) {
|
|
403
|
+
systemDebug("environment", "cache:hit");
|
|
404
|
+
return cached.profile;
|
|
405
|
+
}
|
|
406
|
+
systemDebug("environment", cached ? "cache:stale" : "cache:miss");
|
|
407
|
+
const promise = (async () => {
|
|
408
|
+
const profile = parseEnvironmentProbe(await runProbe(key));
|
|
409
|
+
if (profile.probeError && cached) {
|
|
410
|
+
systemDebug("environment", `detect:keep-previous ${profile.probeError}`);
|
|
411
|
+
return cached.profile;
|
|
412
|
+
}
|
|
413
|
+
systemDebug("environment", `detect:done ${JSON.stringify(profile)}`);
|
|
414
|
+
return profile;
|
|
415
|
+
})();
|
|
416
|
+
const entry = { measuredAt: Date.now(), profile: promise };
|
|
417
|
+
const forget = () => {
|
|
418
|
+
if (profileCache.get(key) === entry) profileCache.delete(key);
|
|
419
|
+
};
|
|
420
|
+
profileCache.set(key, entry);
|
|
421
|
+
promise.then((profile) => {
|
|
422
|
+
if (profile.probeError) forget();
|
|
423
|
+
}, forget);
|
|
424
|
+
return promise;
|
|
425
|
+
}
|
|
426
|
+
function cacheEnvironment(executor, profile) {
|
|
427
|
+
profileCache.set(cacheKeyFor(executor), {
|
|
428
|
+
measuredAt: Date.now(),
|
|
429
|
+
profile: Promise.resolve(profile)
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export {
|
|
434
|
+
ENVIRONMENT_PROBE_SCRIPT,
|
|
435
|
+
ENVIRONMENT_PROBE_TIMEOUT_MS,
|
|
436
|
+
profileIsFresh,
|
|
437
|
+
parseEnvironmentProbe,
|
|
438
|
+
resolveEnvironment,
|
|
439
|
+
cacheEnvironment,
|
|
440
|
+
EXECUTOR_DELEGATE,
|
|
441
|
+
dirOf,
|
|
442
|
+
elevatedExecutor,
|
|
443
|
+
ensureOwnedDir
|
|
444
|
+
};
|