@prismshadow/penguin-server 0.2.6 → 0.2.8
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 +4 -4
- package/dist/api/types.d.ts +393 -21
- package/dist/auth-token.d.ts +29 -0
- package/dist/auth-token.js +51 -0
- package/dist/auth-token.js.map +1 -0
- package/dist/{chunk-WKJ3ANJP.js → chunk-622KL7BQ.js} +92 -381
- package/dist/chunk-622KL7BQ.js.map +1 -0
- package/dist/chunk-K4ZSUT72.js +34 -0
- package/dist/chunk-K4ZSUT72.js.map +1 -0
- package/dist/chunk-UGAFPYVX.js +374 -0
- package/dist/chunk-UGAFPYVX.js.map +1 -0
- package/dist/extension/index.d.ts +212 -0
- package/dist/extension/index.js +1 -0
- package/dist/extension/index.js.map +1 -0
- package/dist/index.js +5758 -1310
- package/dist/index.js.map +1 -1
- package/dist/initial-password.d.ts +9 -12
- package/dist/initial-password.js +3 -7
- package/dist/install.ps1 +729 -0
- package/dist/install.sh +785 -0
- package/dist/reset-admin-password.d.ts +4 -7
- package/dist/reset-admin-password.js +11 -9
- package/dist/reset-admin-password.js.map +1 -1
- package/dist/secret-file.d.ts +3 -0
- package/dist/secret-file.js +17 -0
- package/dist/secret-file.js.map +1 -0
- package/package.json +19 -3
- package/web-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web-dist/assets/{addon-fit-CE-3eCvv.js → addon-fit-DHOzI3Ur.js} +1 -1
- package/web-dist/assets/{addon-web-links-DeODgawX.js → addon-web-links-DC7759H1.js} +1 -1
- package/web-dist/assets/{highlighter-DpnM5FfD.js → highlighter-CGPO6Q4b.js} +1 -1
- package/web-dist/assets/index-CaFDHl-w.js +451 -0
- package/web-dist/assets/index-D8Bk9LtJ.css +1 -0
- package/web-dist/assets/{xterm-BjU-ZsL5.js → xterm-DY5ko-BU.js} +1 -1
- package/web-dist/index.html +2 -2
- package/dist/chunk-UUJCF46V.js +0 -50
- package/dist/chunk-UUJCF46V.js.map +0 -1
- package/dist/chunk-WKJ3ANJP.js.map +0 -1
- package/web-dist/assets/index-aRsvImZt.css +0 -1
- package/web-dist/assets/index-gNzir_kH.js +0 -189
package/dist/install.sh
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# PenguinHarness one-line installer.
|
|
3
|
+
#
|
|
4
|
+
# curl -fsSL https://github.com/Prism-Shadow/penguin-harness/releases/latest/download/install.sh | sh
|
|
5
|
+
#
|
|
6
|
+
# Options:
|
|
7
|
+
# PENGUIN_VERSION=vX.Y.Z choose a version (same as --version vX.Y.Z); a published Release
|
|
8
|
+
# installer defaults to its own version, an unstamped source copy to latest
|
|
9
|
+
# PENGUIN_INSTALL_DIR=<dir> install dir; default ~/.penguin
|
|
10
|
+
# PENGUIN_ARCHIVE=<file> install a local Release archive without network access (same as --archive <file>)
|
|
11
|
+
# PENGUIN_DOWNLOAD_SOURCE=auto|oss|github choose the online source; default auto (speed-probed,
|
|
12
|
+
# with the same-version other source as fallback)
|
|
13
|
+
# PENGUIN_DOWNLOAD_SPEED_PROBE=0 disable same-version OSS/GitHub probe timing in auto mode
|
|
14
|
+
# PENGUIN_DOWNLOAD_BASE_URL=<url> exact online asset directory selected by the stable forwarder
|
|
15
|
+
# PENGUIN_DOWNLOAD_FALLBACK_BASE_URL=<url> fallback for PENGUIN_DOWNLOAD_BASE_URL
|
|
16
|
+
# --universal install the universal package (no bundled Node runtime; needs system Node >= 24)
|
|
17
|
+
#
|
|
18
|
+
# Each Release attaches exactly one artifact per target: penguin-<target>.tar.gz, a shallow
|
|
19
|
+
# installer bundle holding this script, the program payload (payload.tar.gz) and the payload's
|
|
20
|
+
# checksum. Online installs download that bundle and verify it against its published .sha256;
|
|
21
|
+
# offline installs transfer the same single file, extract it once and run the bundled
|
|
22
|
+
# ./install.sh, which installs the sibling payload with no network access. Both paths verify
|
|
23
|
+
# the payload checksum sealed inside the bundle before anything is staged. Releases up to
|
|
24
|
+
# v0.1.5 shipped the program tree directly (top-level penguin/); such archives are still
|
|
25
|
+
# accepted, from --version pins and --archive files alike.
|
|
26
|
+
#
|
|
27
|
+
# The data dir (~/.penguin/data) sits under the install home but is never touched by reinstall/upgrade (which only replace bin/lib/web/node).
|
|
28
|
+
#
|
|
29
|
+
# Docs: https://penguin.ooo/docs/quickstart-cli
|
|
30
|
+
set -eu
|
|
31
|
+
|
|
32
|
+
REPO="https://github.com/Prism-Shadow/penguin-harness"
|
|
33
|
+
OSS_ORIGIN="https://penguin-harness-releases.oss-cn-beijing.aliyuncs.com"
|
|
34
|
+
OSS_RELEASE_ROOT="$OSS_ORIGIN/releases"
|
|
35
|
+
GITHUB_RELEASE_ROOT="$REPO/releases/download"
|
|
36
|
+
GITHUB_LATEST_BASE="$REPO/releases/latest/download"
|
|
37
|
+
VERSION="${PENGUIN_VERSION:-}"
|
|
38
|
+
INSTALL_DIR="${PENGUIN_INSTALL_DIR:-$HOME/.penguin}"
|
|
39
|
+
BIN_DIR="$HOME/.local/bin"
|
|
40
|
+
UNIVERSAL=0
|
|
41
|
+
ARCHIVE="${PENGUIN_ARCHIVE:-}"
|
|
42
|
+
SOURCE_MODE="${PENGUIN_DOWNLOAD_SOURCE:-auto}"
|
|
43
|
+
DOWNLOAD_BASE_URL="${PENGUIN_DOWNLOAD_BASE_URL:-}"
|
|
44
|
+
DOWNLOAD_FALLBACK_BASE_URL="${PENGUIN_DOWNLOAD_FALLBACK_BASE_URL:-}"
|
|
45
|
+
DOWNLOAD_SPEED_PROBE="${PENGUIN_DOWNLOAD_SPEED_PROBE:-1}"
|
|
46
|
+
# Auto-mode source selection, one rule shared by install.sh, install.ps1 and the download page on
|
|
47
|
+
# penguin.ooo (packages/landing/src/lib/download-source.ts):
|
|
48
|
+
#
|
|
49
|
+
# 1. Measure GitHub on the release's large probe file. At or above
|
|
50
|
+
# SPEED_PROBE_GITHUB_MIN_BYTES_PER_SECOND it wins outright and OSS is never touched.
|
|
51
|
+
# 2. Only below that is OSS measured, and it takes over only when it is more than
|
|
52
|
+
# SPEED_PROBE_OSS_SWITCH_RATIO_PERCENT of GitHub — a mirror that is merely a little quicker
|
|
53
|
+
# does not justify its bandwidth bill, and a slow GitHub download still resumes.
|
|
54
|
+
#
|
|
55
|
+
# GitHub is the free source, so every tie and every unmeasurable comparison stays there. The two
|
|
56
|
+
# constants are duplicated because these three implementations cannot import from each other — an
|
|
57
|
+
# installer is a standalone file fetched over the network. Change them in all three at once, which
|
|
58
|
+
# scripts/test-installer.sh pins.
|
|
59
|
+
#
|
|
60
|
+
# The total budget covers the whole probe: manifest, the small reachability pair, and up to two
|
|
61
|
+
# large probes. It is deliberately the sum of their caps, so the second large probe always gets its
|
|
62
|
+
# full window rather than being squeezed into declaring a healthy mirror unreachable.
|
|
63
|
+
SPEED_PROBE_MANIFEST_TIMEOUT_SECONDS=5
|
|
64
|
+
SPEED_PROBE_SMALL_TIMEOUT_SECONDS=5
|
|
65
|
+
SPEED_PROBE_LARGE_TIMEOUT_SECONDS=8
|
|
66
|
+
SPEED_PROBE_TOTAL_TIMEOUT_SECONDS=26
|
|
67
|
+
SPEED_PROBE_GITHUB_MIN_BYTES_PER_SECOND=262144
|
|
68
|
+
SPEED_PROBE_OSS_SWITCH_RATIO_PERCENT=150
|
|
69
|
+
SPEED_PROBE_STARTED_AT=0
|
|
70
|
+
PAYLOAD_NAME="payload.tar.gz"
|
|
71
|
+
# The release workflow replaces this token with the immutable tag before publishing both the
|
|
72
|
+
# standalone installer and the copies sealed inside the Linux, macOS and universal bundles.
|
|
73
|
+
EMBEDDED_RELEASE_VERSION="__PENGUIN_RELEASE_VERSION__"
|
|
74
|
+
|
|
75
|
+
fail() {
|
|
76
|
+
echo "error: $1" >&2
|
|
77
|
+
exit 1
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
validate_https_url() {
|
|
81
|
+
case "$2" in
|
|
82
|
+
https://*) ;;
|
|
83
|
+
*) fail "$1 must be an absolute HTTPS URL" ;;
|
|
84
|
+
esac
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
is_release_tag() {
|
|
88
|
+
case "$1" in
|
|
89
|
+
v[0-9A-Za-z]* ) ;;
|
|
90
|
+
*) return 1 ;;
|
|
91
|
+
esac
|
|
92
|
+
case "$1" in
|
|
93
|
+
*[!0-9A-Za-z._-]*) return 1 ;;
|
|
94
|
+
esac
|
|
95
|
+
return 0
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
validate_release_tag() {
|
|
99
|
+
is_release_tag "$1" || fail "invalid release version: $1"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
download_source_label() {
|
|
103
|
+
case "$1" in
|
|
104
|
+
https://*.aliyuncs.com/*) printf '%s\n' "OSS mirror" ;;
|
|
105
|
+
https://github.com/*) printf '%s\n' "GitHub" ;;
|
|
106
|
+
*) printf '%s\n' "configured mirror" ;;
|
|
107
|
+
esac
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# --- Parse args (also passable via curl | sh -s -- --universal) ---
|
|
111
|
+
while [ $# -gt 0 ]; do
|
|
112
|
+
case "$1" in
|
|
113
|
+
--version)
|
|
114
|
+
[ $# -ge 2 ] || fail "--version requires a value (e.g. --version v1.0.0)"
|
|
115
|
+
VERSION="$2"
|
|
116
|
+
shift 2
|
|
117
|
+
;;
|
|
118
|
+
--universal)
|
|
119
|
+
UNIVERSAL=1
|
|
120
|
+
shift
|
|
121
|
+
;;
|
|
122
|
+
--archive)
|
|
123
|
+
[ $# -ge 2 ] || fail "--archive requires a path to a Release archive"
|
|
124
|
+
ARCHIVE="$2"
|
|
125
|
+
shift 2
|
|
126
|
+
;;
|
|
127
|
+
*)
|
|
128
|
+
fail "unknown option: $1"
|
|
129
|
+
;;
|
|
130
|
+
esac
|
|
131
|
+
done
|
|
132
|
+
|
|
133
|
+
# --- Detect platform: Linux/Darwin x64/arm64; other platforms should use the universal package ---
|
|
134
|
+
TARGET="universal"
|
|
135
|
+
if [ "$UNIVERSAL" -eq 0 ]; then
|
|
136
|
+
case "$(uname -s)" in
|
|
137
|
+
Linux) os="linux" ;;
|
|
138
|
+
Darwin) os="darwin" ;;
|
|
139
|
+
*) fail "unsupported OS: $(uname -s). Install Node.js >= 24, then re-run with --universal." ;;
|
|
140
|
+
esac
|
|
141
|
+
case "$(uname -m)" in
|
|
142
|
+
x86_64) arch="x64" ;;
|
|
143
|
+
aarch64 | arm64) arch="arm64" ;;
|
|
144
|
+
*) fail "unsupported architecture: $(uname -m). Install Node.js >= 24, then re-run with --universal." ;;
|
|
145
|
+
esac
|
|
146
|
+
TARGET="$os-$arch"
|
|
147
|
+
fi
|
|
148
|
+
ASSET="penguin-$TARGET.tar.gz"
|
|
149
|
+
|
|
150
|
+
if [ -n "$ARCHIVE" ] && [ -n "$VERSION" ]; then
|
|
151
|
+
fail "--archive/PENGUIN_ARCHIVE cannot be combined with --version/PENGUIN_VERSION"
|
|
152
|
+
fi
|
|
153
|
+
if [ -n "$VERSION" ]; then
|
|
154
|
+
validate_release_tag "$VERSION"
|
|
155
|
+
fi
|
|
156
|
+
case "$SOURCE_MODE" in
|
|
157
|
+
auto | oss | github) ;;
|
|
158
|
+
*) fail "PENGUIN_DOWNLOAD_SOURCE must be auto, oss, or github" ;;
|
|
159
|
+
esac
|
|
160
|
+
case "$DOWNLOAD_SPEED_PROBE" in
|
|
161
|
+
0 | 1) ;;
|
|
162
|
+
*) fail "PENGUIN_DOWNLOAD_SPEED_PROBE must be 0 or 1" ;;
|
|
163
|
+
esac
|
|
164
|
+
RESOLVED_RELEASE_VERSION="$VERSION"
|
|
165
|
+
if [ -z "$RESOLVED_RELEASE_VERSION" ] && is_release_tag "$EMBEDDED_RELEASE_VERSION"; then
|
|
166
|
+
RESOLVED_RELEASE_VERSION="$EMBEDDED_RELEASE_VERSION"
|
|
167
|
+
fi
|
|
168
|
+
if [ -n "$DOWNLOAD_BASE_URL" ]; then
|
|
169
|
+
DOWNLOAD_BASE_URL="${DOWNLOAD_BASE_URL%/}"
|
|
170
|
+
validate_https_url PENGUIN_DOWNLOAD_BASE_URL "$DOWNLOAD_BASE_URL"
|
|
171
|
+
if [ -n "$DOWNLOAD_FALLBACK_BASE_URL" ]; then
|
|
172
|
+
DOWNLOAD_FALLBACK_BASE_URL="${DOWNLOAD_FALLBACK_BASE_URL%/}"
|
|
173
|
+
validate_https_url PENGUIN_DOWNLOAD_FALLBACK_BASE_URL "$DOWNLOAD_FALLBACK_BASE_URL"
|
|
174
|
+
fi
|
|
175
|
+
else
|
|
176
|
+
DOWNLOAD_FALLBACK_BASE_URL=""
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# --- Universal package precheck: system Node >= 24 (platform packages bundle the runtime, so exempt) ---
|
|
180
|
+
if [ "$UNIVERSAL" -eq 1 ]; then
|
|
181
|
+
command -v node >/dev/null 2>&1 \
|
|
182
|
+
|| fail "the universal package needs Node.js >= 24 on PATH (none found)."
|
|
183
|
+
node_version="$(node --version)" # e.g. v24.18.0
|
|
184
|
+
v="${node_version#v}"
|
|
185
|
+
major="${v%%.*}"
|
|
186
|
+
if [ "$major" -lt 24 ]; then
|
|
187
|
+
fail "the universal package needs Node.js >= 24, found $node_version."
|
|
188
|
+
fi
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
TMP="$(mktemp -d)"
|
|
192
|
+
STAGING=""
|
|
193
|
+
OLD_DIR=""
|
|
194
|
+
SWAP_ACTIVE=0
|
|
195
|
+
MOVED_OLD=""
|
|
196
|
+
MOVED_NEW=""
|
|
197
|
+
|
|
198
|
+
# Moves one directory to a new location even when the directory inode itself cannot be renamed
|
|
199
|
+
# — a mount point, a process's CWD, or a filesystem that pins in-use directories (overlayfs
|
|
200
|
+
# under Docker reports EBUSY when `penguin update` replaces the very lib/ its own process runs
|
|
201
|
+
# from). Strategies, in order: plain rename; per-entry renames into a fresh or existing
|
|
202
|
+
# destination; per-entry copy with the source entry removed (POSIX keeps an unlinked-but-open
|
|
203
|
+
# file valid for the process using it). A pinned, now-empty source directory is left in place
|
|
204
|
+
# and reused by the incoming move.
|
|
205
|
+
relocate_dir() {
|
|
206
|
+
rl_src="$1"
|
|
207
|
+
rl_dst="$2"
|
|
208
|
+
if [ ! -e "$rl_dst" ] && mv "$rl_src" "$rl_dst" 2>/dev/null; then
|
|
209
|
+
return 0
|
|
210
|
+
fi
|
|
211
|
+
[ -d "$rl_src" ] || return 1
|
|
212
|
+
mkdir -p "$rl_dst" || return 1
|
|
213
|
+
rl_failed=0
|
|
214
|
+
for rl_entry in "$rl_src"/* "$rl_src"/.[!.]* "$rl_src"/..?*; do
|
|
215
|
+
[ -e "$rl_entry" ] || [ -L "$rl_entry" ] || continue
|
|
216
|
+
if ! mv "$rl_entry" "$rl_dst/" 2>/dev/null; then
|
|
217
|
+
cp -Rp "$rl_entry" "$rl_dst/" || rl_failed=1
|
|
218
|
+
rm -rf "$rl_entry" || rl_failed=1
|
|
219
|
+
fi
|
|
220
|
+
done
|
|
221
|
+
[ "$rl_failed" -eq 0 ] || return 1
|
|
222
|
+
[ -z "$(ls -A "$rl_src" 2>/dev/null)" ] || return 1
|
|
223
|
+
rmdir "$rl_src" 2>/dev/null || :
|
|
224
|
+
return 0
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
rollback_install() {
|
|
228
|
+
rollback_failed=0
|
|
229
|
+
for d in $MOVED_NEW; do
|
|
230
|
+
# Clearing may leave a pinned-but-empty directory husk; the restore below reuses it.
|
|
231
|
+
rm -rf "$INSTALL_DIR/$d" 2>/dev/null || :
|
|
232
|
+
if [ -e "$INSTALL_DIR/$d" ] && [ -n "$(ls -A "$INSTALL_DIR/$d" 2>/dev/null)" ]; then
|
|
233
|
+
rollback_failed=1
|
|
234
|
+
fi
|
|
235
|
+
done
|
|
236
|
+
for d in $MOVED_OLD; do
|
|
237
|
+
if [ -e "$OLD_DIR/$d" ]; then
|
|
238
|
+
relocate_dir "$OLD_DIR/$d" "$INSTALL_DIR/$d" || rollback_failed=1
|
|
239
|
+
fi
|
|
240
|
+
done
|
|
241
|
+
if [ "$rollback_failed" -ne 0 ]; then
|
|
242
|
+
echo "error: automatic rollback was incomplete; previous files remain in $OLD_DIR" >&2
|
|
243
|
+
return 1
|
|
244
|
+
fi
|
|
245
|
+
echo "Previous PenguinHarness installation restored." >&2
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
cleanup() {
|
|
249
|
+
status=$?
|
|
250
|
+
trap - EXIT HUP INT TERM
|
|
251
|
+
set +e
|
|
252
|
+
if [ "$SWAP_ACTIVE" -eq 1 ]; then
|
|
253
|
+
rollback_install
|
|
254
|
+
fi
|
|
255
|
+
rm -rf "$TMP"
|
|
256
|
+
[ -z "$STAGING" ] || rm -rf "$STAGING"
|
|
257
|
+
[ -z "$OLD_DIR" ] || rm -rf "$OLD_DIR"
|
|
258
|
+
exit "$status"
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
trap cleanup EXIT
|
|
262
|
+
trap 'exit 129' HUP
|
|
263
|
+
trap 'exit 130' INT
|
|
264
|
+
trap 'exit 143' TERM
|
|
265
|
+
|
|
266
|
+
# Verifies file $1 against the sha256sum-format file $2 ($3 names the layer in messages).
|
|
267
|
+
# Checksums are never optional: every install path either downloads the published .sha256 or
|
|
268
|
+
# reads the one sealed inside the bundle.
|
|
269
|
+
verify_sha256() {
|
|
270
|
+
vs_expected="$(awk 'NR == 1 { print $1 }' "$2" | tr 'A-F' 'a-f')"
|
|
271
|
+
[ -n "$vs_expected" ] || fail "checksum file is empty or malformed: $2"
|
|
272
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
273
|
+
vs_actual="$(sha256sum "$1" | awk '{ print $1 }')"
|
|
274
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
275
|
+
vs_actual="$(shasum -a 256 "$1" | awk '{ print $1 }')"
|
|
276
|
+
else
|
|
277
|
+
fail "sha256sum or shasum is required for checksum verification"
|
|
278
|
+
fi
|
|
279
|
+
[ "$vs_actual" = "$vs_expected" ] || fail "checksum mismatch for $3."
|
|
280
|
+
echo "$3 checksum OK."
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
sha256_file() {
|
|
284
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
285
|
+
sha256sum "$1" | awk '{ print $1 }'
|
|
286
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
287
|
+
shasum -a 256 "$1" | awk '{ print $1 }'
|
|
288
|
+
else
|
|
289
|
+
fail "sha256sum or shasum is required for checksum verification"
|
|
290
|
+
fi
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
is_positive_integer() {
|
|
294
|
+
case "$1" in
|
|
295
|
+
'' | *[!0-9]*) return 1 ;;
|
|
296
|
+
0) return 1 ;;
|
|
297
|
+
*) return 0 ;;
|
|
298
|
+
esac
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
speed_probe_now_seconds() {
|
|
302
|
+
date +%s 2>/dev/null || printf '%s\n' 0
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
speed_probe_remaining_seconds() {
|
|
306
|
+
brs_now="$(speed_probe_now_seconds)"
|
|
307
|
+
case "$brs_now:$SPEED_PROBE_STARTED_AT" in
|
|
308
|
+
*[!0-9:]* | :* | *:) printf '%s\n' "$SPEED_PROBE_TOTAL_TIMEOUT_SECONDS"; return 0 ;;
|
|
309
|
+
esac
|
|
310
|
+
brs_elapsed=$((brs_now - SPEED_PROBE_STARTED_AT))
|
|
311
|
+
brs_remaining=$((SPEED_PROBE_TOTAL_TIMEOUT_SECONDS - brs_elapsed))
|
|
312
|
+
if [ "$brs_remaining" -gt 0 ]; then
|
|
313
|
+
printf '%s\n' "$brs_remaining"
|
|
314
|
+
else
|
|
315
|
+
printf '%s\n' 0
|
|
316
|
+
fi
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
speed_probe_curl_timeout() {
|
|
320
|
+
bct_cap="$1"
|
|
321
|
+
bct_remaining="$(speed_probe_remaining_seconds)"
|
|
322
|
+
[ "$bct_remaining" -gt 0 ] || return 1
|
|
323
|
+
if [ "$bct_remaining" -lt "$bct_cap" ]; then
|
|
324
|
+
printf '%s\n' "$bct_remaining"
|
|
325
|
+
else
|
|
326
|
+
printf '%s\n' "$bct_cap"
|
|
327
|
+
fi
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
load_release_download_manifest() {
|
|
331
|
+
lrdm_tag="$1"
|
|
332
|
+
lrdm_manifest="$TMP/release-download-manifest.tsv"
|
|
333
|
+
rm -f "$lrdm_manifest"
|
|
334
|
+
if lrdm_timeout="$(speed_probe_curl_timeout "$SPEED_PROBE_MANIFEST_TIMEOUT_SECONDS")" \
|
|
335
|
+
&& curl -fsSL --connect-timeout "$lrdm_timeout" --max-time "$lrdm_timeout" "$OSS_RELEASE_ROOT/$lrdm_tag/release-download-manifest.tsv" -o "$lrdm_manifest" 2>/dev/null; then
|
|
336
|
+
:
|
|
337
|
+
elif lrdm_timeout="$(speed_probe_curl_timeout "$SPEED_PROBE_MANIFEST_TIMEOUT_SECONDS")" \
|
|
338
|
+
&& curl -fsSL --connect-timeout "$lrdm_timeout" --max-time "$lrdm_timeout" "$GITHUB_RELEASE_ROOT/$lrdm_tag/release-download-manifest.tsv" -o "$lrdm_manifest" 2>/dev/null; then
|
|
339
|
+
:
|
|
340
|
+
else
|
|
341
|
+
return 1
|
|
342
|
+
fi
|
|
343
|
+
|
|
344
|
+
lrdm_expected_header="$(printf 'penguin-release-download-manifest\t1\t%s' "$lrdm_tag")"
|
|
345
|
+
[ "$(sed -n '1p' "$lrdm_manifest")" = "$lrdm_expected_header" ] || return 1
|
|
346
|
+
|
|
347
|
+
SPEED_PROBE_SMALL_PROBE="$(awk -F '\t' '$1 == "probe" && $2 == "small" { print $3; exit }' "$lrdm_manifest")"
|
|
348
|
+
SPEED_PROBE_SMALL_SIZE="$(awk -F '\t' '$1 == "probe" && $2 == "small" { print $4; exit }' "$lrdm_manifest")"
|
|
349
|
+
SPEED_PROBE_SMALL_HASH="$(awk -F '\t' '$1 == "probe" && $2 == "small" { print $5; exit }' "$lrdm_manifest")"
|
|
350
|
+
SPEED_PROBE_LARGE_PROBE="$(awk -F '\t' '$1 == "probe" && $2 == "large" { print $3; exit }' "$lrdm_manifest")"
|
|
351
|
+
SPEED_PROBE_LARGE_SIZE="$(awk -F '\t' '$1 == "probe" && $2 == "large" { print $4; exit }' "$lrdm_manifest")"
|
|
352
|
+
SPEED_PROBE_LARGE_HASH="$(awk -F '\t' '$1 == "probe" && $2 == "large" { print $5; exit }' "$lrdm_manifest")"
|
|
353
|
+
# Not part of the decision — an integrity check that this manifest belongs to a release which
|
|
354
|
+
# actually carries this target's bundle, so a mismatched manifest cannot steer the probe.
|
|
355
|
+
SPEED_PROBE_ASSET_SIZE="$(awk -F '\t' -v asset="$ASSET" '$1 == "asset" && $2 == asset { print $3; exit }' "$lrdm_manifest")"
|
|
356
|
+
|
|
357
|
+
for lrdm_file in "$SPEED_PROBE_SMALL_PROBE" "$SPEED_PROBE_LARGE_PROBE"; do
|
|
358
|
+
case "$lrdm_file" in
|
|
359
|
+
*[!A-Za-z0-9._+-]* | *..* | '') return 1 ;;
|
|
360
|
+
esac
|
|
361
|
+
done
|
|
362
|
+
is_positive_integer "$SPEED_PROBE_SMALL_SIZE" || return 1
|
|
363
|
+
is_positive_integer "$SPEED_PROBE_LARGE_SIZE" || return 1
|
|
364
|
+
is_positive_integer "$SPEED_PROBE_ASSET_SIZE" || return 1
|
|
365
|
+
case "$SPEED_PROBE_SMALL_HASH:$SPEED_PROBE_LARGE_HASH" in
|
|
366
|
+
*[!0-9a-f:]* | *::* | :* | *:) return 1 ;;
|
|
367
|
+
esac
|
|
368
|
+
[ "${#SPEED_PROBE_SMALL_HASH}" -eq 64 ] || return 1
|
|
369
|
+
[ "${#SPEED_PROBE_LARGE_HASH}" -eq 64 ] || return 1
|
|
370
|
+
return 0
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
probe_download_source() {
|
|
374
|
+
pds_label="$1"
|
|
375
|
+
pds_base="$2"
|
|
376
|
+
pds_file="$3"
|
|
377
|
+
pds_size="$4"
|
|
378
|
+
pds_hash="$5"
|
|
379
|
+
pds_metrics="$6"
|
|
380
|
+
pds_timeout_cap="${7:-$SPEED_PROBE_SMALL_TIMEOUT_SECONDS}"
|
|
381
|
+
pds_body="$TMP/probe-$pds_label-$pds_file"
|
|
382
|
+
pds_write="$pds_metrics.tmp"
|
|
383
|
+
rm -f "$pds_body" "$pds_metrics" "$pds_write"
|
|
384
|
+
pds_timeout="$(speed_probe_curl_timeout "$pds_timeout_cap")" || {
|
|
385
|
+
printf '%s\n' "fail" > "$pds_metrics"
|
|
386
|
+
return 0
|
|
387
|
+
}
|
|
388
|
+
if curl -fsSL --connect-timeout "$pds_timeout" --max-time "$pds_timeout" -H "Accept-Encoding: identity" \
|
|
389
|
+
-w '%{time_starttransfer} %{time_total} %{speed_download}' \
|
|
390
|
+
"$pds_base/$pds_file" -o "$pds_body" > "$pds_write" 2>/dev/null; then
|
|
391
|
+
pds_actual_size="$(wc -c < "$pds_body" | tr -d ' ')"
|
|
392
|
+
pds_actual_hash="$(sha256_file "$pds_body" | tr 'A-F' 'a-f')"
|
|
393
|
+
if [ "$pds_actual_size" = "$pds_size" ] && [ "$pds_actual_hash" = "$pds_hash" ]; then
|
|
394
|
+
read pds_start pds_total pds_speed < "$pds_write" || :
|
|
395
|
+
case "$pds_start:$pds_total" in
|
|
396
|
+
*[!0-9.:]* | :* | *:) printf '%s\n' "fail" > "$pds_metrics" ;;
|
|
397
|
+
*) printf '%s %s %s %s\n' "ok" "$pds_start" "$pds_total" "${pds_speed:-0}" > "$pds_metrics" ;;
|
|
398
|
+
esac
|
|
399
|
+
else
|
|
400
|
+
printf '%s\n' "fail" > "$pds_metrics"
|
|
401
|
+
fi
|
|
402
|
+
else
|
|
403
|
+
printf '%s\n' "fail" > "$pds_metrics"
|
|
404
|
+
fi
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
run_probe_pair() {
|
|
408
|
+
rpp_file="$1"
|
|
409
|
+
rpp_size="$2"
|
|
410
|
+
rpp_hash="$3"
|
|
411
|
+
OSS_PROBE_METRICS="$TMP/probe-oss-$rpp_file.metrics"
|
|
412
|
+
GITHUB_PROBE_METRICS="$TMP/probe-github-$rpp_file.metrics"
|
|
413
|
+
probe_download_source oss "$OSS_SPEED_PROBE_BASE_URL" "$rpp_file" "$rpp_size" "$rpp_hash" "$OSS_PROBE_METRICS" &
|
|
414
|
+
rpp_oss_pid=$!
|
|
415
|
+
probe_download_source github "$GITHUB_SPEED_PROBE_BASE_URL" "$rpp_file" "$rpp_size" "$rpp_hash" "$GITHUB_PROBE_METRICS" &
|
|
416
|
+
rpp_github_pid=$!
|
|
417
|
+
wait "$rpp_oss_pid" 2>/dev/null || :
|
|
418
|
+
wait "$rpp_github_pid" 2>/dev/null || :
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
probe_status() {
|
|
422
|
+
ps_file="$1"
|
|
423
|
+
if [ -f "$ps_file" ]; then
|
|
424
|
+
awk 'NR == 1 { print $1 }' "$ps_file"
|
|
425
|
+
else
|
|
426
|
+
printf '%s\n' "fail"
|
|
427
|
+
fi
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
# Throughput a completed probe measured, in whole bytes per second; 0 when it did not complete,
|
|
431
|
+
# which sorts it below any real measurement in the comparison below.
|
|
432
|
+
probe_bytes_per_second() {
|
|
433
|
+
pbs_metrics="$1"
|
|
434
|
+
if [ ! -f "$pbs_metrics" ]; then
|
|
435
|
+
printf '%s\n' 0
|
|
436
|
+
return 0
|
|
437
|
+
fi
|
|
438
|
+
awk 'NR == 1 && $1 == "ok" && $4 + 0 > 0 { printf "%d\n", $4 + 0; measured = 1 }
|
|
439
|
+
END { if (!measured) print 0 }' "$pbs_metrics"
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
# The shared rule, in one place: GitHub clears the minimum and wins outright, otherwise the mirror
|
|
443
|
+
# has to beat it by the switch ratio to take over. A POSIX shell has no floating point, so the ratio
|
|
444
|
+
# is applied to the GitHub side as an integer percent — scaling the mirror's side instead could
|
|
445
|
+
# overflow a 32-bit shell on a fast link, while this one is bounded by the minimum itself.
|
|
446
|
+
select_speed_probe_source() {
|
|
447
|
+
ssp_github_speed="$1"
|
|
448
|
+
ssp_oss_speed="$2"
|
|
449
|
+
if [ "$ssp_github_speed" -ge "$SPEED_PROBE_GITHUB_MIN_BYTES_PER_SECOND" ]; then
|
|
450
|
+
printf '%s\n' "github"
|
|
451
|
+
elif [ "$ssp_oss_speed" -gt "$((ssp_github_speed * SPEED_PROBE_OSS_SWITCH_RATIO_PERCENT / 100))" ]; then
|
|
452
|
+
printf '%s\n' "oss"
|
|
453
|
+
else
|
|
454
|
+
printf '%s\n' "github"
|
|
455
|
+
fi
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
speed_probe_release_sources() {
|
|
459
|
+
brs_tag="$1"
|
|
460
|
+
SPEED_PROBE_BASE_URL=""
|
|
461
|
+
SPEED_PROBE_FALLBACK_BASE_URL=""
|
|
462
|
+
OSS_SPEED_PROBE_BASE_URL="$OSS_RELEASE_ROOT/$brs_tag"
|
|
463
|
+
GITHUB_SPEED_PROBE_BASE_URL="$GITHUB_RELEASE_ROOT/$brs_tag"
|
|
464
|
+
SPEED_PROBE_STARTED_AT="$(speed_probe_now_seconds)"
|
|
465
|
+
|
|
466
|
+
load_release_download_manifest "$brs_tag" || return 1
|
|
467
|
+
echo "Testing OSS mirror and GitHub download sources ..."
|
|
468
|
+
|
|
469
|
+
run_probe_pair "$SPEED_PROBE_SMALL_PROBE" "$SPEED_PROBE_SMALL_SIZE" "$SPEED_PROBE_SMALL_HASH"
|
|
470
|
+
brs_oss_small="$(probe_status "$OSS_PROBE_METRICS")"
|
|
471
|
+
brs_github_small="$(probe_status "$GITHUB_PROBE_METRICS")"
|
|
472
|
+
|
|
473
|
+
if [ "$brs_oss_small" != "ok" ] && [ "$brs_github_small" = "ok" ]; then
|
|
474
|
+
SPEED_PROBE_BASE_URL="$GITHUB_SPEED_PROBE_BASE_URL"
|
|
475
|
+
SPEED_PROBE_FALLBACK_BASE_URL="$OSS_SPEED_PROBE_BASE_URL"
|
|
476
|
+
echo "Selected GitHub (OSS mirror probe unavailable)."
|
|
477
|
+
return 0
|
|
478
|
+
fi
|
|
479
|
+
if [ "$brs_oss_small" = "ok" ] && [ "$brs_github_small" != "ok" ]; then
|
|
480
|
+
SPEED_PROBE_BASE_URL="$OSS_SPEED_PROBE_BASE_URL"
|
|
481
|
+
SPEED_PROBE_FALLBACK_BASE_URL="$GITHUB_SPEED_PROBE_BASE_URL"
|
|
482
|
+
echo "Selected OSS mirror (GitHub probe unavailable)."
|
|
483
|
+
return 0
|
|
484
|
+
fi
|
|
485
|
+
if [ "$brs_oss_small" != "ok" ] && [ "$brs_github_small" != "ok" ]; then
|
|
486
|
+
return 1
|
|
487
|
+
fi
|
|
488
|
+
|
|
489
|
+
GITHUB_PROBE_METRICS="$TMP/probe-github-$SPEED_PROBE_LARGE_PROBE.metrics"
|
|
490
|
+
probe_download_source github "$GITHUB_SPEED_PROBE_BASE_URL" "$SPEED_PROBE_LARGE_PROBE" "$SPEED_PROBE_LARGE_SIZE" "$SPEED_PROBE_LARGE_HASH" "$GITHUB_PROBE_METRICS" "$SPEED_PROBE_LARGE_TIMEOUT_SECONDS"
|
|
491
|
+
brs_github_speed="$(probe_bytes_per_second "$GITHUB_PROBE_METRICS")"
|
|
492
|
+
|
|
493
|
+
# OSS is measured only once GitHub has failed the minimum: above it GitHub has already won, and
|
|
494
|
+
# the mirror's bandwidth is not spent on a probe that could not change the answer. Probing runs
|
|
495
|
+
# one source at a time on purpose — two concurrent transfers share the link and would each read
|
|
496
|
+
# as half as fast, which an absolute threshold cannot tolerate.
|
|
497
|
+
brs_oss_speed=0
|
|
498
|
+
if [ "$brs_github_speed" -lt "$SPEED_PROBE_GITHUB_MIN_BYTES_PER_SECOND" ]; then
|
|
499
|
+
OSS_PROBE_METRICS="$TMP/probe-oss-$SPEED_PROBE_LARGE_PROBE.metrics"
|
|
500
|
+
probe_download_source oss "$OSS_SPEED_PROBE_BASE_URL" "$SPEED_PROBE_LARGE_PROBE" "$SPEED_PROBE_LARGE_SIZE" "$SPEED_PROBE_LARGE_HASH" "$OSS_PROBE_METRICS" "$SPEED_PROBE_LARGE_TIMEOUT_SECONDS"
|
|
501
|
+
brs_oss_speed="$(probe_bytes_per_second "$OSS_PROBE_METRICS")"
|
|
502
|
+
fi
|
|
503
|
+
|
|
504
|
+
brs_choice="$(select_speed_probe_source "$brs_github_speed" "$brs_oss_speed")"
|
|
505
|
+
if [ "$brs_choice" = "github" ]; then
|
|
506
|
+
SPEED_PROBE_BASE_URL="$GITHUB_SPEED_PROBE_BASE_URL"
|
|
507
|
+
SPEED_PROBE_FALLBACK_BASE_URL="$OSS_SPEED_PROBE_BASE_URL"
|
|
508
|
+
if [ "$brs_github_speed" -ge "$SPEED_PROBE_GITHUB_MIN_BYTES_PER_SECOND" ]; then
|
|
509
|
+
echo "Selected GitHub (meets minimum download speed)."
|
|
510
|
+
else
|
|
511
|
+
echo "Selected GitHub (the OSS mirror was not enough faster to be worth switching)."
|
|
512
|
+
fi
|
|
513
|
+
else
|
|
514
|
+
SPEED_PROBE_BASE_URL="$OSS_SPEED_PROBE_BASE_URL"
|
|
515
|
+
SPEED_PROBE_FALLBACK_BASE_URL="$GITHUB_SPEED_PROBE_BASE_URL"
|
|
516
|
+
echo "Selected OSS mirror (clearly faster than GitHub here)."
|
|
517
|
+
fi
|
|
518
|
+
return 0
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
# Downloads the bundle and its checksum as a pair. Transport failures may try a same-version
|
|
522
|
+
# fallback; checksum failures are handled afterwards and always abort rather than being hidden
|
|
523
|
+
# by a different source.
|
|
524
|
+
download_release_pair() {
|
|
525
|
+
drp_base="$1"
|
|
526
|
+
drp_label="$(download_source_label "$drp_base")"
|
|
527
|
+
echo "Downloading $ASSET from $drp_label ..."
|
|
528
|
+
rm -f "$ARCHIVE_PATH" "$TMP/$ASSET.sha256"
|
|
529
|
+
curl -fSL --progress-bar --connect-timeout 5 --speed-limit 65536 --speed-time 20 "$drp_base/$ASSET" -o "$ARCHIVE_PATH" \
|
|
530
|
+
|| return 1
|
|
531
|
+
curl -fsSL --connect-timeout 5 --speed-limit 1024 --speed-time 20 "$drp_base/$ASSET.sha256" -o "$TMP/$ASSET.sha256" \
|
|
532
|
+
|| return 1
|
|
533
|
+
return 0
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
# Resolves the OSS mirror's latest immutable tag. The advertised base URL must exactly match
|
|
537
|
+
# the expected bucket path so metadata cannot redirect downloads to an arbitrary host.
|
|
538
|
+
get_oss_latest_tag() {
|
|
539
|
+
gol_manifest="$1"
|
|
540
|
+
rm -f "$gol_manifest"
|
|
541
|
+
curl -fsSL --connect-timeout 3 --max-time 8 "$OSS_ORIGIN/latest.json" -o "$gol_manifest" 2>/dev/null \
|
|
542
|
+
|| return 1
|
|
543
|
+
gol_schema_version="$(sed -n 's/.*"schemaVersion":[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$gol_manifest" | head -n 1)"
|
|
544
|
+
gol_candidate_tag="$(sed -n 's/.*"tag":[[:space:]]*"\([^"]*\)".*/\1/p' "$gol_manifest" | head -n 1)"
|
|
545
|
+
gol_candidate_base="$(sed -n 's/.*"releaseBaseUrl":[[:space:]]*"\([^"]*\)".*/\1/p' "$gol_manifest" | head -n 1)"
|
|
546
|
+
[ "$gol_schema_version" = "1" ] \
|
|
547
|
+
&& is_release_tag "$gol_candidate_tag" \
|
|
548
|
+
&& [ "$gol_candidate_base" = "$OSS_RELEASE_ROOT/$gol_candidate_tag" ] \
|
|
549
|
+
|| return 1
|
|
550
|
+
printf '%s\n' "$gol_candidate_tag"
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
# --- Resolve the program payload. Three entries converge on PAYLOAD_PATH:
|
|
554
|
+
# (a) bundled offline: this script sits next to payload.tar.gz in an extracted bundle;
|
|
555
|
+
# (b) --archive <file>: a local installer bundle, or a payload/legacy program archive;
|
|
556
|
+
# (c) online: download penguin-<target>.tar.gz and verify it, then open it.
|
|
557
|
+
# A bundle is recognized by containing payload.tar.gz at its top level; anything else is a
|
|
558
|
+
# program archive (payload.tar.gz itself, or a pre-0.1.6 release archive) whose top level
|
|
559
|
+
# is penguin/. ---
|
|
560
|
+
LOCAL_ARCHIVE=0
|
|
561
|
+
ARCHIVE_SHAPE=""
|
|
562
|
+
ARCHIVE_PATH=""
|
|
563
|
+
ARCHIVE_NAME=""
|
|
564
|
+
PAYLOAD_PATH=""
|
|
565
|
+
|
|
566
|
+
# Sibling pickup engages only for a real file actually named install.sh — the name it carries
|
|
567
|
+
# inside a bundle. A forwarder or `curl | sh` run never satisfies that (no file, or a random
|
|
568
|
+
# temp name), so an online installer cannot be steered by archives someone planted next to a
|
|
569
|
+
# temporary script in a shared directory.
|
|
570
|
+
SIBLING_PAYLOAD=""
|
|
571
|
+
if [ -z "$ARCHIVE" ] && [ -f "$0" ] && [ "$(basename "$0")" = "install.sh" ]; then
|
|
572
|
+
script_dir="$(CDPATH= cd "$(dirname "$0")" && pwd)"
|
|
573
|
+
if [ -f "$script_dir/$PAYLOAD_NAME" ]; then
|
|
574
|
+
SIBLING_PAYLOAD="$script_dir/$PAYLOAD_NAME"
|
|
575
|
+
fi
|
|
576
|
+
fi
|
|
577
|
+
|
|
578
|
+
if [ -n "$SIBLING_PAYLOAD" ]; then
|
|
579
|
+
# (a) Extracted bundle: install the sibling payload; no network access at all.
|
|
580
|
+
[ -z "$VERSION" ] \
|
|
581
|
+
|| fail "--version/PENGUIN_VERSION cannot be combined with the bundled offline installer"
|
|
582
|
+
echo "Using bundled payload $SIBLING_PAYLOAD ..."
|
|
583
|
+
[ -f "$SIBLING_PAYLOAD.sha256" ] \
|
|
584
|
+
|| fail "offline checksum file not found: $SIBLING_PAYLOAD.sha256"
|
|
585
|
+
verify_sha256 "$SIBLING_PAYLOAD" "$SIBLING_PAYLOAD.sha256" "Payload"
|
|
586
|
+
PAYLOAD_PATH="$SIBLING_PAYLOAD"
|
|
587
|
+
ARCHIVE_NAME="$PAYLOAD_NAME"
|
|
588
|
+
LOCAL_ARCHIVE=1
|
|
589
|
+
elif [ -n "$ARCHIVE" ]; then
|
|
590
|
+
# (b) Explicit local archive; its shape is probed below.
|
|
591
|
+
[ -f "$ARCHIVE" ] || fail "local archive not found: $ARCHIVE"
|
|
592
|
+
archive_name="${ARCHIVE##*/}"
|
|
593
|
+
archive_dir="$(CDPATH= cd "$(dirname "$ARCHIVE")" && pwd)"
|
|
594
|
+
ARCHIVE_PATH="$archive_dir/$archive_name"
|
|
595
|
+
ARCHIVE_NAME="$archive_name"
|
|
596
|
+
LOCAL_ARCHIVE=1
|
|
597
|
+
echo "Using local archive $ARCHIVE_PATH ..."
|
|
598
|
+
else
|
|
599
|
+
# (c) Online: explicit forwarder/configured URLs win. Otherwise a stamped Release installer
|
|
600
|
+
# uses its own immutable version: auto prefers OSS and falls back only to the same GitHub
|
|
601
|
+
# tag. An unstamped source-tree installer resolves latest.json first so it also locks one
|
|
602
|
+
# version before downloading assets.
|
|
603
|
+
FALLBACK_BASE_URL="$DOWNLOAD_FALLBACK_BASE_URL"
|
|
604
|
+
if [ -n "$DOWNLOAD_BASE_URL" ]; then
|
|
605
|
+
BASE_URL="$DOWNLOAD_BASE_URL"
|
|
606
|
+
elif [ "$SOURCE_MODE" = "github" ]; then
|
|
607
|
+
if [ -n "$RESOLVED_RELEASE_VERSION" ]; then
|
|
608
|
+
BASE_URL="$GITHUB_RELEASE_ROOT/$RESOLVED_RELEASE_VERSION"
|
|
609
|
+
else
|
|
610
|
+
BASE_URL="$GITHUB_LATEST_BASE"
|
|
611
|
+
fi
|
|
612
|
+
else
|
|
613
|
+
SELECTED_TAG="$RESOLVED_RELEASE_VERSION"
|
|
614
|
+
if [ -z "$SELECTED_TAG" ]; then
|
|
615
|
+
SELECTED_TAG="$(get_oss_latest_tag "$TMP/latest.json" || :)"
|
|
616
|
+
fi
|
|
617
|
+
if [ -n "$SELECTED_TAG" ]; then
|
|
618
|
+
BASE_URL="$OSS_RELEASE_ROOT/$SELECTED_TAG"
|
|
619
|
+
if [ "$SOURCE_MODE" = "auto" ] && [ -z "$FALLBACK_BASE_URL" ]; then
|
|
620
|
+
FALLBACK_BASE_URL="$GITHUB_RELEASE_ROOT/$SELECTED_TAG"
|
|
621
|
+
fi
|
|
622
|
+
if [ "$SOURCE_MODE" = "auto" ] && [ "$DOWNLOAD_SPEED_PROBE" = "1" ] && [ -z "$DOWNLOAD_BASE_URL" ]; then
|
|
623
|
+
if speed_probe_release_sources "$SELECTED_TAG"; then
|
|
624
|
+
BASE_URL="$SPEED_PROBE_BASE_URL"
|
|
625
|
+
FALLBACK_BASE_URL="$SPEED_PROBE_FALLBACK_BASE_URL"
|
|
626
|
+
else
|
|
627
|
+
echo "Download source test was inconclusive; using OSS with same-version GitHub fallback."
|
|
628
|
+
fi
|
|
629
|
+
fi
|
|
630
|
+
elif [ "$SOURCE_MODE" = "oss" ]; then
|
|
631
|
+
fail "the OSS mirror is unavailable or its release metadata is invalid."
|
|
632
|
+
else
|
|
633
|
+
BASE_URL="$GITHUB_LATEST_BASE"
|
|
634
|
+
fi
|
|
635
|
+
fi
|
|
636
|
+
ARCHIVE_PATH="$TMP/$ASSET"
|
|
637
|
+
ARCHIVE_NAME="$ASSET"
|
|
638
|
+
if ! download_release_pair "$BASE_URL"; then
|
|
639
|
+
if [ -n "$FALLBACK_BASE_URL" ] && [ "$FALLBACK_BASE_URL" != "$BASE_URL" ]; then
|
|
640
|
+
echo "Primary download source unavailable; trying $(download_source_label "$FALLBACK_BASE_URL") ..."
|
|
641
|
+
download_release_pair "$FALLBACK_BASE_URL" \
|
|
642
|
+
|| fail "download failed from both the primary source and its fallback. Check your network, then retry."
|
|
643
|
+
else
|
|
644
|
+
fail "download failed from $(download_source_label "$BASE_URL"). Check the version tag and your network, then retry."
|
|
645
|
+
fi
|
|
646
|
+
fi
|
|
647
|
+
verify_sha256 "$ARCHIVE_PATH" "$TMP/$ASSET.sha256" "Bundle"
|
|
648
|
+
fi
|
|
649
|
+
|
|
650
|
+
if [ -z "$PAYLOAD_PATH" ]; then
|
|
651
|
+
if tar -tzf "$ARCHIVE_PATH" 2>/dev/null | head -50 | grep -qE "^(\./)?$PAYLOAD_NAME\$"; then
|
|
652
|
+
ARCHIVE_SHAPE="bundle"
|
|
653
|
+
else
|
|
654
|
+
ARCHIVE_SHAPE="program"
|
|
655
|
+
fi
|
|
656
|
+
if [ "$ARCHIVE_SHAPE" = "bundle" ]; then
|
|
657
|
+
# A local bundle is self-verifying through its sealed payload checksum; when the published
|
|
658
|
+
# outer .sha256 was transferred alongside it, verify that layer too.
|
|
659
|
+
if [ "$LOCAL_ARCHIVE" -eq 1 ] && [ -f "$ARCHIVE_PATH.sha256" ]; then
|
|
660
|
+
verify_sha256 "$ARCHIVE_PATH" "$ARCHIVE_PATH.sha256" "Bundle"
|
|
661
|
+
fi
|
|
662
|
+
BUNDLE_DIR="$TMP/bundle"
|
|
663
|
+
mkdir -p "$BUNDLE_DIR"
|
|
664
|
+
tar -xzf "$ARCHIVE_PATH" -C "$BUNDLE_DIR"
|
|
665
|
+
PAYLOAD_PATH="$BUNDLE_DIR/$PAYLOAD_NAME"
|
|
666
|
+
[ -f "$PAYLOAD_PATH" ] || fail "unexpected bundle layout: $PAYLOAD_NAME missing."
|
|
667
|
+
[ -f "$PAYLOAD_PATH.sha256" ] || fail "unexpected bundle layout: $PAYLOAD_NAME.sha256 missing."
|
|
668
|
+
verify_sha256 "$PAYLOAD_PATH" "$PAYLOAD_PATH.sha256" "Payload"
|
|
669
|
+
else
|
|
670
|
+
# Program archive (payload.tar.gz, or a pre-0.1.6 release archive). A local file needs an
|
|
671
|
+
# adjacent checksum — its own, or the canonical asset checksum next to a renamed legacy
|
|
672
|
+
# file; an online download was already verified against the published .sha256 above.
|
|
673
|
+
if [ "$LOCAL_ARCHIVE" -eq 1 ]; then
|
|
674
|
+
SHA_PATH="$ARCHIVE_PATH.sha256"
|
|
675
|
+
if [ ! -f "$SHA_PATH" ] && [ "$ARCHIVE_NAME" != "$ASSET" ] && [ -f "$archive_dir/$ASSET.sha256" ]; then
|
|
676
|
+
SHA_PATH="$archive_dir/$ASSET.sha256"
|
|
677
|
+
fi
|
|
678
|
+
[ -f "$SHA_PATH" ] || fail "offline checksum file not found: $SHA_PATH"
|
|
679
|
+
verify_sha256 "$ARCHIVE_PATH" "$SHA_PATH" "Payload"
|
|
680
|
+
fi
|
|
681
|
+
PAYLOAD_PATH="$ARCHIVE_PATH"
|
|
682
|
+
fi
|
|
683
|
+
fi
|
|
684
|
+
|
|
685
|
+
# --- Extract and validate in staging before touching the current install. The final same-filesystem
|
|
686
|
+
# swap keeps the previous dirs in .old.$$ until the installed command runs successfully; any
|
|
687
|
+
# move or launch failure restores them automatically. The data dir is never part of the swap. ---
|
|
688
|
+
tar -xzf "$PAYLOAD_PATH" -C "$TMP"
|
|
689
|
+
[ -d "$TMP/penguin" ] || fail "unexpected archive layout: top-level penguin/ missing."
|
|
690
|
+
MANIFEST_PATH="$TMP/penguin/package-manifest.json"
|
|
691
|
+
if [ -f "$MANIFEST_PATH" ]; then
|
|
692
|
+
manifest_target="$(sed -n 's/.*"target"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$MANIFEST_PATH" | head -n 1)"
|
|
693
|
+
[ -n "$manifest_target" ] || fail "package manifest is malformed: target missing."
|
|
694
|
+
[ "$manifest_target" = "$TARGET" ] \
|
|
695
|
+
|| fail "package target mismatch: expected $TARGET, found $manifest_target."
|
|
696
|
+
elif [ "$LOCAL_ARCHIVE" -eq 1 ] && [ "$ARCHIVE_SHAPE" = "program" ] && [ "$ARCHIVE_NAME" != "$ASSET" ]; then
|
|
697
|
+
fail "a renamed local archive must contain package-manifest.json; use the original filename for legacy packages."
|
|
698
|
+
fi
|
|
699
|
+
mkdir -p "$INSTALL_DIR"
|
|
700
|
+
STAGING="$INSTALL_DIR/.staging.$$"
|
|
701
|
+
OLD_DIR="$INSTALL_DIR/.old.$$"
|
|
702
|
+
rm -rf "$STAGING"
|
|
703
|
+
rm -rf "$OLD_DIR"
|
|
704
|
+
mkdir -p "$STAGING"
|
|
705
|
+
for d in bin lib web node; do
|
|
706
|
+
if [ -e "$TMP/penguin/$d" ]; then
|
|
707
|
+
mv "$TMP/penguin/$d" "$STAGING/$d"
|
|
708
|
+
fi
|
|
709
|
+
done
|
|
710
|
+
[ -x "$STAGING/bin/penguin" ] || fail "unexpected archive layout: bin/penguin missing."
|
|
711
|
+
|
|
712
|
+
# The launcher resolves lib/, web/ and node/ relative to itself, so staging is a faithful
|
|
713
|
+
# preflight that catches macOS execution policy and runtime/package failures before replacement.
|
|
714
|
+
if candidate_version="$("$STAGING/bin/penguin" --version)"; then
|
|
715
|
+
[ -n "$candidate_version" ] || fail "candidate PenguinHarness returned an empty version."
|
|
716
|
+
else
|
|
717
|
+
candidate_status=$?
|
|
718
|
+
fail "candidate PenguinHarness failed to run (exit status $candidate_status). See the error above."
|
|
719
|
+
fi
|
|
720
|
+
|
|
721
|
+
mkdir -p "$OLD_DIR"
|
|
722
|
+
SWAP_ACTIVE=1
|
|
723
|
+
for d in bin lib web node; do
|
|
724
|
+
if [ -e "$INSTALL_DIR/$d" ]; then
|
|
725
|
+
if relocate_dir "$INSTALL_DIR/$d" "$OLD_DIR/$d"; then
|
|
726
|
+
MOVED_OLD="$MOVED_OLD $d"
|
|
727
|
+
else
|
|
728
|
+
fail "could not move the existing $d directory aside (stop running penguin processes and retry); the previous installation will be restored."
|
|
729
|
+
fi
|
|
730
|
+
fi
|
|
731
|
+
done
|
|
732
|
+
for d in bin lib web node; do
|
|
733
|
+
if [ -e "$STAGING/$d" ]; then
|
|
734
|
+
if relocate_dir "$STAGING/$d" "$INSTALL_DIR/$d"; then
|
|
735
|
+
MOVED_NEW="$MOVED_NEW $d"
|
|
736
|
+
else
|
|
737
|
+
fail "could not install the new $d directory; the previous installation will be restored."
|
|
738
|
+
fi
|
|
739
|
+
fi
|
|
740
|
+
done
|
|
741
|
+
[ -x "$INSTALL_DIR/bin/penguin" ] || fail "install incomplete: $INSTALL_DIR/bin/penguin missing."
|
|
742
|
+
|
|
743
|
+
# Verify again from the final path before deleting the backup. Keep stderr visible so platform
|
|
744
|
+
# policy, permission and runtime errors are not disguised as an "unknown" version.
|
|
745
|
+
if installed_version="$("$INSTALL_DIR/bin/penguin" --version)"; then
|
|
746
|
+
[ -n "$installed_version" ] || fail "installed PenguinHarness returned an empty version."
|
|
747
|
+
else
|
|
748
|
+
version_status=$?
|
|
749
|
+
fail "installed PenguinHarness failed to run (exit status $version_status); the previous installation will be restored. See the error above."
|
|
750
|
+
fi
|
|
751
|
+
|
|
752
|
+
SWAP_ACTIVE=0
|
|
753
|
+
if ! rm -rf "$OLD_DIR"; then
|
|
754
|
+
echo "warning: could not remove the previous installation backup at $OLD_DIR" >&2
|
|
755
|
+
fi
|
|
756
|
+
OLD_DIR=""
|
|
757
|
+
rm -rf "$STAGING"
|
|
758
|
+
STAGING=""
|
|
759
|
+
|
|
760
|
+
# --- Symlink into ~/.local/bin and check PATH only after the install is known to work. ---
|
|
761
|
+
mkdir -p "$BIN_DIR"
|
|
762
|
+
ln -sf "$INSTALL_DIR/bin/penguin" "$BIN_DIR/penguin"
|
|
763
|
+
PATH_MISSING=0
|
|
764
|
+
case ":$PATH:" in
|
|
765
|
+
*":$BIN_DIR:"*) ;;
|
|
766
|
+
*) PATH_MISSING=1 ;;
|
|
767
|
+
esac
|
|
768
|
+
|
|
769
|
+
echo ""
|
|
770
|
+
echo "PenguinHarness $installed_version installed to $INSTALL_DIR"
|
|
771
|
+
if [ "$PATH_MISSING" -eq 1 ]; then
|
|
772
|
+
echo ""
|
|
773
|
+
echo "note: installation succeeded, but $BIN_DIR is not on your PATH. Add it to your shell profile:"
|
|
774
|
+
case "${SHELL:-}" in
|
|
775
|
+
*/zsh) echo " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.zshrc && source ~/.zshrc" ;;
|
|
776
|
+
*/bash) echo " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.bashrc && source ~/.bashrc" ;;
|
|
777
|
+
*/fish) echo " fish_add_path \$HOME/.local/bin" ;;
|
|
778
|
+
*) echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" ;;
|
|
779
|
+
esac
|
|
780
|
+
fi
|
|
781
|
+
echo ""
|
|
782
|
+
echo "Get started:"
|
|
783
|
+
echo " penguin --help # all commands"
|
|
784
|
+
echo " penguin web # start the Web UI at http://127.0.0.1:7364 (a first-login link is printed on first start)"
|
|
785
|
+
echo " penguin server # headless server (PORT / HOST to override)"
|