@pawlogic/dl 0.4.0-staging.d17e90a → 0.4.0-staging.d357346

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/dl-npm-update.sh CHANGED
@@ -1,13 +1,21 @@
1
1
  #!/bin/sh
2
2
  # dl 沙箱内自更新 (npm 版本) —— 替代 dl-selfupdate.sh 的 GCS 路径。
3
3
  #
4
- # wrapper 在 exec dl 之前调一次本脚本。任何步骤失败一律 swallow,永远 exit 0,
5
- # 绝不阻塞业务 dl 调用。当次仍用旧 bundle,新 bundle 装完后**下次** dl 调用才生效
6
- # (agent 失败重试天然吃到新版)。
4
+ # wrapper 在 exec dl 之前同步调一次本脚本。更新失败不阻止继续调用已有 bundle。
5
+ # --cli 绕过 TTL,输出 outcome 给 self-update 命令;安装与恢复仍使用同一把锁。
7
6
  #
8
- # 触发条件:DL_NPM_DIST_TAG 已 bake 到 sandbox env(prod=latest,staging=staging;PR
9
- # preview 留空 → 本脚本直接 exit,沙箱内 dl 锁在 template 烤进去那版,便于 PR 间
10
- # 复现 / 互不污染)。
7
+ # dist-tag 来源,优先级从高到低:
8
+ # 1. PI_DL_DIST_TAG —— hono-pi-e2b 每条命令注入 (e2b-operations.ts),反映
9
+ # **当前**这条 lane。
10
+ # 2. DL_NPM_DIST_TAG —— template build 时按 alias 烤进 wrapper 第二行
11
+ # (prod=latest,staging=staging;PR preview 留空 → 本脚本直接 exit,沙箱内
12
+ # dl 锁在 template 烤进去那版,便于 PR 间复现 / 互不污染)。
13
+ #
14
+ # 1 必须压过 2:project → sandbox_id 这条记录是 prod / staging 共享的 (staging
15
+ # 切片连的是同一个 pi-agent-prod-pg),切换环境时会 resume 到对方那台沙箱,而 2
16
+ # 跟着**建沙箱时**那个模板走 —— prod 建的沙箱被 staging resume 之后仍然会去装
17
+ # @latest。wrapper 是烤死在镜像里的改不动,但本脚本每次 npm install 后会从新包里
18
+ # 拷一份覆盖自己 (见下方"自我热更"),所以存量沙箱两代之内就能吃到这条优先级。
11
19
  #
12
20
  # 鉴权: 不需要。包发到 npmjs.com 的 @pawlogic scope (public),npm 默认 registry
13
21
  # 就是 npmjs.com,匿名 download 直接通,沙箱没有 ~/.npmrc。
@@ -28,6 +36,30 @@ STAMP=/tmp/dl-npm-checkedat
28
36
  LOCK=/tmp/dl-npm-update.lock
29
37
  TTL=300
30
38
  PACKAGE="@pawlogic/dl"
39
+ # 独立下发恢复脚本时,拒绝被尚未包含修复的 npm 包降级。
40
+ UPDATER_REVISION=2
41
+ CLI=false
42
+ [ "${1:-}" = "--cli" ] && CLI=true
43
+ RECOVERED=false
44
+
45
+ # 注入值优先,回落到 wrapper 烤死的值(理由见文件头)。
46
+ #
47
+ # 逐个来源单独校验、非法就继续往下回落,而不是先选后校验 —— 后者会让一个
48
+ # 写坏的 PI_DL_DIST_TAG 把整台沙箱的自更新彻底关掉,而不是退回模板烤死的值。
49
+ # 规则跟 self-update.ts 的 resolveDistTag 保持一致(首字符必须是字母数字)。
50
+ # 这个值会流到 npm install 的参数和 stamp 文件里,不合法一律当没给。
51
+ _valid_tag() {
52
+ case "${1:-}" in
53
+ '' | [!A-Za-z0-9]* | *[!A-Za-z0-9._-]*) return 1 ;;
54
+ *) return 0 ;;
55
+ esac
56
+ }
57
+ TAG=""
58
+ if _valid_tag "${PI_DL_DIST_TAG:-}"; then
59
+ TAG="$PI_DL_DIST_TAG"
60
+ elif _valid_tag "${DL_NPM_DIST_TAG:-}"; then
61
+ TAG="$DL_NPM_DIST_TAG"
62
+ fi
31
63
 
32
64
  # ── 观测上报: fire-and-forget JSON POST,失败 swallow ────────────────────────
33
65
  # args: outcome duration_ms [error_tail_file]
