aaden-cli 1.6.95 → 1.6.98
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/lightUpdate.sh +98 -7
- package/AadenDeploy/upgrade.sh +4 -1
- package/dist/index.js +100 -2
- package/dist/patch/202608181934restoreBackendViaS3.sh +4 -0
- package/dist/patch/202608190700removeGitDoUpdateCron.sh +39 -0
- package/index.js +99 -1
- package/package.json +2 -2
- package/patch/202608181934restoreBackendViaS3.sh +4 -0
- package/patch/202608190700removeGitDoUpdateCron.sh +39 -0
- package/scripts/fleet-rollout-status.py +55 -0
|
@@ -1,11 +1,102 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
+
# 轻量后端部署(light-update 的执行体)。
|
|
3
|
+
# - composer.lock 门:远端 raw/PHP/composer.lock == 本机? 相同→code.zip(~1MB,保留vendor);不同→latest.zip全量
|
|
4
|
+
# - **先下载校验,确认源存在后才做任何破坏性操作**(避免"删了迁移又没铺上")
|
|
5
|
+
# - 部署前 tar 备份 PHP 代码(排除 vendor);migrate 后健康检查;失败自动回滚
|
|
6
|
+
# - 绝不 snap refresh 前端、绝不 apt
|
|
7
|
+
# 退出码:0=成功(或无需更新),1=失败/已回滚。末行输出 DEPLOY_OK <ver> | ROLLED_BACK | ABORT_*
|
|
8
|
+
set -uo pipefail
|
|
9
|
+
S3="https://aaden-backend.s3.eu-central-1.amazonaws.com"
|
|
10
|
+
STAGE=/var/tmp/aaden-lu-stage # 绝对路径:download 与 sudo cp 必须看到同一个目录(~ 在 sudo 下会变 /root)
|
|
11
|
+
BACKUP=/var/lib/aaden-php-backup
|
|
12
|
+
PW='Innerken322.'
|
|
13
|
+
sudoS(){ echo "$PW" | sudo -S bash -c "$*"; }
|
|
2
14
|
|
|
3
|
-
|
|
4
|
-
|
|
15
|
+
# 早失败:老 aaden 设备的 sudo 密码可能不是共享密码。先探一次 ——
|
|
16
|
+
# 否则会一路穿到 ABORT_DOWNLOAD,把"密码不符"误报成"下载问题",排障跑偏。
|
|
17
|
+
# NOPASSWD 设备此探测同样通过(sudo 不问,管道密码被忽略)。
|
|
18
|
+
if ! echo "$PW" | sudo -S true 2>/dev/null; then echo "ABORT_SUDO_AUTH (aaden sudo 密码不符?)"; exit 1; fi
|
|
5
19
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
20
|
+
# ---- composer.lock 门 ----
|
|
21
|
+
REMOTE_LOCK=$(curl -s "$S3/raw/PHP/composer.lock" --max-time 30)
|
|
22
|
+
LOCAL_LOCK=$(cat /var/www/html/PHP/composer.lock 2>/dev/null)
|
|
23
|
+
RH=$(printf '%s' "$REMOTE_LOCK" | shasum -a 256 | cut -d' ' -f1)
|
|
24
|
+
LH=$(printf '%s' "$LOCAL_LOCK" | shasum -a 256 | cut -d' ' -f1)
|
|
25
|
+
if [ -n "$REMOTE_LOCK" ] && [ "$RH" = "$LH" ]; then
|
|
26
|
+
ARTIFACT="code.zip"; echo "composer.lock 一致 → 轻量 code.zip"
|
|
27
|
+
else
|
|
28
|
+
ARTIFACT="latest.zip"; echo "composer.lock 变化(或取不到) → 全量 latest.zip"
|
|
29
|
+
fi
|
|
9
30
|
|
|
10
|
-
|
|
11
|
-
|
|
31
|
+
# ---- 下载 + 解压到绝对暂存 ----
|
|
32
|
+
sudoS "rm -rf $STAGE && mkdir -p $STAGE && chown $(id -un):$(id -gn) $STAGE"
|
|
33
|
+
# 用 curl 而非 axel:axel 在设备上是 snap 包,受限写不进 /var/tmp。
|
|
34
|
+
# code.zip 仅 ~1MB,curl 瞬间完成;latest.zip 全量走 curl 稍慢但那是罕见 fallback。
|
|
35
|
+
ZIP="$STAGE/$ARTIFACT"
|
|
36
|
+
curl -fsSL "$S3/$ARTIFACT" -o "$ZIP" --max-time 1200
|
|
37
|
+
if [ ! -s "$ZIP" ]; then echo "下载失败或为空: $ARTIFACT"; echo "ABORT_DOWNLOAD"; exit 1; fi
|
|
38
|
+
unzip -q -o "$ZIP" -d "$STAGE" && rm -f "$ZIP"
|
|
39
|
+
|
|
40
|
+
# ---- 关键:破坏任何东西之前,先确认源 PHP 代码确实在 ----
|
|
41
|
+
if [ ! -f "$STAGE/PHP/composer.lock" ] || [ ! -d "$STAGE/PHP" ]; then
|
|
42
|
+
echo "源包缺 PHP/ 或 composer.lock,中止(未改动任何文件)"
|
|
43
|
+
echo "ABORT_BAD_ARTIFACT"; exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# ---- TSE 门:绝不在有进行中的交易/TSE 签名时部署 ----
|
|
47
|
+
# 结账把 START/FINISH 塞进 TseExecuteQueue,后台 worker 签名后置 finished=1(出错也置 1)。
|
|
48
|
+
# 信号 = "未完成 且 已到期 且 新鲜(<5min)":
|
|
49
|
+
# · 开着的桌吃两小时 → 不阻塞(TSE 任务早签完置 1 了)——正是要避免的"活动单门过严"
|
|
50
|
+
# · 正在签/待签的交易 → 有 finished=0 行 → 跳过等下轮 ——正是不能打断的瞬间
|
|
51
|
+
# · 5min 上界 → worker 挂了留下的卡死行不会永久拦住更新
|
|
52
|
+
# 紧贴 cp 之前查,消掉"下载几秒期间开了新交易"的竞态。DB 凭据从 config.ini 读(全网 aaden/共享密码)。
|
|
53
|
+
CONF=/var/www/html/Resource/config/config.ini
|
|
54
|
+
DBCREDS=$(sudoS "php -r '\$c=@parse_ini_file(\"$CONF\",true);\$d=\$c[\"DB\"]??[];echo (\$d[\"host\"]??\"localhost\").\"|\".(\$d[\"user\"]??\"aaden\").\"|\".(\$d[\"password\"]??\"\");'" 2>/dev/null)
|
|
55
|
+
DB_HOST="${DBCREDS%%|*}"; _r="${DBCREDS#*|}"; DB_USER="${_r%%|*}"; DB_PASS="${_r#*|}"
|
|
56
|
+
if [ -n "$DB_USER" ] && [ -n "$DB_PASS" ]; then
|
|
57
|
+
DUE=$(MYSQL_PWD="$DB_PASS" mysql -u"$DB_USER" -h"${DB_HOST:-localhost}" -N -e \
|
|
58
|
+
"SELECT COUNT(*) FROM aaden.TseExecuteQueue WHERE finished=0 AND nextExecuteTime<=NOW() AND createdAt>=NOW()-INTERVAL 5 MINUTE;" 2>/dev/null)
|
|
59
|
+
else
|
|
60
|
+
DUE=""
|
|
61
|
+
fi
|
|
62
|
+
if [ -z "$DUE" ]; then
|
|
63
|
+
# 查不到 = 无法确认 TSE 状态 → 对财政数据保守跳过(不部署也不冒险打断签名)。
|
|
64
|
+
# 非破坏性:此刻还没动任何文件。版本停滞会被 fleet-rollout-status 监控发现。
|
|
65
|
+
echo "TSE 队列查询失败,保守跳过本轮(未改动任何文件)"; echo "SKIP_TSE_QUERY"; exit 0
|
|
66
|
+
fi
|
|
67
|
+
if [ "$DUE" -gt 0 ]; then
|
|
68
|
+
echo "有进行中的交易/TSE 签名 (due=$DUE),跳过本轮,等下个周期"; echo "SKIP_TSE_BUSY"; exit 0
|
|
69
|
+
fi
|
|
70
|
+
echo "TSE 队列空闲 (due=0),可安全部署"
|
|
71
|
+
|
|
72
|
+
# ---- 备份 PHP 代码(排除 vendor) ----
|
|
73
|
+
sudoS "rm -rf $BACKUP && mkdir -p $BACKUP && tar czf $BACKUP/php-code.tgz --exclude='PHP/vendor' -C /var/www/html PHP package.json 2>/dev/null"
|
|
74
|
+
|
|
75
|
+
# ---- 部署(源已校验,可安全 rm+cp)----
|
|
76
|
+
sudoS "rm -rf /var/www/html/PHP/DB/migrations/*"
|
|
77
|
+
if [ "$ARTIFACT" = "code.zip" ]; then
|
|
78
|
+
sudoS "cp -rf $STAGE/PHP/. /var/www/html/PHP/"
|
|
79
|
+
sudoS "curl -s '$S3/raw/package.json' -o /var/www/html/package.json --max-time 30"
|
|
80
|
+
else
|
|
81
|
+
sudoS "cp -rf $STAGE/. /var/www/html/"
|
|
82
|
+
fi
|
|
83
|
+
sudoS "chmod -R 777 /var/www/html/Resource; chmod +x /var/www/html/crontab.sh /var/www/html/PHP/vendor/bin/phinx 2>/dev/null; chown -hR www-data:www-data /var/www/html"
|
|
84
|
+
|
|
85
|
+
# ---- migrate ----
|
|
86
|
+
sudoS "cd /var/www/html/PHP && vendor/bin/phinx migrate" 2>&1 | tail -3
|
|
87
|
+
|
|
88
|
+
# ---- 健康检查 ----
|
|
89
|
+
HEALTHY=1
|
|
90
|
+
for ep in "Orders.php?op=getCurrentActiveOrders&lang=DE" "MyVersion.php?op=version"; do
|
|
91
|
+
c=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost/PHP/$ep" --max-time 15)
|
|
92
|
+
echo " 健康检查 $ep -> $c"; [ "$c" = "200" ] || HEALTHY=0
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
# ---- 回滚 or 成功 ----
|
|
96
|
+
if [ "$HEALTHY" != "1" ]; then
|
|
97
|
+
echo "健康检查失败 → 回滚"
|
|
98
|
+
sudoS "tar xzf $BACKUP/php-code.tgz -C /var/www/html && chown -hR www-data:www-data /var/www/html"
|
|
99
|
+
sudoS "rm -rf $STAGE"; echo "ROLLED_BACK"; exit 1
|
|
100
|
+
fi
|
|
101
|
+
sudoS "rm -rf $STAGE"
|
|
102
|
+
echo "DEPLOY_OK $(python3 -c "import json;print(json.load(open('/var/www/html/package.json'))['version'])" 2>/dev/null)"
|
package/AadenDeploy/upgrade.sh
CHANGED
package/dist/index.js
CHANGED
|
@@ -18507,7 +18507,7 @@ module.exports = JSON.parse('{"name":"axios","version":"0.19.2","description":"P
|
|
|
18507
18507
|
/***/ ((module) => {
|
|
18508
18508
|
|
|
18509
18509
|
"use strict";
|
|
18510
|
-
module.exports = {"i8":"1.6.
|
|
18510
|
+
module.exports = {"i8":"1.6.98"};
|
|
18511
18511
|
|
|
18512
18512
|
/***/ })
|
|
18513
18513
|
|
|
@@ -18589,7 +18589,7 @@ const colors = __nccwpck_require__(3045)
|
|
|
18589
18589
|
const fs = __nccwpck_require__(7147)
|
|
18590
18590
|
const InnerKenTimeStampFormat = 'YYYYMMDDHHmmss'
|
|
18591
18591
|
const prompts = __nccwpck_require__(9646)
|
|
18592
|
-
const {deviceEcho, logAction} = __nccwpck_require__(7415)
|
|
18592
|
+
const {deviceEcho, logAction, getCurrentDeviceId} = __nccwpck_require__(7415)
|
|
18593
18593
|
const GlobalDeviceId = 'notset'
|
|
18594
18594
|
|
|
18595
18595
|
const {program} = __nccwpck_require__(1904)
|
|
@@ -18609,6 +18609,7 @@ console.log(
|
|
|
18609
18609
|
program
|
|
18610
18610
|
.option('-d, --debug', 'use Debug Out put')
|
|
18611
18611
|
program.command('update [channel]').action(update)
|
|
18612
|
+
program.command('light-update').action(lightUpdate) // 轻量+带闸门的后端部署,供 auto-upgrade/patch 调用
|
|
18612
18613
|
program.command('rollback').action(rollback)
|
|
18613
18614
|
program
|
|
18614
18615
|
.command('install <deviceId>')
|
|
@@ -19072,6 +19073,103 @@ async function update(channel = '') {
|
|
|
19072
19073
|
|
|
19073
19074
|
}
|
|
19074
19075
|
|
|
19076
|
+
/**
|
|
19077
|
+
* 轻量、带闸门的后端部署 —— 专为「每日 auto-upgrade / 一次性 patch」自动调用而设计。
|
|
19078
|
+
*
|
|
19079
|
+
* 与 `update`(走 update.sh)的区别:
|
|
19080
|
+
* - **不 snap refresh aaden-vue-app**(不会重启收银前端)
|
|
19081
|
+
* - **不 apt install**
|
|
19082
|
+
* - 三道闸门保证「营业时间被触发也安全」:
|
|
19083
|
+
* ① 版本门:S3 版本 == 本机版本 → 跳过(已最新,重启也不白部署)
|
|
19084
|
+
* ② 灰度门:只部署到 cloud `deviceLog.maxVersion` 允许的版本(-1=不设限)
|
|
19085
|
+
* → 可先给少数设备设 maxVersion 做金丝雀,其余自动跳过
|
|
19086
|
+
* ③ TSE 门:有进行中的交易/TSE 签名 → 跳过(在 lightUpdate.sh 里查 TseExecuteQueue,
|
|
19087
|
+
* 紧贴 cp 之前,避免"下载期间开了新交易"的竞态;开桌吃饭不阻塞,只有正在签的交易阻塞)
|
|
19088
|
+
* - 结果(部署/跳过/失败)经 logAction 写入 cloud 事件日志,可在 deviceLog/eventList 观测
|
|
19089
|
+
*/
|
|
19090
|
+
async function lightUpdate() {
|
|
19091
|
+
try {
|
|
19092
|
+
const localVersion = getBackendVersionOffline()
|
|
19093
|
+
const remoteVersion = (await hillo.get('https://aaden-backend.s3.eu-central-1.amazonaws.com/raw/package.json')).version
|
|
19094
|
+
|
|
19095
|
+
// ① 版本门:只在 S3 **严格更新** 时部署。
|
|
19096
|
+
// 不能只判 `===`:raw/package.json 可能比 latest.zip 晚几分钟才刷新,
|
|
19097
|
+
// 那个窗口里某台设备可能已经比 raw 标记的版本更新——只判相等会把它降级重铺。
|
|
19098
|
+
// 只往上升、绝不降级。
|
|
19099
|
+
if (!versionGt(remoteVersion, localVersion)) {
|
|
19100
|
+
console.log(('后端已是最新或更高 (本机 ' + localVersion + ' / S3 ' + remoteVersion + '),跳过 light-update').green)
|
|
19101
|
+
return
|
|
19102
|
+
}
|
|
19103
|
+
|
|
19104
|
+
// ② 灰度门(maxVersion 来自 cloud deviceLog;-1 或取不到 = 不设限,与既有 git 逻辑一致)
|
|
19105
|
+
const deviceId = await getCurrentDeviceId()
|
|
19106
|
+
const maxVersion = await getDeviceMaxVersion(deviceId)
|
|
19107
|
+
if (maxVersion && maxVersion !== '-1' && versionGt(remoteVersion, maxVersion)) {
|
|
19108
|
+
const msg = 'light-update 跳过:灰度闸门 remote=' + remoteVersion + ' > maxVersion=' + maxVersion
|
|
19109
|
+
console.log(msg.yellow)
|
|
19110
|
+
await logAction(msg, 0)
|
|
19111
|
+
return
|
|
19112
|
+
}
|
|
19113
|
+
|
|
19114
|
+
// ③ TSE 门已下移到 lightUpdate.sh:在 cp 之前查 TseExecuteQueue(finished=0 且到期且新鲜),
|
|
19115
|
+
// 有进行中的交易/TSE 签名就跳过。放在脚本里而非这里,是为了紧贴破坏性 cp、
|
|
19116
|
+
// 消掉"这里查过没单 → 下载几秒 → 期间开了新交易 → cp 打断签名"的竞态窗口。
|
|
19117
|
+
|
|
19118
|
+
// lightUpdate.sh 自己完成:TSE 门 + composer.lock 门(选 code.zip 或 latest.zip) + 下载 + 备份
|
|
19119
|
+
// + 部署 + migrate + 健康检查/失败回滚。退出码:0=成功,1=已回滚。
|
|
19120
|
+
const command = [
|
|
19121
|
+
...getPrivilegeCMD('AadenDeploy/lightUpdate.sh'),
|
|
19122
|
+
'cd AadenDeploy',
|
|
19123
|
+
'./lightUpdate.sh'
|
|
19124
|
+
]
|
|
19125
|
+
const {code} = await cli.executeInShell(command, program.debug)
|
|
19126
|
+
|
|
19127
|
+
const newVersion = readBackendVersionFresh()
|
|
19128
|
+
if (code !== 0) {
|
|
19129
|
+
await logAction('light-update 失败/已回滚 (仍 ' + newVersion + ')', code || 1)
|
|
19130
|
+
} else if (newVersion !== localVersion) {
|
|
19131
|
+
await logAction('light-update 完成 ' + localVersion + ' -> ' + newVersion, 0)
|
|
19132
|
+
} else {
|
|
19133
|
+
await logAction('light-update 无实际变化 (' + newVersion + ')', 0)
|
|
19134
|
+
}
|
|
19135
|
+
} catch (e) {
|
|
19136
|
+
console.error(e)
|
|
19137
|
+
try { await logAction('light-update 异常: ' + (e.message || e), 1) } catch (_) {}
|
|
19138
|
+
}
|
|
19139
|
+
}
|
|
19140
|
+
|
|
19141
|
+
/** 比较点分数字版本,a > b 返回 true。'1.9.61' > '1.7.908' === true。 */
|
|
19142
|
+
function versionGt(a, b) {
|
|
19143
|
+
const pa = String(a).split('.').map(n => parseInt(n) || 0)
|
|
19144
|
+
const pb = String(b).split('.').map(n => parseInt(n) || 0)
|
|
19145
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
19146
|
+
const x = pa[i] || 0, y = pb[i] || 0
|
|
19147
|
+
if (x > y) return true
|
|
19148
|
+
if (x < y) return false
|
|
19149
|
+
}
|
|
19150
|
+
return false
|
|
19151
|
+
}
|
|
19152
|
+
|
|
19153
|
+
/** 从 cloud deviceLog 取本机版本天花板;取不到按不设限(-1),与 UpdateSelf 的 getDeviceLog 一致。 */
|
|
19154
|
+
async function getDeviceMaxVersion(deviceId) {
|
|
19155
|
+
try {
|
|
19156
|
+
const res = await hillo.get('https://cloud-v2.aaden.io/deviceLog/get/' + deviceId)
|
|
19157
|
+
return (res && res.maxVersion) ? res.maxVersion : '-1'
|
|
19158
|
+
} catch (e) {
|
|
19159
|
+
console.log('取 maxVersion 失败,按不设限处理:', e.message)
|
|
19160
|
+
return '-1'
|
|
19161
|
+
}
|
|
19162
|
+
}
|
|
19163
|
+
|
|
19164
|
+
/** 直接读文件、绕过 require 缓存 —— 部署后读到的必须是新版本号,不能是进程启动时缓存的旧值。 */
|
|
19165
|
+
function readBackendVersionFresh() {
|
|
19166
|
+
try {
|
|
19167
|
+
return JSON.parse(fs.readFileSync('/var/www/html/package.json', 'utf8')).version
|
|
19168
|
+
} catch (e) {
|
|
19169
|
+
return '0.0.0'
|
|
19170
|
+
}
|
|
19171
|
+
}
|
|
19172
|
+
|
|
19075
19173
|
async function install(deviceId) {
|
|
19076
19174
|
const channel = 'stable'
|
|
19077
19175
|
const installApp = true
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 一次性修复:摘掉老 git 通道的 doUpdate cron。
|
|
3
|
+
#
|
|
4
|
+
# 病症:设备 crontab 里有
|
|
5
|
+
# 59 4 * * * curl -X POST 'http://localhost/PHP/UpdateSelf.php?op=doUpdate'
|
|
6
|
+
# @reboot ... curl -X POST '.../UpdateSelf.php?op=doUpdate'
|
|
7
|
+
# 它每天 04:59(和 @reboot)从**设备本地陈旧 gitCache** 部署后端;而 git fetch 因 deploy key
|
|
8
|
+
# 未分发到全网而失败(FETCH_HEAD 0 字节),缓存卡在旧版本;git 通道又没有"绝不降级"保护 →
|
|
9
|
+
# 于是把 light-update 刚更好的新版本降级回退(实测 1.9.63 → 1.9.62/1.9.61,每天来回打架)。
|
|
10
|
+
#
|
|
11
|
+
# 结论:light-update(S3/code.zip 通道)已是后端唯一活着的更新通道,且自带"只升不降"版本门。
|
|
12
|
+
# 摘掉 doUpdate cron,终结两条通道打架。crontab 不走 git/S3 部署 → 改了就稳。
|
|
13
|
+
#
|
|
14
|
+
# 幂等:没有该行就不动;grep -vF 精确匹配整串,不误伤 crontab.sh / light-update 那几行。
|
|
15
|
+
set -u
|
|
16
|
+
|
|
17
|
+
MARK='UpdateSelf.php?op=doUpdate'
|
|
18
|
+
|
|
19
|
+
strip_for() { # $1 = 目标用户(空=当前用户)
|
|
20
|
+
local u="$1" cur new
|
|
21
|
+
local args=()
|
|
22
|
+
[ -n "$u" ] && args=(-u "$u")
|
|
23
|
+
cur=$(crontab "${args[@]}" -l 2>/dev/null) || return 0
|
|
24
|
+
printf '%s\n' "$cur" | grep -qF "$MARK" || return 0 # 没有该 cron → 不动
|
|
25
|
+
new=$(printf '%s\n' "$cur" | grep -vF "$MARK")
|
|
26
|
+
printf '%s\n' "$new" | crontab "${args[@]}" - \
|
|
27
|
+
&& echo " 已从 ${u:-当前用户} 的 crontab 摘除 doUpdate cron"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# doUpdate cron 在 aaden 用户的 crontab。patch 正常由 root 的 auto-upgrade 触发 → 用 -u aaden;
|
|
31
|
+
# 兜底再扫一遍当前用户(万一某些设备以 aaden 直接跑 patch),谁有就摘谁。
|
|
32
|
+
if [ "$(id -u)" = "0" ]; then
|
|
33
|
+
strip_for aaden
|
|
34
|
+
strip_for ""
|
|
35
|
+
else
|
|
36
|
+
strip_for ""
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
echo " removeGitDoUpdateCron 完成(light-update 成为后端唯一更新通道)"
|
package/index.js
CHANGED
|
@@ -8,7 +8,7 @@ const colors = require('colors')
|
|
|
8
8
|
const fs = require('fs')
|
|
9
9
|
const InnerKenTimeStampFormat = 'YYYYMMDDHHmmss'
|
|
10
10
|
const prompts = require('prompts')
|
|
11
|
-
const {deviceEcho, logAction} = require('./modules/firebase')
|
|
11
|
+
const {deviceEcho, logAction, getCurrentDeviceId} = require('./modules/firebase')
|
|
12
12
|
const GlobalDeviceId = 'notset'
|
|
13
13
|
|
|
14
14
|
const {program} = require('commander')
|
|
@@ -28,6 +28,7 @@ console.log(
|
|
|
28
28
|
program
|
|
29
29
|
.option('-d, --debug', 'use Debug Out put')
|
|
30
30
|
program.command('update [channel]').action(update)
|
|
31
|
+
program.command('light-update').action(lightUpdate) // 轻量+带闸门的后端部署,供 auto-upgrade/patch 调用
|
|
31
32
|
program.command('rollback').action(rollback)
|
|
32
33
|
program
|
|
33
34
|
.command('install <deviceId>')
|
|
@@ -491,6 +492,103 @@ async function update(channel = '') {
|
|
|
491
492
|
|
|
492
493
|
}
|
|
493
494
|
|
|
495
|
+
/**
|
|
496
|
+
* 轻量、带闸门的后端部署 —— 专为「每日 auto-upgrade / 一次性 patch」自动调用而设计。
|
|
497
|
+
*
|
|
498
|
+
* 与 `update`(走 update.sh)的区别:
|
|
499
|
+
* - **不 snap refresh aaden-vue-app**(不会重启收银前端)
|
|
500
|
+
* - **不 apt install**
|
|
501
|
+
* - 三道闸门保证「营业时间被触发也安全」:
|
|
502
|
+
* ① 版本门:S3 版本 == 本机版本 → 跳过(已最新,重启也不白部署)
|
|
503
|
+
* ② 灰度门:只部署到 cloud `deviceLog.maxVersion` 允许的版本(-1=不设限)
|
|
504
|
+
* → 可先给少数设备设 maxVersion 做金丝雀,其余自动跳过
|
|
505
|
+
* ③ TSE 门:有进行中的交易/TSE 签名 → 跳过(在 lightUpdate.sh 里查 TseExecuteQueue,
|
|
506
|
+
* 紧贴 cp 之前,避免"下载期间开了新交易"的竞态;开桌吃饭不阻塞,只有正在签的交易阻塞)
|
|
507
|
+
* - 结果(部署/跳过/失败)经 logAction 写入 cloud 事件日志,可在 deviceLog/eventList 观测
|
|
508
|
+
*/
|
|
509
|
+
async function lightUpdate() {
|
|
510
|
+
try {
|
|
511
|
+
const localVersion = getBackendVersionOffline()
|
|
512
|
+
const remoteVersion = (await hillo.get('https://aaden-backend.s3.eu-central-1.amazonaws.com/raw/package.json')).version
|
|
513
|
+
|
|
514
|
+
// ① 版本门:只在 S3 **严格更新** 时部署。
|
|
515
|
+
// 不能只判 `===`:raw/package.json 可能比 latest.zip 晚几分钟才刷新,
|
|
516
|
+
// 那个窗口里某台设备可能已经比 raw 标记的版本更新——只判相等会把它降级重铺。
|
|
517
|
+
// 只往上升、绝不降级。
|
|
518
|
+
if (!versionGt(remoteVersion, localVersion)) {
|
|
519
|
+
console.log(('后端已是最新或更高 (本机 ' + localVersion + ' / S3 ' + remoteVersion + '),跳过 light-update').green)
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ② 灰度门(maxVersion 来自 cloud deviceLog;-1 或取不到 = 不设限,与既有 git 逻辑一致)
|
|
524
|
+
const deviceId = await getCurrentDeviceId()
|
|
525
|
+
const maxVersion = await getDeviceMaxVersion(deviceId)
|
|
526
|
+
if (maxVersion && maxVersion !== '-1' && versionGt(remoteVersion, maxVersion)) {
|
|
527
|
+
const msg = 'light-update 跳过:灰度闸门 remote=' + remoteVersion + ' > maxVersion=' + maxVersion
|
|
528
|
+
console.log(msg.yellow)
|
|
529
|
+
await logAction(msg, 0)
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ③ TSE 门已下移到 lightUpdate.sh:在 cp 之前查 TseExecuteQueue(finished=0 且到期且新鲜),
|
|
534
|
+
// 有进行中的交易/TSE 签名就跳过。放在脚本里而非这里,是为了紧贴破坏性 cp、
|
|
535
|
+
// 消掉"这里查过没单 → 下载几秒 → 期间开了新交易 → cp 打断签名"的竞态窗口。
|
|
536
|
+
|
|
537
|
+
// lightUpdate.sh 自己完成:TSE 门 + composer.lock 门(选 code.zip 或 latest.zip) + 下载 + 备份
|
|
538
|
+
// + 部署 + migrate + 健康检查/失败回滚。退出码:0=成功,1=已回滚。
|
|
539
|
+
const command = [
|
|
540
|
+
...getPrivilegeCMD('AadenDeploy/lightUpdate.sh'),
|
|
541
|
+
'cd AadenDeploy',
|
|
542
|
+
'./lightUpdate.sh'
|
|
543
|
+
]
|
|
544
|
+
const {code} = await cli.executeInShell(command, program.debug)
|
|
545
|
+
|
|
546
|
+
const newVersion = readBackendVersionFresh()
|
|
547
|
+
if (code !== 0) {
|
|
548
|
+
await logAction('light-update 失败/已回滚 (仍 ' + newVersion + ')', code || 1)
|
|
549
|
+
} else if (newVersion !== localVersion) {
|
|
550
|
+
await logAction('light-update 完成 ' + localVersion + ' -> ' + newVersion, 0)
|
|
551
|
+
} else {
|
|
552
|
+
await logAction('light-update 无实际变化 (' + newVersion + ')', 0)
|
|
553
|
+
}
|
|
554
|
+
} catch (e) {
|
|
555
|
+
console.error(e)
|
|
556
|
+
try { await logAction('light-update 异常: ' + (e.message || e), 1) } catch (_) {}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** 比较点分数字版本,a > b 返回 true。'1.9.61' > '1.7.908' === true。 */
|
|
561
|
+
function versionGt(a, b) {
|
|
562
|
+
const pa = String(a).split('.').map(n => parseInt(n) || 0)
|
|
563
|
+
const pb = String(b).split('.').map(n => parseInt(n) || 0)
|
|
564
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
565
|
+
const x = pa[i] || 0, y = pb[i] || 0
|
|
566
|
+
if (x > y) return true
|
|
567
|
+
if (x < y) return false
|
|
568
|
+
}
|
|
569
|
+
return false
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** 从 cloud deviceLog 取本机版本天花板;取不到按不设限(-1),与 UpdateSelf 的 getDeviceLog 一致。 */
|
|
573
|
+
async function getDeviceMaxVersion(deviceId) {
|
|
574
|
+
try {
|
|
575
|
+
const res = await hillo.get('https://cloud-v2.aaden.io/deviceLog/get/' + deviceId)
|
|
576
|
+
return (res && res.maxVersion) ? res.maxVersion : '-1'
|
|
577
|
+
} catch (e) {
|
|
578
|
+
console.log('取 maxVersion 失败,按不设限处理:', e.message)
|
|
579
|
+
return '-1'
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** 直接读文件、绕过 require 缓存 —— 部署后读到的必须是新版本号,不能是进程启动时缓存的旧值。 */
|
|
584
|
+
function readBackendVersionFresh() {
|
|
585
|
+
try {
|
|
586
|
+
return JSON.parse(fs.readFileSync('/var/www/html/package.json', 'utf8')).version
|
|
587
|
+
} catch (e) {
|
|
588
|
+
return '0.0.0'
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
494
592
|
async function install(deviceId) {
|
|
495
593
|
const channel = 'stable'
|
|
496
594
|
const installApp = true
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aaden-cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.98",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "node test.js",
|
|
8
|
-
"update": "git add . && release-it
|
|
8
|
+
"update": "git add . && release-it"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
11
|
"aaden-cli": "dist/index.js"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 一次性修复:摘掉老 git 通道的 doUpdate cron。
|
|
3
|
+
#
|
|
4
|
+
# 病症:设备 crontab 里有
|
|
5
|
+
# 59 4 * * * curl -X POST 'http://localhost/PHP/UpdateSelf.php?op=doUpdate'
|
|
6
|
+
# @reboot ... curl -X POST '.../UpdateSelf.php?op=doUpdate'
|
|
7
|
+
# 它每天 04:59(和 @reboot)从**设备本地陈旧 gitCache** 部署后端;而 git fetch 因 deploy key
|
|
8
|
+
# 未分发到全网而失败(FETCH_HEAD 0 字节),缓存卡在旧版本;git 通道又没有"绝不降级"保护 →
|
|
9
|
+
# 于是把 light-update 刚更好的新版本降级回退(实测 1.9.63 → 1.9.62/1.9.61,每天来回打架)。
|
|
10
|
+
#
|
|
11
|
+
# 结论:light-update(S3/code.zip 通道)已是后端唯一活着的更新通道,且自带"只升不降"版本门。
|
|
12
|
+
# 摘掉 doUpdate cron,终结两条通道打架。crontab 不走 git/S3 部署 → 改了就稳。
|
|
13
|
+
#
|
|
14
|
+
# 幂等:没有该行就不动;grep -vF 精确匹配整串,不误伤 crontab.sh / light-update 那几行。
|
|
15
|
+
set -u
|
|
16
|
+
|
|
17
|
+
MARK='UpdateSelf.php?op=doUpdate'
|
|
18
|
+
|
|
19
|
+
strip_for() { # $1 = 目标用户(空=当前用户)
|
|
20
|
+
local u="$1" cur new
|
|
21
|
+
local args=()
|
|
22
|
+
[ -n "$u" ] && args=(-u "$u")
|
|
23
|
+
cur=$(crontab "${args[@]}" -l 2>/dev/null) || return 0
|
|
24
|
+
printf '%s\n' "$cur" | grep -qF "$MARK" || return 0 # 没有该 cron → 不动
|
|
25
|
+
new=$(printf '%s\n' "$cur" | grep -vF "$MARK")
|
|
26
|
+
printf '%s\n' "$new" | crontab "${args[@]}" - \
|
|
27
|
+
&& echo " 已从 ${u:-当前用户} 的 crontab 摘除 doUpdate cron"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# doUpdate cron 在 aaden 用户的 crontab。patch 正常由 root 的 auto-upgrade 触发 → 用 -u aaden;
|
|
31
|
+
# 兜底再扫一遍当前用户(万一某些设备以 aaden 直接跑 patch),谁有就摘谁。
|
|
32
|
+
if [ "$(id -u)" = "0" ]; then
|
|
33
|
+
strip_for aaden
|
|
34
|
+
strip_for ""
|
|
35
|
+
else
|
|
36
|
+
strip_for ""
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
echo " removeGitDoUpdateCron 完成(light-update 成为后端唯一更新通道)"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""车队后端版本滚动进度表。数据源:cloud-v2 的 deviceLog/list(每台每 5 分钟上报)。
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
python3 scripts/fleet-rollout-status.py 1.9.62 # 看向 1.9.62 铺开的进度
|
|
6
|
+
python3 scripts/fleet-rollout-status.py 1.9.62 --behind # additionally 列出落后设备
|
|
7
|
+
python3 scripts/fleet-rollout-status.py # 不给目标版本则只看分布
|
|
8
|
+
"""
|
|
9
|
+
import sys, json, time, urllib.request
|
|
10
|
+
from collections import Counter
|
|
11
|
+
|
|
12
|
+
LIST_URL = "https://cloud-v2.aaden.io/deviceLog/list"
|
|
13
|
+
STALE_MIN = 30 # 超过这么多分钟没上报,视为离线/失联
|
|
14
|
+
|
|
15
|
+
def fetch():
|
|
16
|
+
with urllib.request.urlopen(LIST_URL, timeout=60) as r:
|
|
17
|
+
return json.load(r)
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
target = next((a for a in sys.argv[1:] if not a.startswith('-')), None)
|
|
21
|
+
show_behind = '--behind' in sys.argv
|
|
22
|
+
d = fetch()
|
|
23
|
+
now = int(time.time() * 1000)
|
|
24
|
+
total = len(d)
|
|
25
|
+
dist = Counter(x.get('backendVersion', '?') for x in d)
|
|
26
|
+
stale = [x for x in d if now - int(x.get('timestamp', 0) or 0) > STALE_MIN * 60 * 1000]
|
|
27
|
+
|
|
28
|
+
print(f"车队共 {total} 台 (数据源 deviceLog/list,每台每 5 分钟上报)")
|
|
29
|
+
print(f"失联(>{STALE_MIN}分钟无上报): {len(stale)} 台")
|
|
30
|
+
print()
|
|
31
|
+
if target:
|
|
32
|
+
on = [x for x in d if x.get('backendVersion') == target]
|
|
33
|
+
behind = [x for x in d if x.get('backendVersion') != target]
|
|
34
|
+
pct = len(on) * 100.0 / total if total else 0
|
|
35
|
+
bar = '#' * int(pct // 2.5) + '.' * (40 - int(pct // 2.5))
|
|
36
|
+
print(f"目标后端版本: {target}")
|
|
37
|
+
print(f" 已到位 [{bar}] {len(on)}/{total} {pct:.1f}%")
|
|
38
|
+
print(f" 未到位 {len(behind)} 台")
|
|
39
|
+
print()
|
|
40
|
+
print("未到位版本分布:")
|
|
41
|
+
for v, n in Counter(x.get('backendVersion', '?') for x in behind).most_common():
|
|
42
|
+
print(f" {v:>10} {n} 台")
|
|
43
|
+
if show_behind:
|
|
44
|
+
print()
|
|
45
|
+
print("未到位设备(deviceId / 版本 / 名称首行):")
|
|
46
|
+
for x in sorted(behind, key=lambda x: x.get('deviceId', '')):
|
|
47
|
+
name = (x.get('deviceName') or '').split('\n')[0][:32]
|
|
48
|
+
print(f" {str(x.get('deviceId','?')):>6} {x.get('backendVersion','?'):>10} {name}")
|
|
49
|
+
else:
|
|
50
|
+
print("后端版本分布:")
|
|
51
|
+
for v, n in dist.most_common():
|
|
52
|
+
print(f" {v:>10} {n} 台")
|
|
53
|
+
|
|
54
|
+
if __name__ == '__main__':
|
|
55
|
+
main()
|