aaden-cli 1.6.103-rc.2 → 1.6.103
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/AadenDeploy/appCenterSync.sh +166 -0
- package/AadenDeploy/appCenterUsage.py +141 -0
- package/AadenDeploy/appcenterRefs.py +67 -0
- package/AadenDeploy/ensureAppCenterNoCache.sh +84 -0
- package/AadenDeploy/upgrade.sh +7 -0
- package/dist/index.js +85 -8
- package/index.js +77 -0
- package/package.json +1 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# AppCenter 前端同步(aaden-cli appcenter-sync 的执行体)。产物由 aaden-php-backend scripts/publish-appcenter-artifact.sh 发布:
|
|
3
|
+
# <base>/<channel>/manifest.json + <base>/blobs/<App>-<sha>.zip
|
|
4
|
+
#
|
|
5
|
+
# light-update 只发 PHP/,AppCenter 以前只能跟 465MB 的 latest.zip 走;这里单独按应用同步。
|
|
6
|
+
# - 灰度门:manifest 的 rollout = {"devices":[...], "percent":N},本机在名单里或 deviceId%100 < N 才动;没有 rollout = 全量
|
|
7
|
+
# - ensureNoCache:manifest 打开时先确保 AppCenter 的 index.html 带 no-cache(ensureAppCenterNoCache.sh)
|
|
8
|
+
# - manifest 门:本机 .appcenter-manifest 的 sha256 == manifest 的 treeSha256 且 `sha256sum -c` 通过 → 什么都不做,
|
|
9
|
+
# 每天只花一次几 KB 的请求。被 latest.zip 全量覆盖过、文件对不上 → 校验失败 → 重新铺一遍(自愈)
|
|
10
|
+
# - 先下载并校验(zipSha256 + 解压后 sha256sum -c),确认无误才动线上目录
|
|
11
|
+
# - 保留上一代:旧目录里「旧 index.html 用得到、新版没有」的文件拷进新目录(appcenterRefs.py),
|
|
12
|
+
# 正开着的旧页面懒加载 chunk 时不会 404;更早手工覆盖堆下来的历史文件就此清掉
|
|
13
|
+
# - 整目录 rename 替换,旧目录留一份在 BACKUP;健康检查失败就换回去
|
|
14
|
+
# - 不碰 PHP、不碰数据库,所以不需要 TSE 门
|
|
15
|
+
#
|
|
16
|
+
# 用法: appCenterSync.sh [channel]
|
|
17
|
+
# APPCENTER_BASE 产物根地址,默认 S3;测试可用 file:///var/tmp/...(curl 都支持)
|
|
18
|
+
# APPCENTER_APPS 只同步这些应用(空格分隔);不传 = manifest 里列出的全部应用,发哪些由发布端决定
|
|
19
|
+
# APPCENTER_DEVICE_ID 本机 deviceId(CLI 会传);不传就问本机 PHP
|
|
20
|
+
# APPCENTER_RESULT 结果行另存的文件(CLI 会传)
|
|
21
|
+
# 结果行(同时写进 $RESULT 给 CLI 上报):APP_OK <App> <sha12> | APP_UP_TO_DATE <App> | APP_SKIP <App> <原因> |
|
|
22
|
+
# APP_ROLLED_BACK <App> | APP_ABORT_* <App> | NOCACHE_* | SKIP_ROLLOUT | SKIP_DEPS | SKIP_LOCKED | ABORT_*
|
|
23
|
+
# 退出码:0=全部成功、无需更新或不在灰度内;1=有失败(失败的已回滚或未动线上目录)
|
|
24
|
+
set -uo pipefail
|
|
25
|
+
BASE="${APPCENTER_BASE:-https://aaden-backend.s3.eu-central-1.amazonaws.com/appcenter}"
|
|
26
|
+
CHANNEL="${1:-stable}"
|
|
27
|
+
WEB=/var/www/html/AppCenter
|
|
28
|
+
STAGE=/var/tmp/aaden-appcenter-stage
|
|
29
|
+
BACKUP=/var/lib/aaden-appcenter-backup
|
|
30
|
+
LOCK=/var/tmp/aaden-appcenter.lock
|
|
31
|
+
RESULT="${APPCENTER_RESULT:-/var/tmp/aaden-appcenter-result.txt}" # CLI 传一个自己建的临时文件,读回来上报
|
|
32
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
33
|
+
PW='Innerken322.'
|
|
34
|
+
sudoS(){ echo "$PW" | sudo -S bash -c "$*"; }
|
|
35
|
+
res(){ echo "$*"; echo "$*" >> "$RESULT"; }
|
|
36
|
+
|
|
37
|
+
: > "$RESULT" 2>/dev/null || { RESULT=$(mktemp); : > "$RESULT"; }
|
|
38
|
+
|
|
39
|
+
MISSING=""
|
|
40
|
+
for c in curl python3 unzip sha256sum flock tar cut comm; do command -v "$c" >/dev/null || MISSING="$MISSING $c"; done
|
|
41
|
+
if [ -n "$MISSING" ]; then res "SKIP_DEPS$MISSING"; exit 0; fi
|
|
42
|
+
if ! echo "$PW" | sudo -S true 2>/dev/null; then res "ABORT_SUDO_AUTH"; exit 1; fi
|
|
43
|
+
|
|
44
|
+
# 同一时间只跑一个(auto-upgrade 与手动触发可能撞上)。只读打开即可 flock,root/aaden 谁先建文件都行
|
|
45
|
+
[ -e "$LOCK" ] || touch "$LOCK"
|
|
46
|
+
exec 9<"$LOCK"
|
|
47
|
+
flock -n 9 || { res "SKIP_LOCKED"; exit 0; }
|
|
48
|
+
|
|
49
|
+
MANIFEST=$(curl -fsSL --max-time 30 "$BASE/$CHANNEL/manifest.json") \
|
|
50
|
+
|| { echo "取不到 $BASE/$CHANNEL/manifest.json"; res "ABORT_MANIFEST"; exit 1; }
|
|
51
|
+
# 读 manifest:mf apps.Admin.zip / mf ensureNoCache / mf eligible <deviceId>
|
|
52
|
+
mf(){
|
|
53
|
+
printf '%s' "$MANIFEST" | python3 -c '
|
|
54
|
+
import json, sys
|
|
55
|
+
m = json.load(sys.stdin)
|
|
56
|
+
if sys.argv[1] == "eligible":
|
|
57
|
+
r = m.get("rollout")
|
|
58
|
+
if r is None:
|
|
59
|
+
print("yes"); sys.exit()
|
|
60
|
+
try:
|
|
61
|
+
d = int(sys.argv[2])
|
|
62
|
+
except ValueError:
|
|
63
|
+
print("no"); sys.exit()
|
|
64
|
+
ok = d in [int(x) for x in r.get("devices", [])] or d % 100 < int(r.get("percent", 0))
|
|
65
|
+
print("yes" if ok else "no"); sys.exit()
|
|
66
|
+
v = m
|
|
67
|
+
for k in sys.argv[1].split("."):
|
|
68
|
+
v = v.get(k) if isinstance(v, dict) else None
|
|
69
|
+
print("" if v is None else (str(v).lower() if isinstance(v, bool) else v))
|
|
70
|
+
' "$@"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# ---- 灰度门 ----
|
|
74
|
+
DEVICE_ID="${APPCENTER_DEVICE_ID:-}"
|
|
75
|
+
if [ -z "$DEVICE_ID" ] || [ "$DEVICE_ID" = "-1" ]; then
|
|
76
|
+
DEVICE_ID=$(curl -s --max-time 10 "http://localhost/PHP/AccessLog.php?op=deviceId" \
|
|
77
|
+
| python3 -c 'import json,sys; print(json.load(sys.stdin).get("content",""))' 2>/dev/null)
|
|
78
|
+
fi
|
|
79
|
+
if [ "$(mf eligible "${DEVICE_ID:-x}")" != "yes" ]; then
|
|
80
|
+
res "SKIP_ROLLOUT device=${DEVICE_ID:-?}"; exit 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
FAILED=0
|
|
84
|
+
# ---- index.html no-cache(先于换前端:让之后拿到的 index.html 都不再被启发式缓存)----
|
|
85
|
+
if [ "$(mf ensureNoCache)" = "true" ]; then
|
|
86
|
+
NC=$(bash "$HERE/ensureAppCenterNoCache.sh"); NC_CODE=$?
|
|
87
|
+
printf '%s\n' "$NC" | sed '$d'
|
|
88
|
+
res "$(printf '%s\n' "$NC" | tail -1)"
|
|
89
|
+
[ "$NC_CODE" = 0 ] || FAILED=1
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# 清单第 67 列起是路径(64 位 hash + 两个空格)
|
|
93
|
+
paths(){ cut -c67- "$1" | LC_ALL=C sort; }
|
|
94
|
+
tree_ok(){ # $1=目录 $2=期望 treeSha256
|
|
95
|
+
[ -f "$1/.appcenter-manifest" ] \
|
|
96
|
+
&& [ "$(sha256sum < "$1/.appcenter-manifest" | cut -d' ' -f1)" = "$2" ] \
|
|
97
|
+
&& (cd "$1" && sha256sum -c --quiet --strict .appcenter-manifest >/dev/null 2>&1)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
sync_app(){
|
|
101
|
+
local APP="$1" LIVE="$WEB/$1" NEW="$STAGE/$1" TREE ZIP ZSHA
|
|
102
|
+
TREE=$(mf "apps.$APP.treeSha256"); ZIP=$(mf "apps.$APP.zip"); ZSHA=$(mf "apps.$APP.zipSha256")
|
|
103
|
+
if [ -z "$TREE" ]; then res "APP_SKIP $APP 不在manifest里"; return 0; fi
|
|
104
|
+
# manifest 来自网络:只接受预期格式,防止路径穿越
|
|
105
|
+
if ! [[ "$TREE" =~ ^[0-9a-f]{64}$ && "$ZSHA" =~ ^[0-9a-f]{64}$ && "$ZIP" =~ ^blobs/[A-Za-z0-9._-]+\.zip$ ]]; then
|
|
106
|
+
res "APP_ABORT_BAD_MANIFEST $APP"; return 1
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# ---- manifest 门 ----
|
|
110
|
+
if tree_ok "$LIVE" "$TREE"; then res "APP_UP_TO_DATE $APP"; return 0; fi
|
|
111
|
+
echo "$APP: 本机与 manifest 不一致 → 同步到 ${TREE:0:12}"
|
|
112
|
+
|
|
113
|
+
# ---- 下载 + 校验(此时还没动线上目录)----
|
|
114
|
+
sudoS "rm -rf '$NEW' '$NEW.zip' '$NEW.failed'"
|
|
115
|
+
curl -fsSL --max-time 1200 "$BASE/$ZIP" -o "$NEW.zip" || { res "APP_ABORT_DOWNLOAD $APP"; return 1; }
|
|
116
|
+
[ "$(sha256sum "$NEW.zip" | cut -d' ' -f1)" = "$ZSHA" ] || { res "APP_ABORT_CHECKSUM $APP"; rm -f "$NEW.zip"; return 1; }
|
|
117
|
+
mkdir -p "$NEW" && unzip -q -o "$NEW.zip" -d "$NEW"; rm -f "$NEW.zip"
|
|
118
|
+
if ! { [ -f "$NEW/index.html" ] && tree_ok "$NEW" "$TREE"; }; then
|
|
119
|
+
res "APP_ABORT_BAD_ARTIFACT $APP"; rm -rf "$NEW"; return 1
|
|
120
|
+
fi
|
|
121
|
+
# zip 里带的是打包时的旧 mtime;浏览器按 Last-Modified 的年龄启发式缓存,所以改成现在(只改 mtime 不改内容,不影响校验)
|
|
122
|
+
touch "$NEW/index.html"
|
|
123
|
+
|
|
124
|
+
# ---- 保留上一代 ----
|
|
125
|
+
local CARRIED=0
|
|
126
|
+
if [ -d "$LIVE" ]; then
|
|
127
|
+
python3 "$HERE/appcenterRefs.py" "$LIVE" | LC_ALL=C sort > "$NEW.old" \
|
|
128
|
+
|| { res "APP_ABORT_REFS $APP"; rm -rf "$NEW" "$NEW.old"; return 1; }
|
|
129
|
+
paths "$NEW/.appcenter-manifest" > "$NEW.new"
|
|
130
|
+
LC_ALL=C comm -23 "$NEW.old" "$NEW.new" > "$NEW.carry"
|
|
131
|
+
CARRIED=$(wc -l < "$NEW.carry" | tr -d ' ')
|
|
132
|
+
# 用 tar 而不是 rsync:老设备不一定装了 rsync;清单里的文件都确实存在(appcenterRefs 只列存在的)
|
|
133
|
+
[ "$CARRIED" -gt 0 ] && tar -C "$LIVE" -cf - -T "$NEW.carry" | tar -C "$NEW" -xf -
|
|
134
|
+
rm -f "$NEW.old" "$NEW.new" "$NEW.carry"
|
|
135
|
+
fi
|
|
136
|
+
echo "$APP: 保留上一代文件 $CARRIED 个"
|
|
137
|
+
|
|
138
|
+
# ---- 替换:两次 rename,中间只有毫秒级空窗 ----
|
|
139
|
+
sudoS "mkdir -p '$BACKUP' && rm -rf '$BACKUP/$APP' && chown -R www-data:www-data '$NEW'"
|
|
140
|
+
[ -d "$LIVE" ] && sudoS "mv '$LIVE' '$BACKUP/$APP'"
|
|
141
|
+
sudoS "mv '$NEW' '$LIVE'"
|
|
142
|
+
|
|
143
|
+
# ---- 健康检查:首页、入口 js、文件校验 ----
|
|
144
|
+
local HEALTHY=1 MAIN c
|
|
145
|
+
MAIN=$(grep -oE 'src=["'"'"']?[^"'"'"' >]+\.js' "$LIVE/index.html" | grep -vE '//' | head -1 | sed -E 's/^src=["'"'"']?(\.\/)?//')
|
|
146
|
+
for p in "" "$MAIN"; do
|
|
147
|
+
c=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost/AppCenter/$APP/$p" --max-time 15)
|
|
148
|
+
echo " 健康检查 /AppCenter/$APP/$p -> $c"; [ "$c" = "200" ] || HEALTHY=0
|
|
149
|
+
done
|
|
150
|
+
tree_ok "$LIVE" "$TREE" || { echo " 线上目录校验失败"; HEALTHY=0; }
|
|
151
|
+
|
|
152
|
+
if [ "$HEALTHY" != "1" ]; then
|
|
153
|
+
sudoS "mv '$LIVE' '$NEW.failed'; [ -d '$BACKUP/$APP' ] && mv '$BACKUP/$APP' '$LIVE'"
|
|
154
|
+
res "APP_ROLLED_BACK $APP"; return 1
|
|
155
|
+
fi
|
|
156
|
+
res "APP_OK $APP ${TREE:0:12} carried=$CARRIED"
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# 不指定就同步 manifest 里的全部应用(营业中一直开着的 App/KDS 等,发布端先别放进 manifest)
|
|
160
|
+
APPS="${APPCENTER_APPS:-$(printf '%s' "$MANIFEST" | python3 -c 'import json, sys; print(" ".join(sorted(json.load(sys.stdin).get("apps", {}))))')}"
|
|
161
|
+
sudoS "mkdir -p '$STAGE' && chown $(id -un):$(id -gn) '$STAGE'"
|
|
162
|
+
for APP in $APPS; do
|
|
163
|
+
if ! [[ "$APP" =~ ^[A-Za-z0-9_-]+$ ]]; then res "APP_SKIP $APP 名字不合法"; continue; fi
|
|
164
|
+
sync_app "$APP" || FAILED=1
|
|
165
|
+
done
|
|
166
|
+
exit $FAILED
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""门店前端目录的实际访问量(aaden-cli appcenter-usage 的执行体)。只读 Apache access.log,结果 JSON 打到 stdout。
|
|
3
|
+
|
|
4
|
+
统计两类目录,应用名用 docroot 下的相对路径:
|
|
5
|
+
- AppCenter 下的应用 → "AppCenter/Admin"、"AppCenter/App" …
|
|
6
|
+
- docroot 根目录下带 index.html 的前端(老的 /Admin、/App 等)→ "Admin"、"App" …
|
|
7
|
+
一个请求归到某个目录,满足其一即可:
|
|
8
|
+
- 请求路径在该目录下(页面和静态资源)
|
|
9
|
+
- Referer 指向该目录(页面里发出的 PHP 接口请求;index.html 被浏览器缓存时只能靠这个看出有人在用)
|
|
10
|
+
来源按 Referer 主机名分:ngrok(远程。经 ik%04d.ngrok 进来的客户端 IP 都是 127.0.0.1,只能看 Referer)、
|
|
11
|
+
local(localhost/127.x,收银机本机)、lan(局域网 IP,平板等)、none(没有 Referer,一般是直接打开页面)。
|
|
12
|
+
只统计真实存在的目录,扫描器乱打的路径不算。
|
|
13
|
+
|
|
14
|
+
用法: appCenterUsage.py [日志目录,默认 /var/log/apache2];需要能读 access.log*(root 或 adm 组)。
|
|
15
|
+
"""
|
|
16
|
+
import glob
|
|
17
|
+
import gzip
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from collections import defaultdict
|
|
23
|
+
|
|
24
|
+
LOG_DIR = sys.argv[1] if len(sys.argv) > 1 else '/var/log/apache2'
|
|
25
|
+
DOCROOT = '/var/www/html'
|
|
26
|
+
APPCENTER = os.path.join(DOCROOT, 'AppCenter')
|
|
27
|
+
# 根目录下不是前端的目录
|
|
28
|
+
NOT_FRONTEND = {'AppCenter', 'PHP', 'Resource', 'TseHost', 'docs', 'scripts', 'ngrok', 'vendor', 'node_modules'}
|
|
29
|
+
LINE = re.compile(
|
|
30
|
+
r'^(?P<ip>\S+) \S+ \S+ \[(?P<ts>[^\]]+)\] "(?P<method>[A-Z]+) (?P<path>\S+)[^"]*" (?P<status>\d{3}) \S+ '
|
|
31
|
+
r'"(?P<ref>[^"]*)" "(?P<ua>[^"]*)"'
|
|
32
|
+
)
|
|
33
|
+
# /AppCenter/<App>/... 或 /<根目录前端>/...;第一组是 "AppCenter/"(可空),第二组是目录名
|
|
34
|
+
APP_IN_PATH = re.compile(r'^/(AppCenter/)?([^/?#]+)')
|
|
35
|
+
APP_IN_REF = re.compile(r'^https?://([^/]+)/(AppCenter/)?([^/?#]+)')
|
|
36
|
+
MONTHS = {m: i + 1 for i, m in enumerate('Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split())}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def source_of(ref_host):
|
|
40
|
+
if ref_host is None:
|
|
41
|
+
return 'none'
|
|
42
|
+
host = ref_host.split(':')[0].lower()
|
|
43
|
+
if 'ngrok' in host:
|
|
44
|
+
return 'ngrok'
|
|
45
|
+
if host == 'localhost' or host.startswith('127.'):
|
|
46
|
+
return 'local'
|
|
47
|
+
return 'lan'
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_ts(ts):
|
|
51
|
+
"""'03/Oct/2026:12:20:40 +0200' → ('2026-10-03', '2026-10-03T12:20:40'),比 strptime 快得多"""
|
|
52
|
+
d, mon, rest = ts[0:2], ts[3:6], ts[7:]
|
|
53
|
+
year, hms = rest[0:4], rest[5:13]
|
|
54
|
+
day = f'{year}-{MONTHS.get(mon, 0):02d}-{d}'
|
|
55
|
+
return day, f'{day}T{hms}'
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main():
|
|
59
|
+
installed = []
|
|
60
|
+
if os.path.isdir(APPCENTER):
|
|
61
|
+
installed += ['AppCenter/' + d for d in os.listdir(APPCENTER) if os.path.isdir(os.path.join(APPCENTER, d))]
|
|
62
|
+
if os.path.isdir(DOCROOT):
|
|
63
|
+
installed += [
|
|
64
|
+
d for d in os.listdir(DOCROOT)
|
|
65
|
+
if d not in NOT_FRONTEND and not d.startswith('.') and os.path.isfile(os.path.join(DOCROOT, d, 'index.html'))
|
|
66
|
+
]
|
|
67
|
+
installed.sort()
|
|
68
|
+
known = set(installed)
|
|
69
|
+
# 快速预筛:行里得出现某个已知目录的路径段,绝大多数 PHP 轮询行直接跳过
|
|
70
|
+
roots = sorted({k.split('/')[0] for k in known}, key=len, reverse=True)
|
|
71
|
+
prefilter = re.compile('/(?:' + '|'.join(re.escape(r) for r in roots) + ')[/?# "]') if roots else None
|
|
72
|
+
stats = defaultdict(lambda: {
|
|
73
|
+
'pageLoads': 0, 'requests': 0, 'clients': set(),
|
|
74
|
+
'sources': {'none': 0, 'local': 0, 'lan': 0, 'ngrok': 0}, 'lastSeen': '',
|
|
75
|
+
})
|
|
76
|
+
log_from = log_to = None
|
|
77
|
+
total = 0
|
|
78
|
+
for f in sorted(glob.glob(os.path.join(LOG_DIR, 'access.log*'))):
|
|
79
|
+
opener = gzip.open if f.endswith('.gz') else open
|
|
80
|
+
try:
|
|
81
|
+
fh = opener(f, 'rt', errors='replace')
|
|
82
|
+
except OSError:
|
|
83
|
+
continue
|
|
84
|
+
with fh:
|
|
85
|
+
for line in fh:
|
|
86
|
+
total += 1
|
|
87
|
+
# 时间范围按所有行算,统计只看和 AppCenter 有关的行
|
|
88
|
+
lb = line.find('[')
|
|
89
|
+
if lb > 0:
|
|
90
|
+
day, at = parse_ts(line[lb + 1:lb + 21])
|
|
91
|
+
if log_from is None or at < log_from:
|
|
92
|
+
log_from = at
|
|
93
|
+
if log_to is None or at > log_to:
|
|
94
|
+
log_to = at
|
|
95
|
+
if prefilter is None or not prefilter.search(line):
|
|
96
|
+
continue
|
|
97
|
+
m = LINE.match(line)
|
|
98
|
+
if not m:
|
|
99
|
+
continue
|
|
100
|
+
path, ref = m['path'], m['ref']
|
|
101
|
+
pm = APP_IN_PATH.match(path)
|
|
102
|
+
rm = APP_IN_REF.match(ref) if ref and ref != '-' else None
|
|
103
|
+
path_app = (pm.group(1) or '') + pm.group(2) if pm else None
|
|
104
|
+
ref_app = (rm.group(2) or '') + rm.group(3) if rm else None
|
|
105
|
+
if path_app not in known:
|
|
106
|
+
path_app = None
|
|
107
|
+
if ref_app not in known:
|
|
108
|
+
ref_app, rm = None, None
|
|
109
|
+
app = path_app or ref_app
|
|
110
|
+
if not app:
|
|
111
|
+
continue
|
|
112
|
+
day, at = parse_ts(m['ts'])
|
|
113
|
+
s = stats[(app, day)]
|
|
114
|
+
s['requests'] += 1
|
|
115
|
+
bare = path.split('?')[0].split('#')[0]
|
|
116
|
+
if path_app and m['method'] == 'GET' and m['status'] in ('200', '304') and bare in (
|
|
117
|
+
f'/{app}', f'/{app}/', f'/{app}/index.html'):
|
|
118
|
+
s['pageLoads'] += 1
|
|
119
|
+
ref_host = rm.group(1) if rm else None
|
|
120
|
+
s['clients'].add((m['ip'], ref_host or '-', m['ua'][:120]))
|
|
121
|
+
s['sources'][source_of(ref_host)] += 1
|
|
122
|
+
if at > s['lastSeen']:
|
|
123
|
+
s['lastSeen'] = at
|
|
124
|
+
out = {
|
|
125
|
+
'logFrom': log_from,
|
|
126
|
+
'logTo': log_to,
|
|
127
|
+
'totalRequests': total,
|
|
128
|
+
'installedApps': installed,
|
|
129
|
+
'usage': [
|
|
130
|
+
{
|
|
131
|
+
'app': app, 'day': day, 'pageLoads': s['pageLoads'], 'requests': s['requests'],
|
|
132
|
+
'clients': len(s['clients']), 'sources': s['sources'], 'lastSeen': s['lastSeen'],
|
|
133
|
+
}
|
|
134
|
+
for (app, day), s in sorted(stats.items())
|
|
135
|
+
],
|
|
136
|
+
}
|
|
137
|
+
json.dump(out, sys.stdout, ensure_ascii=False, separators=(',', ':'))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == '__main__':
|
|
141
|
+
main()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
列出一个 AppCenter 应用目录里「当前这版 index.html 实际用得到」的文件(相对路径,一行一个,只列存在的)。
|
|
4
|
+
|
|
5
|
+
appCenterSync.sh 用它保留上一代:替换目录前,把旧目录里这些文件中新版没有的拷进新目录,
|
|
6
|
+
正开着的旧页面懒加载 chunk 时就不会 404。再往前历次手工覆盖堆下来的文件不在这个闭包里,顺带清掉。
|
|
7
|
+
|
|
8
|
+
闭包算法与 aaden-php-backend scripts/appcenter_artifact.py 的 referenced() 相同,改一边记得改另一边:
|
|
9
|
+
index.html 的 src/href → webpack runtime 的 chunk→hash 表 → js/css 文本里的 hash 资源路径。
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
ATTR = re.compile(r'''(?:src|href)=["']?([^"'\s>]+)''')
|
|
16
|
+
RUNTIME = re.compile(r'(js|css)/"\+\(\{([^}]*)\}\[\w+\]\|\|\w+\)\+"\."\+\{([^}]*)\}')
|
|
17
|
+
PAIR = re.compile(r'"?([\w\-]+)"?:"([\w\-]+)"')
|
|
18
|
+
ASSET_REF = re.compile(r'(?<![\w\-])((?:js|css|img|fonts|media)/[\w\-.@]+\.[0-9a-f]{8}\.[A-Za-z0-9]+)')
|
|
19
|
+
PRECACHE = re.compile(r'precache-manifest\.[0-9a-f]+\.js')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def norm(ref):
|
|
23
|
+
ref = ref.split('?')[0].split('#')[0]
|
|
24
|
+
if re.match(r'^[a-z]+:|^//', ref):
|
|
25
|
+
return None
|
|
26
|
+
while ref.startswith('./'):
|
|
27
|
+
ref = ref[2:]
|
|
28
|
+
return ref.lstrip('/') or None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_text(path):
|
|
32
|
+
with open(path, 'rb') as f:
|
|
33
|
+
return f.read().decode('utf-8', 'replace')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main(root):
|
|
37
|
+
if not os.path.isfile(os.path.join(root, 'index.html')):
|
|
38
|
+
return
|
|
39
|
+
seen, found = set(), []
|
|
40
|
+
queue = [r for r in (norm(m) for m in ATTR.findall(read_text(os.path.join(root, 'index.html')))) if r]
|
|
41
|
+
queue.append('index.html')
|
|
42
|
+
sw = os.path.join(root, 'service-worker.js')
|
|
43
|
+
if os.path.exists(sw):
|
|
44
|
+
queue.append('service-worker.js')
|
|
45
|
+
queue += PRECACHE.findall(read_text(sw))
|
|
46
|
+
while queue:
|
|
47
|
+
ref = queue.pop()
|
|
48
|
+
if ref in seen:
|
|
49
|
+
continue
|
|
50
|
+
seen.add(ref)
|
|
51
|
+
path = os.path.join(root, ref)
|
|
52
|
+
if not os.path.isfile(path):
|
|
53
|
+
continue
|
|
54
|
+
found.append(ref)
|
|
55
|
+
if not ref.endswith(('.js', '.css')):
|
|
56
|
+
continue
|
|
57
|
+
text = read_text(path)
|
|
58
|
+
for kind, names, hashes in RUNTIME.findall(text):
|
|
59
|
+
name_of = dict(PAIR.findall(names))
|
|
60
|
+
for chunk, h in PAIR.findall(hashes):
|
|
61
|
+
queue.append('%s/%s.%s.%s' % (kind, name_of.get(chunk, chunk), h, kind))
|
|
62
|
+
queue += ASSET_REF.findall(text)
|
|
63
|
+
sys.stdout.write(''.join(r + '\n' for r in sorted(found)))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == '__main__':
|
|
67
|
+
main(sys.argv[1])
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 确保 AppCenter 下各前端的 index.html 带 `Cache-Control: no-cache`。由 appCenterSync.sh 在 manifest 打开
|
|
3
|
+
# ensureNoCache 时调用(受 manifest 的灰度门控制);幂等,已生效时只做几个文件比较就返回。
|
|
4
|
+
#
|
|
5
|
+
# 病症:设备 Apache 默认没开 mod_headers,index.html 只带 Last-Modified/ETag,浏览器按「文件年龄 × 10%」自行缓存,
|
|
6
|
+
# 这段时间里同一个 URL 直接用缓存、不问服务器。aaden-vue-app 打开后台带的 chaos= 是收银程序启动时间,
|
|
7
|
+
# 程序不重启 URL 就不变 → appcenter-sync 换了新版也看不到;期间若又发一版,缓存里的旧 index.html
|
|
8
|
+
# 去请求已被清掉的旧 chunk → 白屏。js/css 文件名带内容 hash,不受影响,所以只管 index.html。
|
|
9
|
+
# no-cache = 每次用之前先向服务器确认,不是不缓存。(mod_deflate 给 ETag 加了 -gzip,条件请求对不上,
|
|
10
|
+
# 实际每次回 200 约 3KB 而不是 304,可忽略。)只对之后的新响应生效,已缓存的旧响应要等它自己过期。
|
|
11
|
+
#
|
|
12
|
+
# 风险控制(配置写坏时 reload 会被 configtest 拦住、继续跑旧配置,但下次重启 Apache 就起不来、收银全停):
|
|
13
|
+
# - 动手前先 configtest:本来就过不了的设备一律不碰
|
|
14
|
+
# - 配置包在 <IfModule> 里;写完立刻 configtest,不过就原样撤回(删配置、关掉本次才开的模块)
|
|
15
|
+
# - 只 graceful reload,不 restart:正在处理的请求(打印、TSE 签名)不被打断
|
|
16
|
+
# 末行输出:NOCACHE_OK(已生效)| NOCACHE_APPLIED | NOCACHE_SKIP_<原因> | NOCACHE_REVERTED | NOCACHE_FAILED
|
|
17
|
+
# 退出码:只有 NOCACHE_FAILED(撤回后配置仍坏 / reload 失败)为 1
|
|
18
|
+
# 撤销:a2disconf aaden-appcenter-cache && systemctl reload apache2
|
|
19
|
+
set -u
|
|
20
|
+
NAME=aaden-appcenter-cache
|
|
21
|
+
CONF=/etc/apache2/conf-available/$NAME.conf
|
|
22
|
+
PW='Innerken322.'
|
|
23
|
+
sudoS(){ echo "$PW" | sudo -S bash -c "$*"; }
|
|
24
|
+
configtest(){ sudoS "apache2ctl configtest" >/dev/null 2>&1; }
|
|
25
|
+
|
|
26
|
+
if ! command -v apache2ctl >/dev/null || ! command -v a2enmod >/dev/null; then
|
|
27
|
+
echo " 没有 apache2ctl/a2enmod(不是 apt 装的 Apache?),不碰"; echo "NOCACHE_SKIP_NO_APACHE"; exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
TMP=$(mktemp)
|
|
31
|
+
trap 'rm -f "$TMP" "$TMP.old"' EXIT
|
|
32
|
+
cat > "$TMP" <<'EOF'
|
|
33
|
+
# 由 aaden-cli AadenDeploy/ensureAppCenterNoCache.sh 维护,手改会被覆盖。
|
|
34
|
+
# AppCenter 前端的入口页每次先向服务器确认,避免更新后还在用缓存里的旧 index.html。
|
|
35
|
+
# 只匹配 /var/www/html/AppCenter 下的 index.html;PHP 接口、js/css、根目录 /App/ /Admin/ 不受影响。
|
|
36
|
+
<IfModule mod_headers.c>
|
|
37
|
+
<Directory /var/www/html/AppCenter>
|
|
38
|
+
<FilesMatch "^index\.html$">
|
|
39
|
+
Header set Cache-Control "no-cache"
|
|
40
|
+
</FilesMatch>
|
|
41
|
+
</Directory>
|
|
42
|
+
</IfModule>
|
|
43
|
+
EOF
|
|
44
|
+
|
|
45
|
+
MOD_WAS_ON=0; [ -e /etc/apache2/mods-enabled/headers.load ] && MOD_WAS_ON=1
|
|
46
|
+
CONF_WAS_ON=0; [ -e "/etc/apache2/conf-enabled/$NAME.conf" ] && CONF_WAS_ON=1
|
|
47
|
+
if [ "$MOD_WAS_ON" = 1 ] && [ "$CONF_WAS_ON" = 1 ] && cmp -s "$TMP" "$CONF"; then
|
|
48
|
+
echo "NOCACHE_OK"; exit 0
|
|
49
|
+
fi
|
|
50
|
+
if ! configtest; then
|
|
51
|
+
echo " Apache 配置本来就过不了 configtest,不碰(这台设备需要人工处理)"; echo "NOCACHE_SKIP_CONFIG_BROKEN"; exit 0
|
|
52
|
+
fi
|
|
53
|
+
HAD_CONF=0; [ -f "$CONF" ] && { HAD_CONF=1; cp "$CONF" "$TMP.old"; }
|
|
54
|
+
|
|
55
|
+
# 撤回到动手前的状态:只关本次才开的模块/配置,原来就有的不碰
|
|
56
|
+
revert(){
|
|
57
|
+
[ "$CONF_WAS_ON" = 0 ] && sudoS "a2disconf -q $NAME" >/dev/null 2>&1
|
|
58
|
+
if [ "$HAD_CONF" = 1 ]; then sudoS "cp '$TMP.old' '$CONF'"; else sudoS "rm -f '$CONF'"; fi
|
|
59
|
+
[ "$MOD_WAS_ON" = 0 ] && sudoS "a2dismod -q headers" >/dev/null 2>&1
|
|
60
|
+
if configtest; then echo " 已撤回,Apache 配置恢复正常"; return 0; fi
|
|
61
|
+
echo " !!! 撤回后 configtest 仍失败,需要人工处理(重启前务必修好)"; return 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
sudoS "cp '$TMP' '$CONF' && chmod 644 '$CONF'"
|
|
65
|
+
[ "$MOD_WAS_ON" = 1 ] || sudoS "a2enmod -q headers" >/dev/null
|
|
66
|
+
[ "$CONF_WAS_ON" = 1 ] || sudoS "a2enconf -q $NAME" >/dev/null
|
|
67
|
+
|
|
68
|
+
if ! configtest; then
|
|
69
|
+
echo " 写入后 configtest 失败(未 reload,线上仍是旧配置),撤回"
|
|
70
|
+
if revert; then echo "NOCACHE_REVERTED"; exit 0; fi
|
|
71
|
+
echo "NOCACHE_FAILED"; exit 1
|
|
72
|
+
fi
|
|
73
|
+
if ! sudoS "systemctl reload apache2 || apache2ctl graceful"; then
|
|
74
|
+
echo " reload 失败,撤回"
|
|
75
|
+
revert; echo "NOCACHE_FAILED"; exit 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# graceful 后新配置由新起的子进程接手,稍等再验
|
|
79
|
+
sleep 2
|
|
80
|
+
if [ -f /var/www/html/AppCenter/Admin/index.html ]; then
|
|
81
|
+
H=$(curl -sI --max-time 10 http://localhost/AppCenter/Admin/ | tr -d '\r' | grep -i '^cache-control:')
|
|
82
|
+
echo " 验证 /AppCenter/Admin/ -> ${H:-(没有 Cache-Control 头!)}"
|
|
83
|
+
fi
|
|
84
|
+
echo "NOCACHE_APPLIED"
|
package/AadenDeploy/upgrade.sh
CHANGED
|
@@ -17,7 +17,14 @@ sudo npm i aaden-cli -g
|
|
|
17
17
|
|
|
18
18
|
aaden-cli patch
|
|
19
19
|
|
|
20
|
+
# 前端目录访问量上报(AppCenter 各应用 + 根目录老前端;只读 Apache 日志,20 小时内报过就空跑)。放在 light-update 前面,后端部署出问题也不影响统计
|
|
21
|
+
aaden-cli appcenter-usage
|
|
22
|
+
|
|
20
23
|
# 后端也走 aaden-cli 通道自愈:light-update 自带 版本门/灰度门/活动单门,
|
|
21
24
|
# 已最新则空跑,营业时间被 @reboot 触发也不会打断收银(不 snap refresh 前端、不 apt)。
|
|
22
25
|
# 这条让"后端自动更新"重新依赖活着的 npm+S3 管线,而不是失效的 git token。
|
|
23
26
|
aaden-cli light-update
|
|
27
|
+
|
|
28
|
+
# AppCenter 前端(Admin 等)单独同步:light-update 不发 AppCenter。灰度由 S3 manifest 的 rollout 控制,
|
|
29
|
+
# 不在灰度内 / 已最新时空跑(一次几 KB 的 manifest 请求);不碰 PHP 和数据库,营业时间触发也安全。
|
|
30
|
+
aaden-cli appcenter-sync
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const util = __nccwpck_require__(3837)
|
|
9
9
|
const colors = __nccwpck_require__(3045)
|
|
10
|
-
const {spawnSync, spawn} = __nccwpck_require__(
|
|
10
|
+
const {spawnSync, spawn} = __nccwpck_require__(2081)
|
|
11
11
|
|
|
12
12
|
function getLine(char, lineLength = 20) {
|
|
13
13
|
return char.repeat(lineLength)
|
|
@@ -187,7 +187,7 @@ async function getNiceRestaurantInfo() {
|
|
|
187
187
|
|
|
188
188
|
const os = __nccwpck_require__(2037)
|
|
189
189
|
const fs = __nccwpck_require__(7147)
|
|
190
|
-
const {execSync} = __nccwpck_require__(
|
|
190
|
+
const {execSync} = __nccwpck_require__(2081)
|
|
191
191
|
|
|
192
192
|
// do-log 顺带上报的宿主机信息,用来在云端看车队的系统/硬件分布。
|
|
193
193
|
// 每一项单独兜底:探测失败就报 null,不能让 do-log 本身挂掉。
|
|
@@ -3149,7 +3149,7 @@ module.exports = {
|
|
|
3149
3149
|
*/
|
|
3150
3150
|
|
|
3151
3151
|
const EventEmitter = (__nccwpck_require__(2361).EventEmitter);
|
|
3152
|
-
const spawn = (__nccwpck_require__(
|
|
3152
|
+
const spawn = (__nccwpck_require__(2081).spawn);
|
|
3153
3153
|
const path = __nccwpck_require__(1017);
|
|
3154
3154
|
const fs = __nccwpck_require__(7147);
|
|
3155
3155
|
|
|
@@ -6921,7 +6921,7 @@ module.exports = (flag, argv = process.argv) => {
|
|
|
6921
6921
|
|
|
6922
6922
|
/***/ }),
|
|
6923
6923
|
|
|
6924
|
-
/***/
|
|
6924
|
+
/***/ 7299:
|
|
6925
6925
|
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
|
|
6926
6926
|
|
|
6927
6927
|
"use strict";
|
|
@@ -7094,7 +7094,7 @@ Object.defineProperty(exports, "__esModule", ({
|
|
|
7094
7094
|
}));
|
|
7095
7095
|
exports["default"] = void 0;
|
|
7096
7096
|
|
|
7097
|
-
var _http = __nccwpck_require__(
|
|
7097
|
+
var _http = __nccwpck_require__(7299);
|
|
7098
7098
|
|
|
7099
7099
|
var _sweetalert = _interopRequireDefault(__nccwpck_require__(6091));
|
|
7100
7100
|
|
|
@@ -7283,7 +7283,7 @@ exports["default"] = exports.hillo = void 0;
|
|
|
7283
7283
|
|
|
7284
7284
|
var _request = _interopRequireDefault(__nccwpck_require__(7798));
|
|
7285
7285
|
|
|
7286
|
-
var _http = __nccwpck_require__(
|
|
7286
|
+
var _http = __nccwpck_require__(7299);
|
|
7287
7287
|
|
|
7288
7288
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
7289
7289
|
|
|
@@ -18453,7 +18453,7 @@ module.exports = require("assert");
|
|
|
18453
18453
|
|
|
18454
18454
|
/***/ }),
|
|
18455
18455
|
|
|
18456
|
-
/***/
|
|
18456
|
+
/***/ 2081:
|
|
18457
18457
|
/***/ ((module) => {
|
|
18458
18458
|
|
|
18459
18459
|
"use strict";
|
|
@@ -18577,7 +18577,7 @@ module.exports = JSON.parse('{"name":"axios","version":"0.19.2","description":"P
|
|
|
18577
18577
|
/***/ ((module) => {
|
|
18578
18578
|
|
|
18579
18579
|
"use strict";
|
|
18580
|
-
module.exports =
|
|
18580
|
+
module.exports = {"i8":"1.6.103"};
|
|
18581
18581
|
|
|
18582
18582
|
/***/ })
|
|
18583
18583
|
|
|
@@ -18657,6 +18657,9 @@ var __webpack_exports__ = {};
|
|
|
18657
18657
|
const dayjs = __nccwpck_require__(7401)
|
|
18658
18658
|
const colors = __nccwpck_require__(3045)
|
|
18659
18659
|
const fs = __nccwpck_require__(7147)
|
|
18660
|
+
const path = __nccwpck_require__(1017)
|
|
18661
|
+
const {execSync} = __nccwpck_require__(2081)
|
|
18662
|
+
const os = __nccwpck_require__(2037)
|
|
18660
18663
|
const InnerKenTimeStampFormat = 'YYYYMMDDHHmmss'
|
|
18661
18664
|
const prompts = __nccwpck_require__(9646)
|
|
18662
18665
|
const {deviceEcho, logAction, getCurrentDeviceId} = __nccwpck_require__(7415)
|
|
@@ -18681,6 +18684,8 @@ program
|
|
|
18681
18684
|
.option('-d, --debug', 'use Debug Out put')
|
|
18682
18685
|
program.command('update [channel]').action(update)
|
|
18683
18686
|
program.command('light-update').action(lightUpdate) // 轻量+带闸门的后端部署,供 auto-upgrade/patch 调用
|
|
18687
|
+
program.command('appcenter-usage [force]').action(appCenterUsage) // 门店前端目录(AppCenter 各应用 + 根目录老前端)访问量上报云端,供 auto-upgrade 每天调一次
|
|
18688
|
+
program.command('appcenter-sync [channel]').action(appCenterSync) // AppCenter 前端按应用同步,供 auto-upgrade 调用
|
|
18684
18689
|
program.command('rollback').action(rollback)
|
|
18685
18690
|
program
|
|
18686
18691
|
.command('install <deviceId>')
|
|
@@ -19233,6 +19238,78 @@ async function lightUpdate() {
|
|
|
19233
19238
|
}
|
|
19234
19239
|
}
|
|
19235
19240
|
|
|
19241
|
+
/**
|
|
19242
|
+
* 门店前端目录的实际访问量 → cloud(ERP「租户管理」里看全网哪些目录还有人用、从哪儿访问)。
|
|
19243
|
+
* 包括 AppCenter 下各应用("AppCenter/Admin")和 docroot 根目录的老前端("Admin"、"App")。
|
|
19244
|
+
* 统计在 AadenDeploy/appCenterUsage.py(只读 Apache access.log,按 目录×天 汇总)。
|
|
19245
|
+
* Apache 日志保留 14 天,所以每次都把整个窗口重报一遍,云端按 设备×应用×天 覆盖;新装上的第一次就能带回两周历史。
|
|
19246
|
+
* 每天随 auto-upgrade 跑一次,20 小时内报过就跳过(@reboot 也会触发 auto-upgrade),传 force 跳过这道门。
|
|
19247
|
+
* 最低 CPU/IO 优先级;失败只打印,不写 deviceLog 事件(免得 650 台每天各刷一条)。
|
|
19248
|
+
*/
|
|
19249
|
+
async function appCenterUsage(force) {
|
|
19250
|
+
const stamp = '/var/tmp/aaden-appcenter-usage.last'
|
|
19251
|
+
try {
|
|
19252
|
+
if (force !== 'force' && fs.existsSync(stamp) && Date.now() - fs.statSync(stamp).mtimeMs < 20 * 3600 * 1000) {
|
|
19253
|
+
console.log('appcenter-usage: 20 小时内已上报,跳过')
|
|
19254
|
+
return
|
|
19255
|
+
}
|
|
19256
|
+
const deviceId = await getCurrentDeviceId()
|
|
19257
|
+
if (parseInt(deviceId) === -1) {
|
|
19258
|
+
return
|
|
19259
|
+
}
|
|
19260
|
+
const script = path.join(findMe(), '..', 'AadenDeploy', 'appCenterUsage.py')
|
|
19261
|
+
const output = execSync(
|
|
19262
|
+
`echo "Innerken322." | sudo -S nice -n 19 $(command -v ionice >/dev/null && echo ionice -c3) python3 ${script}`,
|
|
19263
|
+
{encoding: 'utf-8', maxBuffer: 32 * 1024 * 1024, timeout: 15 * 60 * 1000, stdio: ['ignore', 'pipe', 'ignore']}
|
|
19264
|
+
)
|
|
19265
|
+
const report = JSON.parse(output)
|
|
19266
|
+
// 和 do-log 一样按 deviceId 错开,别让全网同一秒打云端
|
|
19267
|
+
await new Promise(resolve => setTimeout(resolve, (parseInt(deviceId) % 30) * 1000))
|
|
19268
|
+
await hillo.jsonPost('https://cloud-v2.aaden.io/deviceLog/appUsage', {deviceId, cliVersion: version, ...report})
|
|
19269
|
+
try {
|
|
19270
|
+
fs.writeFileSync(stamp, new Date().toISOString())
|
|
19271
|
+
} catch (_) {}
|
|
19272
|
+
console.log(`appcenter-usage: 已上报 ${report.usage.length} 条(${report.logFrom} ~ ${report.logTo})`)
|
|
19273
|
+
} catch (e) {
|
|
19274
|
+
console.error('appcenter-usage 失败:', e.message || e)
|
|
19275
|
+
}
|
|
19276
|
+
}
|
|
19277
|
+
|
|
19278
|
+
/**
|
|
19279
|
+
* AppCenter 前端同步 —— light-update 只发 PHP/,AppCenter 以前只能跟 465MB 的 latest.zip 走。
|
|
19280
|
+
* 每天随 auto-upgrade 跑一次;不跟后端版本门绑定(只改前端时后端版本不变,绑了就永远不跑)。
|
|
19281
|
+
*
|
|
19282
|
+
* 全部逻辑在 AadenDeploy/appCenterSync.sh:灰度门(S3 manifest 的 rollout)、index.html no-cache、
|
|
19283
|
+
* treeSha256 门(一致就空跑)、下载校验、保留上一代、换目录、健康检查失败回滚。
|
|
19284
|
+
* 这里只负责传 deviceId、把有动作的结果写进 cloud 事件日志 —— 空跑(已最新 / 不在灰度内)不上报,
|
|
19285
|
+
* 免得 650 台每天各刷一条。
|
|
19286
|
+
*/
|
|
19287
|
+
async function appCenterSync(channel = 'stable') {
|
|
19288
|
+
const resultFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'aaden-appcenter-')), 'result.txt')
|
|
19289
|
+
try {
|
|
19290
|
+
const deviceId = await getCurrentDeviceId()
|
|
19291
|
+
const command = [
|
|
19292
|
+
...getPrivilegeCMD('AadenDeploy/appCenterSync.sh'),
|
|
19293
|
+
'cd AadenDeploy',
|
|
19294
|
+
`APPCENTER_DEVICE_ID=${deviceId} APPCENTER_RESULT=${resultFile} ./appCenterSync.sh ${channel}`
|
|
19295
|
+
]
|
|
19296
|
+
const {code} = await cli.executeInShell(command, program.debug)
|
|
19297
|
+
let lines = []
|
|
19298
|
+
try {
|
|
19299
|
+
lines = fs.readFileSync(resultFile, 'utf8').split('\n').filter(l => l.trim())
|
|
19300
|
+
} catch (_) {}
|
|
19301
|
+
const quiet = /^(APP_UP_TO_DATE|SKIP_ROLLOUT|SKIP_LOCKED|NOCACHE_OK)\b/
|
|
19302
|
+
if (code !== 0 || lines.some(l => !quiet.test(l))) {
|
|
19303
|
+
await logAction('appcenter-sync: ' + (lines.join(' | ') || '无结果输出'), code || 0)
|
|
19304
|
+
}
|
|
19305
|
+
} catch (e) {
|
|
19306
|
+
console.error(e)
|
|
19307
|
+
try { await logAction('appcenter-sync 异常: ' + (e.message || e), 1) } catch (_) {}
|
|
19308
|
+
} finally {
|
|
19309
|
+
try { fs.rmSync(path.dirname(resultFile), {recursive: true, force: true}) } catch (_) {}
|
|
19310
|
+
}
|
|
19311
|
+
}
|
|
19312
|
+
|
|
19236
19313
|
/** 比较点分数字版本,a > b 返回 true。'1.9.61' > '1.7.908' === true。 */
|
|
19237
19314
|
function versionGt(a, b) {
|
|
19238
19315
|
const pa = String(a).split('.').map(n => parseInt(n) || 0)
|
package/index.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
const dayjs = require('dayjs')
|
|
7
7
|
const colors = require('colors')
|
|
8
8
|
const fs = require('fs')
|
|
9
|
+
const path = require('path')
|
|
10
|
+
const {execSync} = require('child_process')
|
|
11
|
+
const os = require('os')
|
|
9
12
|
const InnerKenTimeStampFormat = 'YYYYMMDDHHmmss'
|
|
10
13
|
const prompts = require('prompts')
|
|
11
14
|
const {deviceEcho, logAction, getCurrentDeviceId} = require('./modules/firebase')
|
|
@@ -30,6 +33,8 @@ program
|
|
|
30
33
|
.option('-d, --debug', 'use Debug Out put')
|
|
31
34
|
program.command('update [channel]').action(update)
|
|
32
35
|
program.command('light-update').action(lightUpdate) // 轻量+带闸门的后端部署,供 auto-upgrade/patch 调用
|
|
36
|
+
program.command('appcenter-usage [force]').action(appCenterUsage) // 门店前端目录(AppCenter 各应用 + 根目录老前端)访问量上报云端,供 auto-upgrade 每天调一次
|
|
37
|
+
program.command('appcenter-sync [channel]').action(appCenterSync) // AppCenter 前端按应用同步,供 auto-upgrade 调用
|
|
33
38
|
program.command('rollback').action(rollback)
|
|
34
39
|
program
|
|
35
40
|
.command('install <deviceId>')
|
|
@@ -582,6 +587,78 @@ async function lightUpdate() {
|
|
|
582
587
|
}
|
|
583
588
|
}
|
|
584
589
|
|
|
590
|
+
/**
|
|
591
|
+
* 门店前端目录的实际访问量 → cloud(ERP「租户管理」里看全网哪些目录还有人用、从哪儿访问)。
|
|
592
|
+
* 包括 AppCenter 下各应用("AppCenter/Admin")和 docroot 根目录的老前端("Admin"、"App")。
|
|
593
|
+
* 统计在 AadenDeploy/appCenterUsage.py(只读 Apache access.log,按 目录×天 汇总)。
|
|
594
|
+
* Apache 日志保留 14 天,所以每次都把整个窗口重报一遍,云端按 设备×应用×天 覆盖;新装上的第一次就能带回两周历史。
|
|
595
|
+
* 每天随 auto-upgrade 跑一次,20 小时内报过就跳过(@reboot 也会触发 auto-upgrade),传 force 跳过这道门。
|
|
596
|
+
* 最低 CPU/IO 优先级;失败只打印,不写 deviceLog 事件(免得 650 台每天各刷一条)。
|
|
597
|
+
*/
|
|
598
|
+
async function appCenterUsage(force) {
|
|
599
|
+
const stamp = '/var/tmp/aaden-appcenter-usage.last'
|
|
600
|
+
try {
|
|
601
|
+
if (force !== 'force' && fs.existsSync(stamp) && Date.now() - fs.statSync(stamp).mtimeMs < 20 * 3600 * 1000) {
|
|
602
|
+
console.log('appcenter-usage: 20 小时内已上报,跳过')
|
|
603
|
+
return
|
|
604
|
+
}
|
|
605
|
+
const deviceId = await getCurrentDeviceId()
|
|
606
|
+
if (parseInt(deviceId) === -1) {
|
|
607
|
+
return
|
|
608
|
+
}
|
|
609
|
+
const script = path.join(findMe(), '..', 'AadenDeploy', 'appCenterUsage.py')
|
|
610
|
+
const output = execSync(
|
|
611
|
+
`echo "Innerken322." | sudo -S nice -n 19 $(command -v ionice >/dev/null && echo ionice -c3) python3 ${script}`,
|
|
612
|
+
{encoding: 'utf-8', maxBuffer: 32 * 1024 * 1024, timeout: 15 * 60 * 1000, stdio: ['ignore', 'pipe', 'ignore']}
|
|
613
|
+
)
|
|
614
|
+
const report = JSON.parse(output)
|
|
615
|
+
// 和 do-log 一样按 deviceId 错开,别让全网同一秒打云端
|
|
616
|
+
await new Promise(resolve => setTimeout(resolve, (parseInt(deviceId) % 30) * 1000))
|
|
617
|
+
await hillo.jsonPost('https://cloud-v2.aaden.io/deviceLog/appUsage', {deviceId, cliVersion: version, ...report})
|
|
618
|
+
try {
|
|
619
|
+
fs.writeFileSync(stamp, new Date().toISOString())
|
|
620
|
+
} catch (_) {}
|
|
621
|
+
console.log(`appcenter-usage: 已上报 ${report.usage.length} 条(${report.logFrom} ~ ${report.logTo})`)
|
|
622
|
+
} catch (e) {
|
|
623
|
+
console.error('appcenter-usage 失败:', e.message || e)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* AppCenter 前端同步 —— light-update 只发 PHP/,AppCenter 以前只能跟 465MB 的 latest.zip 走。
|
|
629
|
+
* 每天随 auto-upgrade 跑一次;不跟后端版本门绑定(只改前端时后端版本不变,绑了就永远不跑)。
|
|
630
|
+
*
|
|
631
|
+
* 全部逻辑在 AadenDeploy/appCenterSync.sh:灰度门(S3 manifest 的 rollout)、index.html no-cache、
|
|
632
|
+
* treeSha256 门(一致就空跑)、下载校验、保留上一代、换目录、健康检查失败回滚。
|
|
633
|
+
* 这里只负责传 deviceId、把有动作的结果写进 cloud 事件日志 —— 空跑(已最新 / 不在灰度内)不上报,
|
|
634
|
+
* 免得 650 台每天各刷一条。
|
|
635
|
+
*/
|
|
636
|
+
async function appCenterSync(channel = 'stable') {
|
|
637
|
+
const resultFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'aaden-appcenter-')), 'result.txt')
|
|
638
|
+
try {
|
|
639
|
+
const deviceId = await getCurrentDeviceId()
|
|
640
|
+
const command = [
|
|
641
|
+
...getPrivilegeCMD('AadenDeploy/appCenterSync.sh'),
|
|
642
|
+
'cd AadenDeploy',
|
|
643
|
+
`APPCENTER_DEVICE_ID=${deviceId} APPCENTER_RESULT=${resultFile} ./appCenterSync.sh ${channel}`
|
|
644
|
+
]
|
|
645
|
+
const {code} = await cli.executeInShell(command, program.debug)
|
|
646
|
+
let lines = []
|
|
647
|
+
try {
|
|
648
|
+
lines = fs.readFileSync(resultFile, 'utf8').split('\n').filter(l => l.trim())
|
|
649
|
+
} catch (_) {}
|
|
650
|
+
const quiet = /^(APP_UP_TO_DATE|SKIP_ROLLOUT|SKIP_LOCKED|NOCACHE_OK)\b/
|
|
651
|
+
if (code !== 0 || lines.some(l => !quiet.test(l))) {
|
|
652
|
+
await logAction('appcenter-sync: ' + (lines.join(' | ') || '无结果输出'), code || 0)
|
|
653
|
+
}
|
|
654
|
+
} catch (e) {
|
|
655
|
+
console.error(e)
|
|
656
|
+
try { await logAction('appcenter-sync 异常: ' + (e.message || e), 1) } catch (_) {}
|
|
657
|
+
} finally {
|
|
658
|
+
try { fs.rmSync(path.dirname(resultFile), {recursive: true, force: true}) } catch (_) {}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
585
662
|
/** 比较点分数字版本,a > b 返回 true。'1.9.61' > '1.7.908' === true。 */
|
|
586
663
|
function versionGt(a, b) {
|
|
587
664
|
const pa = String(a).split('.').map(n => parseInt(n) || 0)
|