@@ -37,10 +69,21 @@ report_event() {
37
69
  _outcome="$1"
38
70
  _dur="${2:-null}"
39
71
  _errfile="${3:-}"
72
+ _code=""
73
+ _syscall=""
74
+ if [ -n "$_errfile" ] && [ -f "$_errfile" ]; then
75
+ _code=$(awk '/^npm (error|ERR!) code [A-Z0-9_]+$/ {print $4; exit}' "$_errfile")
76
+ _syscall=$(awk '/^npm (error|ERR!) syscall [a-z]+$/ {print $4; exit}' "$_errfile")
77
+ fi
78
+ if [ "$CLI" = true ]; then
79
+ printf '%s\n' "$_outcome"
80
+ printf 'code=%s syscall=%s recovered=%s\n' "$_code" "$_syscall" "$RECOVERED" >&2
81
+ return 0
82
+ fi
40
83
  [ -z "${CALLBACK_SERVER_URL:-}" ] && return 0
41
84
  [ -z "${DL_PROXY_TOKEN:-}" ] && return 0
42
85
  command -v curl >/dev/null 2>&1 || return 0
43
- _tag="${DL_NPM_DIST_TAG:-unknown}"
86
+ _tag="${TAG:-unknown}"
44
87
  case "$_tag" in
45
88
  latest|staging) ;;
46
89
  *) _tag="unknown" ;;
@@ -90,9 +133,9 @@ report_event() {
90
133
  curl -fsS -m 2 -X POST "${CALLBACK_SERVER_URL%/}/sandbox-rpc/dl/selfupdate-event" \
91
134
  -H 'content-type: application/json' \
92
135
  -H "authorization: Bearer ${DL_PROXY_TOKEN}" \
93
- -d "{\"trigger\":\"wrapper\",\"dist_tag\":\"$_tag\",\"outcome\":\"$_outcome\",\"duration_ms\":$_dur,\"installed_version\":\"$_ver\",\"build_short_sha\":null,\"error_tail\":$_err_json,\"project_id\":$_proj_json,\"mode\":$_mode_json,\"user_tier\":$_tier_json,\"user_id\":$_user_json,\"prompt_id\":$_prompt_json,\"tool_call_id\":$_toolcall_json,\"sandbox_id\":$_sandbox_json}" \
136
+ -d "{\"trigger\":\"wrapper\",\"dist_tag\":\"$_tag\",\"outcome\":\"$_outcome\",\"duration_ms\":$_dur,\"installed_version\":\"$_ver\",\"build_short_sha\":null,\"error_tail\":$_err_json,\"error_code\":\"$_code\",\"error_syscall\":\"$_syscall\",\"recovered\":$RECOVERED,\"project_id\":$_proj_json,\"mode\":$_mode_json,\"user_tier\":$_tier_json,\"user_id\":$_user_json,\"prompt_id\":$_prompt_json,\"tool_call_id\":$_toolcall_json,\"sandbox_id\":$_sandbox_json}" \
94
137
  >/dev/null 2>&1 || true
95
- ) &
138
+ ) 9>&- &
96
139
  }
97
140
 
98
141
  # 采样 TTL hit 上报: 10 选 1。awk 比 $RANDOM 更便携 (busybox sh 默认没 $RANDOM)。
@@ -100,8 +143,8 @@ should_sample_ttl() {
100
143
  awk 'BEGIN{srand(); exit !(int(rand()*10)==0)}' 2>/dev/null
101
144
  }
102
145
 
103
- # 环境不全 → no-op。staging / prod 才烤 DL_NPM_DIST_TAG,PR preview / 本地裸跑空。
104
- if [ -z "${DL_NPM_DIST_TAG:-}" ]; then
146
+ # 环境不全 → no-op。staging / prod 两个来源至少有一个有值,PR preview / 本地裸跑空。
147
+ if [ -z "$TAG" ]; then
105
148
  report_event no_tag null
106
149
  exit 0
107
150
  fi
@@ -110,25 +153,78 @@ if ! command -v npm >/dev/null 2>&1; then
110
153
  exit 0
111
154
  fi
112
155
 
