aaden-cli 1.6.91 → 1.6.97
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/Utils/cottiNetplan.sh +84 -0
- package/AadenDeploy/files/getFile.sh +0 -0
- package/AadenDeploy/lightUpdate.sh +98 -7
- package/AadenDeploy/ngrok/ngrok +0 -0
- package/AadenDeploy/update.sh +0 -0
- package/AadenDeploy/upgrade.sh +4 -1
- package/dist/index.js +108 -3
- package/dist/patch/202608181934restoreBackendViaS3.sh +4 -0
- package/index.js +107 -2
- package/package.json +3 -3
- package/patch/202608181934restoreBackendViaS3.sh +4 -0
- package/scripts/fleet-rollout-status.py +55 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# 此脚本为指定的网卡设置静态 IP
|
|
4
|
+
# IP: 192.168.121.20/24
|
|
5
|
+
# 网关: 192.168.121.1
|
|
6
|
+
# DNS: 192.168.121.1
|
|
7
|
+
# 在 /etc/netplan/ 下新增文件 30-cotti.yaml 并应用配置
|
|
8
|
+
# 需要 root 权限运行(推荐使用 sudo)
|
|
9
|
+
|
|
10
|
+
set -e # 遇到错误立即退出
|
|
11
|
+
|
|
12
|
+
# 查找所有非 lo 的以太网接口
|
|
13
|
+
mapfile -t INTERFACES < <(ip -o link show | awk -F': ' '/state UP|state DOWN/ && $2 !~ /lo/ {print $2}')
|
|
14
|
+
|
|
15
|
+
if [ ${#INTERFACES[@]} -eq 0 ]; then
|
|
16
|
+
echo "错误:未找到任何可用网卡!"
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# 优先尝试自动选择以 enp 开头的网卡
|
|
21
|
+
INTERFACE=""
|
|
22
|
+
for iface in "${INTERFACES[@]}"; do
|
|
23
|
+
if [[ $iface == enp* ]]; then
|
|
24
|
+
INTERFACE="$iface"
|
|
25
|
+
break
|
|
26
|
+
fi
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
# 如果没有找到 enp 开头的网卡,或者用户想手动选择,则进入手动选择流程
|
|
30
|
+
if [ -z "$INTERFACE" ]; then
|
|
31
|
+
echo "未找到以 enp 开头的网卡,下面列出所有可用网卡:"
|
|
32
|
+
for i in "${!INTERFACES[@]}"; do
|
|
33
|
+
idx=$((i+1))
|
|
34
|
+
echo "$idx) ${INTERFACES[i]}"
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
if [ ${#INTERFACES[@]} -eq 1 ]; then
|
|
38
|
+
echo "只检测到一个网卡,将自动选择:${INTERFACES[0]}"
|
|
39
|
+
INTERFACE="${INTERFACES[0]}"
|
|
40
|
+
else
|
|
41
|
+
while true; do
|
|
42
|
+
echo -n "请输入要配置的网卡编号(1-${#INTERFACES[@]}):"
|
|
43
|
+
read choice
|
|
44
|
+
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#INTERFACES[@]} ]; then
|
|
45
|
+
INTERFACE="${INTERFACES[$((choice-1))]}"
|
|
46
|
+
break
|
|
47
|
+
else
|
|
48
|
+
echo "输入无效,请输入有效的编号!"
|
|
49
|
+
fi
|
|
50
|
+
done
|
|
51
|
+
fi
|
|
52
|
+
else
|
|
53
|
+
echo "自动选择网卡:$INTERFACE"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
echo "将为网卡 $INTERFACE 配置静态 IP 192.168.121.20/24"
|
|
57
|
+
|
|
58
|
+
CONFIG_FILE="/etc/netplan/30-cotti.yaml"
|
|
59
|
+
|
|
60
|
+
# 使用 cat 和 heredoc 写入 YAML 文件(注意缩进严格要求)
|
|
61
|
+
sudo bash -c "cat > $CONFIG_FILE" << EOF
|
|
62
|
+
network:
|
|
63
|
+
version: 2
|
|
64
|
+
renderer: NetworkManager
|
|
65
|
+
ethernets:
|
|
66
|
+
$INTERFACE:
|
|
67
|
+
dhcp4: false
|
|
68
|
+
addresses:
|
|
69
|
+
- 192.168.121.20/24
|
|
70
|
+
gateway4: 192.168.121.1
|
|
71
|
+
nameservers:
|
|
72
|
+
addresses:
|
|
73
|
+
- 192.168.121.1
|
|
74
|
+
EOF
|
|
75
|
+
|
|
76
|
+
echo "已生成配置文件:$CONFIG_FILE"
|
|
77
|
+
echo "内容如下:"
|
|
78
|
+
sudo cat $CONFIG_FILE
|
|
79
|
+
|
|
80
|
+
# 应用配置
|
|
81
|
+
echo "正在应用配置..."
|
|
82
|
+
sudo netplan apply
|
|
83
|
+
|
|
84
|
+
echo "配置完成!请使用 ip addr show $INTERFACE 检查 IP 是否已生效。"
|
|
File without changes
|
|
@@ -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/ngrok/ngrok
CHANGED
|
File without changes
|
package/AadenDeploy/update.sh
CHANGED
|
File without changes
|
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.97"};
|
|
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>')
|
|
@@ -18630,6 +18631,7 @@ program.command('update-tse').action(updateTse)
|
|
|
18630
18631
|
program.command('update-win-tse').action(updateWindowsTse)
|
|
18631
18632
|
program.command('check-version').action(getBackendVersionOffline)
|
|
18632
18633
|
program.command('full-update').action(fullUpdate)
|
|
18634
|
+
program.command('cotti-netplan').action(cottiNetplan)
|
|
18633
18635
|
|
|
18634
18636
|
program.command('db').action(enterDatabase)
|
|
18635
18637
|
program.command('printer-test').action(testPrinter)
|
|
@@ -18843,7 +18845,8 @@ async function fullUpdate() {
|
|
|
18843
18845
|
'sudo service apache2 restart',
|
|
18844
18846
|
'aaden-cli fix-cronjob',
|
|
18845
18847
|
'aaden-cli auto-write-gdpdu',
|
|
18846
|
-
'sudo systemctl daemon-reload'
|
|
18848
|
+
'sudo systemctl daemon-reload',
|
|
18849
|
+
'aaden-cli ensure-print-log-rotate',
|
|
18847
18850
|
]
|
|
18848
18851
|
const {code} = await cli.executeInShell(command, program.debug)
|
|
18849
18852
|
await logAction("进行了full-update", code)
|
|
@@ -19070,6 +19073,103 @@ async function update(channel = '') {
|
|
|
19070
19073
|
|
|
19071
19074
|
}
|
|
19072
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
|
+
|
|
19073
19173
|
async function install(deviceId) {
|
|
19074
19174
|
const channel = 'stable'
|
|
19075
19175
|
const installApp = true
|
|
@@ -19335,6 +19435,11 @@ function ensurePrintLogRotate() {
|
|
|
19335
19435
|
}
|
|
19336
19436
|
}
|
|
19337
19437
|
|
|
19438
|
+
async function cottiNetplan() {
|
|
19439
|
+
await run('AadenDeploy/Utils/cottiNetplan.sh')
|
|
19440
|
+
logAction("手动设置cotti netplan")
|
|
19441
|
+
}
|
|
19442
|
+
|
|
19338
19443
|
})();
|
|
19339
19444
|
|
|
19340
19445
|
module.exports = __webpack_exports__;
|
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>')
|
|
@@ -49,6 +50,7 @@ program.command('update-tse').action(updateTse)
|
|
|
49
50
|
program.command('update-win-tse').action(updateWindowsTse)
|
|
50
51
|
program.command('check-version').action(getBackendVersionOffline)
|
|
51
52
|
program.command('full-update').action(fullUpdate)
|
|
53
|
+
program.command('cotti-netplan').action(cottiNetplan)
|
|
52
54
|
|
|
53
55
|
program.command('db').action(enterDatabase)
|
|
54
56
|
program.command('printer-test').action(testPrinter)
|
|
@@ -262,7 +264,8 @@ async function fullUpdate() {
|
|
|
262
264
|
'sudo service apache2 restart',
|
|
263
265
|
'aaden-cli fix-cronjob',
|
|
264
266
|
'aaden-cli auto-write-gdpdu',
|
|
265
|
-
'sudo systemctl daemon-reload'
|
|
267
|
+
'sudo systemctl daemon-reload',
|
|
268
|
+
'aaden-cli ensure-print-log-rotate',
|
|
266
269
|
]
|
|
267
270
|
const {code} = await cli.executeInShell(command, program.debug)
|
|
268
271
|
await logAction("进行了full-update", code)
|
|
@@ -489,6 +492,103 @@ async function update(channel = '') {
|
|
|
489
492
|
|
|
490
493
|
}
|
|
491
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
|
+
|
|
492
592
|
async function install(deviceId) {
|
|
493
593
|
const channel = 'stable'
|
|
494
594
|
const installApp = true
|
|
@@ -753,3 +853,8 @@ function ensurePrintLogRotate() {
|
|
|
753
853
|
cli.executeInShell(command, program.debug)
|
|
754
854
|
}
|
|
755
855
|
}
|
|
856
|
+
|
|
857
|
+
async function cottiNetplan() {
|
|
858
|
+
await run('AadenDeploy/Utils/cottiNetplan.sh')
|
|
859
|
+
logAction("手动设置cotti netplan")
|
|
860
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aaden-cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.97",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"update": "git add . && release-it --ci"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
|
-
"aaden-cli": "
|
|
11
|
+
"aaden-cli": "dist/index.js"
|
|
12
12
|
},
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
|
-
"url": "https://github.com/innerken-gmbh/aaden-cli.git"
|
|
15
|
+
"url": "git+https://github.com/innerken-gmbh/aaden-cli.git"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [],
|
|
18
18
|
"author": "JuHaodong@innerken",
|
|
@@ -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()
|