113
- # TTL 拦截。stamp 损坏当 0 处理。
114
- now=$(date +%s 2>/dev/null) || exit 0
115
- if [ -f "$STAMP" ]; then
116
- last=$(cat "$STAMP" 2>/dev/null || echo 0)
117
- case "$last" in
118
- ''|*[!0-9]*) last=0 ;;
119
- esac
120
- delta=$((now - last))
121
- if [ "$delta" -ge 0 ] && [ "$delta" -lt "$TTL" ]; then
122
- if should_sample_ttl; then
123
- report_event ttl_hit null
156
+ # TTL 拦截。stamp 格式 `<epoch> <tag>`,stamp 损坏当 0 处理。
157
+ #
158
+ # tag 也要存:只比时间戳的话,切完 lane 的头 300s 会被 TTL short-circuit 掉,
159
+ # 整个 turn 都还在用另一条 lane 的 dl —— 自愈就不"立刻"了。tag 跟本次解析出
160
+ # 来的不一致直接跳过 TTL 重装。老格式(只有 epoch)解出来的 tag 是空串,同样
161
+ # 判成不一致,升级后第一次调用自然会重装一遍。
162
+ check_ttl() {
163
+ now=$(date +%s 2>/dev/null) || exit 0
164
+ last=0
165
+ last_tag=""
166
+ if [ -f "$STAMP" ]; then
167
+ _stamp_line=$(head -n1 "$STAMP" 2>/dev/null || echo "")
168
+ last="${_stamp_line%% *}"
169
+ case "$_stamp_line" in
170
+ *' '*) last_tag="${_stamp_line#* }" ;;
171
+ *) last_tag="" ;;
172
+ esac
173
+ case "$last" in
174
+ ''|*[!0-9]*) last=0 ;;
175
+ esac
176
+ fi
177
+ if [ "$CLI" = false ] && [ -f /home/user/.local/lib/node_modules/@pawlogic/dl/dl.mjs ] \
178
+ && [ -f /home/user/.local/lib/node_modules/@pawlogic/dl/ilands.mjs ] && [ "$last_tag" = "$TAG" ]; then
179
+ delta=$((now - last))
180
+ if [ "$delta" -ge 0 ] && [ "$delta" -lt "$TTL" ]; then
181
+ if should_sample_ttl; then
182
+ report_event ttl_hit null
183
+ fi
184
+ exit 0
124
185
  fi
125
- exit 0
126
186
  fi
127
- fi
187
+ }
188
+
189
+ check_ttl
128
190
 
129
191
  # 单飞锁:并发 dl 调用同时进来时只让一个跑 npm install,其他直接退出。
130
192
  # flock 不一定有(busybox sh) → mkdir 兜底原子锁。
193
+ install_package() {
194
+ # 在持锁的 shell 内限制 npm 耗时,避免 CLI 超时只杀 shell 却留下安装子进程。
195
+ HOME=/home/user timeout -k 5s 30s npm install -g --prefix=/home/user/.local \
196
+ --no-audit --no-fund --no-update-notifier --force \
197
+ "$PACKAGE@$TAG" >/dev/null 2>"$_errlog"
198
+ _install_status=$?
199
+ case "$_install_status" in
200
+ 124|137) printf 'npm error code ETIMEDOUT\n' >> "$_errlog" ;;
201
+ esac
202
+ return "$_install_status"
203
+ }
204
+
205
+ # 只备份本次 ENOTEMPTY 明确指向的 dl 临时目录,不扫描/删除其它包或工作文件。
206
+ recover_retired_directory() {
207
+ node - "$_errlog" <<'NODE'
208
+ const fs = require('node:fs');
209
+ const path = require('node:path');
210
+ try {
211
+ const log = fs.readFileSync(process.argv[2], 'utf8');
212
+ const field = name => log.match(new RegExp('^npm (?:error|ERR!) ' + name + ' (.+)$', 'm'))?.[1];
213
+ const scope = '/home/user/.local/lib/node_modules/@pawlogic';
214
+ const dest = field('dest');
215
+ if (field('code') !== 'ENOTEMPTY' || field('syscall') !== 'rename'
216
+ || field('path') !== scope + '/dl' || !dest
217
+ || path.dirname(dest) !== scope || !/^\.dl-[A-Za-z0-9]+$/.test(path.basename(dest))
218
+ || fs.realpathSync(scope) !== scope || !fs.lstatSync(dest).isDirectory()) process.exit(1);
219
+ const backup = fs.mkdtempSync('/home/user/.local/lib/dl-update-backup-');
220
+ fs.renameSync(dest, path.join(backup, path.basename(dest)));
221
+ } catch { process.exit(1); }
222
+ NODE
223
+ }
224
+
131
225
  run_install() {
226
+ # 首次安装等待锁期间,另一个入口可能已经装好并写入 TTL。
227
+ check_ttl
132
228
  _t0=$(date +%s 2>/dev/null || echo 0)
133
229
  # stderr 写到独立文件 npm_fail 分支带末尾给观测端;stdout 仍丢掉。
134
230
  _errlog=$(mktemp 2>/dev/null || echo "/tmp/dl-npm-err.$$")
@@ -145,12 +241,23 @@ run_install() {
145
241
  # --force to overwrite"); --force 允许 npm 覆盖。Honeycomb 上 staging
146
242
  # dl-selfupdate dataset 看到的所有 npm_fail outcome (#741 接通后) 都是这条,
147
243
  # 加 --force 就清零。
148
- if HOME=/home/user npm install -g --prefix=/home/user/.local \
149
- --no-audit --no-fund --no-update-notifier --force \
150
- "$PACKAGE@$DL_NPM_DIST_TAG" >/dev/null 2>"$_errlog"; then
244
+ if install_package; then
151
245
  _outcome=ok
152
246
  else
153
247
  _outcome=npm_fail
248
+ if recover_retired_directory; then
249
+ if install_package; then
250
+ _outcome=ok
251
+ RECOVERED=true
252
+ fi
253
+ fi
254
+ fi
255
+ # npm exit 0 还不足以证明两个入口完整,缺失时不记为成功。
256
+ if [ "$_outcome" = ok ] && { [ ! -s /home/user/.local/lib/node_modules/@pawlogic/dl/dl.mjs ] \
257
+ || [ ! -s /home/user/.local/lib/node_modules/@pawlogic/dl/ilands.mjs ]; }; then
258
+ _outcome=npm_fail
259
+ RECOVERED=false
260
+ printf 'npm error code INCOMPLETE_INSTALL\n' >> "$_errlog"
154
261
  fi
155
262
  _t1=$(date +%s 2>/dev/null || echo 0)
156
263
  _dur_ms=$(( (_t1 - _t0) * 1000 ))
@@ -165,13 +272,17 @@ run_install() {
165
272
  # dl-npm-update.sh), npm install 完成后覆盖一份到 wrapper 调用的固定路径。
166
273
  # 1 代 lag: 本次仍跑老逻辑, 新逻辑下次 dl 调用生效 —— 比"等 e2b template republish
167
274
  # + 老 snapshot 自然下线"快好几个量级, snapshot resume 的沙箱也能跟上。
168
- # 当前进程文件是 shell 解释器一次性读入内存的, 覆盖自身不会撕裂正在执行的循环。
169
- if [ -f /home/user/.local/lib/node_modules/@pawlogic/dl/dl-npm-update.sh ]; then
275
+ # 原子替换,避免覆盖当前 shell 仍在读取的 inode;失败时不从旧包回滚恢复脚本。
276
+ _packaged_updater=/home/user/.local/lib/node_modules/@pawlogic/dl/dl-npm-update.sh
277
+ _packaged_revision=$(awk -F= '/^UPDATER_REVISION=[0-9]+$/ {print $2; exit}' "$_packaged_updater" 2>/dev/null || true)
278
+ if [ "$_outcome" = ok ] && [ "${_packaged_revision:-0}" -ge "$UPDATER_REVISION" ]; then
279
+ _refresh=$(mktemp /home/user/.local/lib/dl-npm-update.XXXXXX)
170
280
  cp /home/user/.local/lib/node_modules/@pawlogic/dl/dl-npm-update.sh \
171
- /home/user/.local/lib/dl-npm-update.sh 2>/dev/null || true
172
- chmod +x /home/user/.local/lib/dl-npm-update.sh 2>/dev/null || true
281
+ "$_refresh" 2>/dev/null \
282
+ && chmod +x "$_refresh" 2>/dev/null \
283
+ && mv -f "$_refresh" /home/user/.local/lib/dl-npm-update.sh 2>/dev/null || true
173
284
  fi
174
- echo "$now" > "$STAMP"
285
+ echo "$now $TAG" > "$STAMP"
175
286
  if [ "$_outcome" = "npm_fail" ]; then
176
287
  report_event "$_outcome" "$_dur_ms" "$_errlog"
177
288
  else
@@ -182,7 +293,13 @@ run_install() {
182
293
 
183
294
  if command -v flock >/dev/null 2>&1; then
184
295
  (
185
- if ! flock -n 9; then
296
+ # 首次安装允许等另一入口完成,已有 bundle 的日常调用仍直接跳过。
297
+ _lock_wait=0
298
+ if [ ! -f /home/user/.local/lib/node_modules/@pawlogic/dl/dl.mjs ] \
299
+ || [ ! -f /home/user/.local/lib/node_modules/@pawlogic/dl/ilands.mjs ]; then
300
+ _lock_wait=30
301
+ fi
302
+ if ! flock -w "$_lock_wait" 9; then
186
303
  report_event lock_skip null
187
304
  exit 0
188
305
  fi