aaden-cli 1.6.110 → 1.6.112
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 +25 -5
- package/AadenDeploy/restartNgrok.sh +37 -0
- package/AadenDeploy/upgrade.sh +58 -6
- package/dist/index.js +375 -17
- package/dist/patch/202610072130ensureDoLogCron.sh +42 -0
- package/index.js +59 -13
- package/modules/device-commands.js +301 -0
- package/modules/firebase.js +7 -3
- package/package.json +1 -1
- package/patch/202610072130ensureDoLogCron.sh +42 -0
|
@@ -20,6 +20,14 @@ rm -f "$STATUS_FILE" 2>/dev/null || true
|
|
|
20
20
|
# 每条退出路径都经 finish:状态既打到 stdout(人看日志),也落盘(index.js 读了发 deviceLog)。
|
|
21
21
|
finish(){ echo "$1"; echo "$1" > "$STATUS_FILE" 2>/dev/null || true; exit "$2"; }
|
|
22
22
|
|
|
23
|
+
# 同一时间只跑一个:04:59 cron、开机、do-log 补跑、云端命令、远程触发可能撞在一起,两个同时 cp/migrate 会互相踩。
|
|
24
|
+
# 锁文件谁先建都要让另一个用户也能打开(root 和 aaden 都会跑),所以 666;flock 只要读权限。
|
|
25
|
+
LOCK=/var/tmp/aaden-lu.lock
|
|
26
|
+
[ -e "$LOCK" ] || { touch "$LOCK" && chmod 666 "$LOCK"; } 2>/dev/null
|
|
27
|
+
if exec 9<"$LOCK" 2>/dev/null; then
|
|
28
|
+
flock -n 9 || finish "SKIP_BUSY (另一个 light-update 正在跑)" 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
23
31
|
# 点分数字版本比较:a > b 返回 0。与 index.js 的 versionGt 同一套语义('1.9.61' > '1.7.908')。
|
|
24
32
|
verGt(){ python3 -c "
|
|
25
33
|
import sys
|
|
@@ -93,16 +101,28 @@ echo "产物版本门通过:${LOCAL_VER:-未知} -> $ART_VER"
|
|
|
93
101
|
CONF=/var/www/html/Resource/config/config.ini
|
|
94
102
|
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)
|
|
95
103
|
DB_HOST="${DBCREDS%%|*}"; _r="${DBCREDS#*|}"; DB_USER="${_r%%|*}"; DB_PASS="${_r#*|}"
|
|
104
|
+
sqlq(){ MYSQL_PWD="$DB_PASS" mysql -u"$DB_USER" -h"${DB_HOST:-localhost}" -N -e "$1"; }
|
|
105
|
+
DUE=""; TSE_ERR=""; TSE_NOTE=""
|
|
96
106
|
if [ -n "$DB_USER" ] && [ -n "$DB_PASS" ]; then
|
|
97
|
-
DUE=$(
|
|
98
|
-
|
|
107
|
+
DUE=$(sqlq "SELECT COUNT(*) FROM aaden.TseExecuteQueue WHERE finished=0 AND nextExecuteTime<=NOW() AND createdAt>=NOW()-INTERVAL 5 MINUTE;" 2>"$STAGE/tse.err")
|
|
108
|
+
TSE_ERR=$(head -c 120 "$STAGE/tse.err" 2>/dev/null | tr '\n' ' ')
|
|
109
|
+
if [ -z "$DUE" ]; then
|
|
110
|
+
# 查询失败要分清两种:TseExecuteQueue 是 1.9.1(2025-03 的 migration)才有的,更老的后端没有这张表,
|
|
111
|
+
# 签名在结账请求里同步做、没有队列可查 —— 这种按空闲处理,否则永远「保守跳过」、永远升不上来
|
|
112
|
+
# (2026-10 的 1407 停在 1.7.913,每天 SKIP_TSE_QUERY)。连不上库之类的其他失败照旧跳过。
|
|
113
|
+
HAS_QUEUE=$(sqlq "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='aaden' AND table_name='TseExecuteQueue';" 2>/dev/null)
|
|
114
|
+
if [ "$HAS_QUEUE" = "0" ]; then
|
|
115
|
+
echo "本机后端没有 TseExecuteQueue 表(1.9.1 之前的版本),没有 TSE 队列可查,按空闲处理"
|
|
116
|
+
DUE=0; TSE_NOTE=" TSE_NO_QUEUE"
|
|
117
|
+
fi
|
|
118
|
+
fi
|
|
99
119
|
else
|
|
100
|
-
|
|
120
|
+
TSE_ERR="config.ini 里读不到 [DB] 用户名/密码"
|
|
101
121
|
fi
|
|
102
122
|
if [ -z "$DUE" ]; then
|
|
103
123
|
# 查不到 = 无法确认 TSE 状态 → 对财政数据保守跳过(不部署也不冒险打断签名)。
|
|
104
124
|
# 非破坏性:此刻还没动任何文件。版本停滞会被 fleet-rollout-status 监控发现。
|
|
105
|
-
echo "TSE
|
|
125
|
+
echo "TSE 队列查询失败,保守跳过本轮(未改动任何文件): $TSE_ERR"; finish "SKIP_TSE_QUERY${TSE_ERR:+ ($TSE_ERR)}" 0
|
|
106
126
|
fi
|
|
107
127
|
if [ "$DUE" -gt 0 ]; then
|
|
108
128
|
echo "有进行中的交易/TSE 签名 (due=$DUE),跳过本轮,等下个周期"; finish "SKIP_TSE_BUSY (due=$DUE)" 0
|
|
@@ -141,4 +161,4 @@ if [ "$HEALTHY" != "1" ]; then
|
|
|
141
161
|
sudoS "rm -rf $STAGE"; finish "ROLLED_BACK (回到 ${LOCAL_VER:-未知})" 1
|
|
142
162
|
fi
|
|
143
163
|
sudoS "rm -rf $STAGE"
|
|
144
|
-
finish "DEPLOY_OK $(pkgVer /var/www/html/package.json)" 0
|
|
164
|
+
finish "DEPLOY_OK $(pkgVer /var/www/html/package.json)${TSE_NOTE}" 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 重启 ngrok 隧道。云端发现「设备还在上报、ngrok 却断了 30 分钟以上」时自动下发(RESTART_NGROK 命令),
|
|
3
|
+
# ERP「设备命令」里也能手动下发。输出整段回传云端,ERP 里能看到 ngrok 日志末尾。
|
|
4
|
+
# 退出码:ngrok 进程起来了 = 0,否则 1。
|
|
5
|
+
sudoS() { echo "Innerken322." | sudo -S -p '' "$@"; }
|
|
6
|
+
SVC=aaden-ngrok-v3
|
|
7
|
+
NGROK=/etc/aaden/ngrok
|
|
8
|
+
CONF=/etc/aaden/ngrok-config.yml
|
|
9
|
+
LOG=/etc/aaden/ngrok.log
|
|
10
|
+
|
|
11
|
+
if [ -d /run/systemd/system ] && [ -f "/etc/systemd/system/$SVC.service" ]; then
|
|
12
|
+
echo "重启 systemd 服务 $SVC"
|
|
13
|
+
sudoS systemctl restart "$SVC"
|
|
14
|
+
elif [ -x "$NGROK" ] && [ -f "$CONF" ]; then
|
|
15
|
+
# 没有 systemd 的老 WSL:直接重启进程
|
|
16
|
+
echo "没有 systemd 服务,直接重启 ngrok 进程"
|
|
17
|
+
sudoS pkill -f "$NGROK " 2>/dev/null
|
|
18
|
+
sleep 2
|
|
19
|
+
sudoS sh -c "nohup $NGROK -config=$CONF start-all --log=$LOG >/dev/null 2>&1 &"
|
|
20
|
+
else
|
|
21
|
+
echo "ngrok 没装好(缺 $NGROK 或 $CONF),跑完整修复 fix-ngrok"
|
|
22
|
+
aaden-cli fix-ngrok
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
sleep 20
|
|
26
|
+
echo "--- 状态"
|
|
27
|
+
[ -d /run/systemd/system ] && echo "systemd: $(systemctl is-active "$SVC" 2>&1)"
|
|
28
|
+
echo "--- ngrok 配置"
|
|
29
|
+
sudoS grep -E "server_addr|remote_port|hostname|subdomain" "$CONF" 2>/dev/null
|
|
30
|
+
echo "--- $LOG 最后 15 行"
|
|
31
|
+
sudoS tail -n 15 "$LOG" 2>/dev/null
|
|
32
|
+
echo "--- 进程"
|
|
33
|
+
if pgrep -a -f "$NGROK " ; then
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
echo "ngrok 进程不在"
|
|
37
|
+
exit 1
|
package/AadenDeploy/upgrade.sh
CHANGED
|
@@ -14,8 +14,9 @@ export PATH="/usr/local/bin:$PATH"
|
|
|
14
14
|
# 同样的命令 SSH 进去跑却正常,而 /var/log/aaden-auto-upgrade.log 远程读不到。
|
|
15
15
|
# 输出照常打到 stdout(cron 重定向进日志);单独跑这个脚本、没设变量时写 /dev/null。
|
|
16
16
|
STEPS="${AADEN_UPGRADE_STEPS:-/dev/null}"
|
|
17
|
+
# 失败时摘要优先取 npm 的 code/syscall/path 行:npm 输出的最后两行永远是「完整日志在 …debug-0.log」,看不出原因。
|
|
17
18
|
step() {
|
|
18
|
-
local name=$1 start=$SECONDS out code
|
|
19
|
+
local name=$1 start=$SECONDS out code ex
|
|
19
20
|
shift
|
|
20
21
|
out=$("$@" 2>&1)
|
|
21
22
|
code=$?
|
|
@@ -23,21 +24,72 @@ step() {
|
|
|
23
24
|
if [ "$code" = 0 ]; then
|
|
24
25
|
echo "$name=0/$((SECONDS - start))s" >>"$STEPS"
|
|
25
26
|
else
|
|
26
|
-
|
|
27
|
+
ex=$(printf '%s\n' "$out" | grep -E 'npm (ERR!|error) (code|errno|syscall|path|dest) ' | head -n 4)
|
|
28
|
+
[ -n "$ex" ] || ex=$(printf '%s' "$out" | tail -n 2)
|
|
29
|
+
echo "$name=$code/$((SECONDS - start))s($(printf '%s' "$ex" | tr '\n' ' ' | cut -c1-160))" >>"$STEPS"
|
|
27
30
|
fi
|
|
31
|
+
return "$code"
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
step config sh -c 'mkdir -p /etc/aaden/config && chmod -R 777 /etc/aaden/config'
|
|
31
34
|
|
|
32
35
|
# 和 aaden-cli 其他地方一样用 -S 从 stdin 喂密码。aaden 没有免密 sudo 的设备上,裸 sudo 没有终端读不了密码、直接失败
|
|
33
36
|
# (10-05 步骤上报:clean=1 npm=1 "a terminal is required to read the password"),npm 永远装不上,CLI 卡在 1.6.101。
|
|
34
37
|
# root 跑(cron)时 sudo 不要密码,多喂的一行没人读,无害。
|
|
35
38
|
sudoS() { echo "Innerken322." | sudo -S "$@"; }
|
|
36
39
|
|
|
40
|
+
# 以 aaden 跑(远程 selfUpgrade、云端命令、do-log 补跑)时改不了 root 建的目录权限,也要 sudo
|
|
41
|
+
step config sudoS sh -c 'mkdir -p /etc/aaden/config && chmod -R 777 /etc/aaden/config'
|
|
42
|
+
|
|
43
|
+
# 别的 aaden-cli npm 安装还在跑时,不能动手:
|
|
44
|
+
# crontab 里 04:55 和开机 sleep 30 各有一条单独的 `npm install aaden-cli -g`,慢的设备上要跑好几分钟,
|
|
45
|
+
# 正好撞上 04:59 / 开机 sleep 60 的这里。两个 npm 同时重装同一个包 —— 我们这个 1 秒内 ENOTEMPTY(退出码 217),
|
|
46
|
+
# 那一刻 aaden-cli 已被对方挪走,后面 patch/light-update/appcenter 全部 command not found;
|
|
47
|
+
# 运气差的设备两边都失败,aaden-cli 整个没了,do-log 从此停报(2026-10-07:1333、1541)。
|
|
48
|
+
# SSH 进去手动跑不会撞上,所以以前一直以为是「cron 环境问题」。
|
|
49
|
+
# 认进程看命令行:npm 自己的进程标题不一定带参数,但 cron 起的 sh -c / sudo 外壳一直活到 npm 结束。
|
|
50
|
+
# 本脚本的祖先不算 —— 远程 selfUpgrade 的 bash -lc 命令行里就有 "npm i -g aaden-cli",它在等我们跑完。
|
|
51
|
+
otherNpm() {
|
|
52
|
+
local anc=" " p=$$ q
|
|
53
|
+
while [ -n "$p" ] && [ "$p" -gt 1 ] 2>/dev/null; do
|
|
54
|
+
anc="$anc$p "
|
|
55
|
+
p=$(ps -o ppid= -p "$p" 2>/dev/null | tr -d ' ')
|
|
56
|
+
done
|
|
57
|
+
for q in $(pgrep -f 'npm (i|install) (-g )?aaden-cli' 2>/dev/null); do
|
|
58
|
+
case "$anc" in *" $q "*) ;; *) echo "$q" ;; esac
|
|
59
|
+
done
|
|
60
|
+
}
|
|
61
|
+
waitOtherNpm() {
|
|
62
|
+
local waited=0
|
|
63
|
+
while [ -n "$(otherNpm)" ]; do
|
|
64
|
+
if [ "$waited" -ge 900 ]; then
|
|
65
|
+
echo "等了 ${waited}s 仍有别的 npm 在装 aaden-cli: $(ps -o pid=,etime=,args= -p "$(otherNpm | tr '\n' ',' | sed 's/,$//')" 2>/dev/null | tr '\n' ';')"
|
|
66
|
+
return 1
|
|
67
|
+
fi
|
|
68
|
+
sleep 5
|
|
69
|
+
waited=$((waited + 5))
|
|
70
|
+
done
|
|
71
|
+
echo "waited=${waited}s"
|
|
72
|
+
}
|
|
73
|
+
|
|
37
74
|
# npm 全局重装会先把旧目录改名成 .aaden-cli-<随机串>;上次失败留下的这种目录会让之后每次都 ENOTEMPTY 失败,
|
|
38
|
-
# CLI 永远停在老版本(10-03 车队里有几台卡在 1.6.88~1.6.98
|
|
39
|
-
|
|
40
|
-
|
|
75
|
+
# CLI 永远停在老版本(10-03 车队里有几台卡在 1.6.88~1.6.98)。全局目录不一定是 /usr/local(node 走 apt 装的是 /usr),
|
|
76
|
+
# 两处都清。必须在 waitOtherNpm 之后:正在装的 npm 的 .aaden-cli-* 是它回滚用的,删了它就真回不去了。
|
|
77
|
+
cleanResidue() {
|
|
78
|
+
local root
|
|
79
|
+
root=$(sudoS npm root -g 2>/dev/null | tail -n 1)
|
|
80
|
+
# 只认绝对路径的 node_modules,免得把一行 npm 警告当路径拼进 rm -rf
|
|
81
|
+
case "$root" in /*/node_modules) ;; *) root= ;; esac
|
|
82
|
+
sudoS sh -c "rm -rf /usr/local/lib/node_modules/.aaden-cli-* ${root:+$root/.aaden-cli-*}"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
step wait waitOtherNpm
|
|
86
|
+
step clean cleanResidue
|
|
87
|
+
if ! step npm sudoS npm i aaden-cli -g || ! aaden-cli --version >/dev/null 2>&1; then
|
|
88
|
+
# 失败、或装完 aaden-cli 不见了:等一等、清一遍再装一次。不补救的话后面几步全是 command not found
|
|
89
|
+
step wait2 waitOtherNpm
|
|
90
|
+
step clean2 cleanResidue
|
|
91
|
+
step npm2 sudoS npm i aaden-cli -g
|
|
92
|
+
fi
|
|
41
93
|
|
|
42
94
|
step patch aaden-cli patch
|
|
43
95
|
|
package/dist/index.js
CHANGED
|
@@ -80,6 +80,314 @@ module.exports = {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
/***/ }),
|
|
84
|
+
|
|
85
|
+
/***/ 5363:
|
|
86
|
+
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
|
|
87
|
+
|
|
88
|
+
const fs = __nccwpck_require__(7147)
|
|
89
|
+
const os = __nccwpck_require__(2037)
|
|
90
|
+
const path = __nccwpck_require__(1017)
|
|
91
|
+
const http = __nccwpck_require__(3685)
|
|
92
|
+
const https = __nccwpck_require__(5687)
|
|
93
|
+
const {spawn, execFileSync} = __nccwpck_require__(2081)
|
|
94
|
+
|
|
95
|
+
// 拉取式设备命令 + 自动补跑。
|
|
96
|
+
//
|
|
97
|
+
// 以前云端要让设备做事,只能经 ngrok POST 门店 PHP(UpdateSelf.php),ngrok 一断就够不着;
|
|
98
|
+
// 而 WSL 设备的宿主机夜里睡眠会错过 04:59、又不重启,@reboot 也不触发,自动更新可能几天都不跑。
|
|
99
|
+
// 设备每 5 分钟的 do-log 是唯一一直在跑的东西,所以把两件事都挂在它上面:
|
|
100
|
+
// ① do-log 上报时声明 acceptCommands,云端在回复里带上这台设备待执行的命令(ERP 下发或云端自动下发)
|
|
101
|
+
// → 起一个后台进程逐条认领、执行,输出每 10 秒回传一次(ERP 里能看到回显),最后报退出码。
|
|
102
|
+
// ② 超过 20 小时没跑过 auto-upgrade 就自己补跑一次(避开午市晚市和刚开机)。
|
|
103
|
+
//
|
|
104
|
+
// 只执行下面写死的几种命令,不执行云端发来的任意 shell。
|
|
105
|
+
// 注意:门店设备上的 node 最老到 10,这里不能用 ?. / ?? 等新语法。
|
|
106
|
+
|
|
107
|
+
// 只为本地联调留的口子;设备上从不设置
|
|
108
|
+
const CLOUD = process.env.AADEN_CLOUD_URL || 'https://cloud-v2.aaden.io'
|
|
109
|
+
|
|
110
|
+
const COMMANDS = {
|
|
111
|
+
AUTO_UPGRADE: () => 'aaden-cli auto-upgrade',
|
|
112
|
+
LIGHT_UPDATE: () => 'aaden-cli light-update',
|
|
113
|
+
APPCENTER_SYNC: () => 'aaden-cli appcenter-sync',
|
|
114
|
+
PATCH: () => 'aaden-cli patch',
|
|
115
|
+
RESTART_NGROK: root => 'bash ' + JSON.stringify(path.join(root, 'AadenDeploy/restartNgrok.sh')),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 每条命令最多跑多久;latest.zip 全量在慢网络上要 20 分钟 */
|
|
119
|
+
const COMMAND_TIMEOUT_MS = 45 * 60 * 1000
|
|
120
|
+
const REPORT_INTERVAL_MS = 10 * 1000
|
|
121
|
+
const MAX_OUTPUT = 60 * 1024
|
|
122
|
+
|
|
123
|
+
/** 每次 auto-upgrade 开始时写;cron(root)和补跑(aaden)都要能写,所以 chmod 666 */
|
|
124
|
+
const AUTO_UPGRADE_STAMP = '/etc/aaden/config/lastAutoUpgrade'
|
|
125
|
+
const CATCH_UP_AFTER_MS = 20 * 3600 * 1000
|
|
126
|
+
/** 午市、晚市不补跑(设备本地时间,分钟) */
|
|
127
|
+
const PEAK_WINDOWS = [[11 * 60 + 30, 14 * 60 + 30], [17 * 60 + 30, 21 * 60 + 30]]
|
|
128
|
+
/** 开机 10 分钟内不补跑:@reboot 的 npm + auto-upgrade 会自己跑 */
|
|
129
|
+
const MIN_UPTIME_S = 10 * 60
|
|
130
|
+
|
|
131
|
+
function userName() {
|
|
132
|
+
try {
|
|
133
|
+
return os.userInfo().username
|
|
134
|
+
} catch (e) {
|
|
135
|
+
return String(process.getuid ? process.getuid() : 'x')
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function tmpFile(name) {
|
|
140
|
+
return path.join(os.tmpdir(), name + '-' + userName())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function postJson(url, body, timeoutMs) {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
const data = Buffer.from(JSON.stringify(body))
|
|
146
|
+
const req = (url.indexOf('http://') === 0 ? http : https).request(url, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: {'Content-Type': 'application/json', 'Content-Length': data.length},
|
|
149
|
+
timeout: timeoutMs || 20000,
|
|
150
|
+
}, res => {
|
|
151
|
+
const chunks = []
|
|
152
|
+
res.on('data', c => chunks.push(c))
|
|
153
|
+
res.on('end', () => {
|
|
154
|
+
const text = Buffer.concat(chunks).toString('utf8')
|
|
155
|
+
let json = null
|
|
156
|
+
try {
|
|
157
|
+
json = JSON.parse(text)
|
|
158
|
+
} catch (e) { /* 非 JSON */ }
|
|
159
|
+
if (res.statusCode >= 200 && res.statusCode < 300) resolve(json)
|
|
160
|
+
else reject(Object.assign(new Error('HTTP ' + res.statusCode + ' ' + text.slice(0, 200)), {status: res.statusCode}))
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
req.on('timeout', () => req.destroy(new Error('timeout')))
|
|
164
|
+
req.on('error', reject)
|
|
165
|
+
req.end(data)
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 后台起一个 aaden-cli 子命令,do-log 退出后它照样跑;输出追加进 /tmp 下的日志 */
|
|
170
|
+
function spawnDetached(args, logName, env) {
|
|
171
|
+
let fd = 'ignore'
|
|
172
|
+
try {
|
|
173
|
+
fd = fs.openSync(tmpFile(logName) + '.log', 'a')
|
|
174
|
+
} catch (e) { /* 写不了日志也照跑 */ }
|
|
175
|
+
const child = spawn(process.execPath, [process.argv[1]].concat(args), {
|
|
176
|
+
detached: true,
|
|
177
|
+
stdio: ['ignore', fd, fd],
|
|
178
|
+
env: Object.assign({}, process.env, env || {}),
|
|
179
|
+
})
|
|
180
|
+
child.unref()
|
|
181
|
+
return child.pid
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ------------------------------------------------------------------ ① 命令
|
|
185
|
+
|
|
186
|
+
/** do-log 拿到云端回复后调用:有命令就起后台进程去执行 */
|
|
187
|
+
function handleOffers(deviceId, reply) {
|
|
188
|
+
const body = reply && reply.data && reply.data.commands ? reply.data : reply
|
|
189
|
+
const commands = (body && Array.isArray(body.commands) ? body.commands : [])
|
|
190
|
+
.filter(c => c && c.id && c.command)
|
|
191
|
+
if (commands.length === 0) return []
|
|
192
|
+
const items = commands.map(c => c.id + ':' + c.command)
|
|
193
|
+
console.log('云端有待执行命令:', items.join(' '))
|
|
194
|
+
spawnDetached(['run-commands', String(deviceId)].concat(items), 'aaden-command-runner')
|
|
195
|
+
return commands
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function lockFile() {
|
|
199
|
+
return tmpFile('aaden-command-runner') + '.pid'
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 同一时间只有一个执行进程;上一个还在跑就退出,没领到的命令下次 do-log 会再给 */
|
|
203
|
+
function acquireLock() {
|
|
204
|
+
const file = lockFile()
|
|
205
|
+
try {
|
|
206
|
+
const pid = parseInt(fs.readFileSync(file, 'utf8'), 10)
|
|
207
|
+
if (pid && pid !== process.pid) {
|
|
208
|
+
try {
|
|
209
|
+
process.kill(pid, 0)
|
|
210
|
+
return false
|
|
211
|
+
} catch (e) {
|
|
212
|
+
if (e.code === 'EPERM') return false
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch (e) { /* 没有锁文件 */ }
|
|
216
|
+
try {
|
|
217
|
+
fs.writeFileSync(file, String(process.pid))
|
|
218
|
+
return true
|
|
219
|
+
} catch (e) {
|
|
220
|
+
return false
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function releaseLock() {
|
|
225
|
+
try {
|
|
226
|
+
if (parseInt(fs.readFileSync(lockFile(), 'utf8'), 10) === process.pid) fs.unlinkSync(lockFile())
|
|
227
|
+
} catch (e) { /* 已经没了 */ }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function stripAnsi(s) {
|
|
231
|
+
return s.replace(/\u001b\[[0-9;?]*[A-Za-z]/g, '').replace(/\r(?!\n)/g, '\n')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// aaden-cli 打印要执行的命令时会带上 sudo 密码;输出要存云端、ERP 里能看到,存之前抹掉
|
|
235
|
+
const SECRETS = ['Innerken322.']
|
|
236
|
+
|
|
237
|
+
function redact(s) {
|
|
238
|
+
return SECRETS.reduce((acc, secret) => acc.split(secret).join('***'), s)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function report(id, deviceId, output, exitCode) {
|
|
242
|
+
const body = {deviceId: String(deviceId), output: output}
|
|
243
|
+
if (exitCode !== undefined) body.exitCode = exitCode
|
|
244
|
+
// 最终结果多试几次;中途的回报丢了无所谓,下一次会整段覆盖
|
|
245
|
+
const attempts = exitCode === undefined ? 1 : 3
|
|
246
|
+
for (let i = 0; i < attempts; i++) {
|
|
247
|
+
try {
|
|
248
|
+
await postJson(CLOUD + '/device-command/' + id + '/report', body)
|
|
249
|
+
return true
|
|
250
|
+
} catch (e) {
|
|
251
|
+
console.log('回报命令 #' + id + ' 失败:', e.message)
|
|
252
|
+
if (e.status === 409 || e.status === 403 || e.status === 404) return false
|
|
253
|
+
await new Promise(r => setTimeout(r, 5000))
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return false
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function runOne(id, deviceId, shellCmd) {
|
|
260
|
+
return new Promise(resolve => {
|
|
261
|
+
let output = ''
|
|
262
|
+
let dirty = false
|
|
263
|
+
const append = chunk => {
|
|
264
|
+
output += redact(stripAnsi(chunk.toString('utf8')))
|
|
265
|
+
if (output.length > MAX_OUTPUT) output = output.slice(output.length - MAX_OUTPUT)
|
|
266
|
+
dirty = true
|
|
267
|
+
}
|
|
268
|
+
// 登录 shell:cron 下 PATH 只有 /usr/bin:/bin,aaden-cli 在 /usr/local/bin
|
|
269
|
+
const child = spawn('bash', ['-lc', 'export PATH="/usr/local/bin:$PATH"; ' + shellCmd], {
|
|
270
|
+
detached: true,
|
|
271
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
272
|
+
env: Object.assign({}, process.env, {AADEN_UPGRADE_TRIGGER: '命令 #' + id}),
|
|
273
|
+
})
|
|
274
|
+
child.stdout.on('data', append)
|
|
275
|
+
child.stderr.on('data', append)
|
|
276
|
+
const timer = setInterval(() => {
|
|
277
|
+
if (!dirty) return
|
|
278
|
+
dirty = false
|
|
279
|
+
report(id, deviceId, output)
|
|
280
|
+
}, REPORT_INTERVAL_MS)
|
|
281
|
+
const killer = setTimeout(() => {
|
|
282
|
+
append('\n[aaden-cli] 超过 ' + (COMMAND_TIMEOUT_MS / 60000) + ' 分钟,终止\n')
|
|
283
|
+
try {
|
|
284
|
+
process.kill(-child.pid, 'SIGTERM')
|
|
285
|
+
} catch (e) { /* 已经退出 */ }
|
|
286
|
+
}, COMMAND_TIMEOUT_MS)
|
|
287
|
+
child.on('error', e => append('\n[aaden-cli] 启动失败: ' + e.message + '\n'))
|
|
288
|
+
child.on('close', (code, signal) => {
|
|
289
|
+
clearInterval(timer)
|
|
290
|
+
clearTimeout(killer)
|
|
291
|
+
const exitCode = typeof code === 'number' ? code : 128
|
|
292
|
+
if (signal) append('\n[aaden-cli] 被信号 ' + signal + ' 终止\n')
|
|
293
|
+
resolve({exitCode: exitCode, output: output})
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** `aaden-cli run-commands <deviceId> <id:COMMAND>...`:由 do-log 在后台起,逐条认领、执行、回报 */
|
|
299
|
+
async function runCommands(rootDir, version, deviceId, items) {
|
|
300
|
+
if (!acquireLock()) {
|
|
301
|
+
console.log('已有命令执行进程在跑,这次不领')
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
for (const item of items) {
|
|
306
|
+
const parts = String(item).split(':')
|
|
307
|
+
const id = parseInt(parts[0], 10)
|
|
308
|
+
const name = parts[1]
|
|
309
|
+
if (!id || !name) continue
|
|
310
|
+
try {
|
|
311
|
+
await postJson(CLOUD + '/device-command/' + id + '/start', {deviceId: String(deviceId), cliVersion: version})
|
|
312
|
+
} catch (e) {
|
|
313
|
+
console.log('认领命令 #' + id + ' 失败(可能已被领走或取消):', e.message)
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
const build = COMMANDS[name]
|
|
317
|
+
if (!build) {
|
|
318
|
+
await report(id, deviceId, '这个版本的 aaden-cli (' + version + ') 不认识命令 ' + name, 2)
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
321
|
+
const cmd = build(rootDir)
|
|
322
|
+
console.log('执行命令 #' + id + ' ' + name + ': ' + cmd)
|
|
323
|
+
const result = await runOne(id, deviceId, cmd)
|
|
324
|
+
await report(id, deviceId, '$ ' + cmd + '\n' + result.output, result.exitCode)
|
|
325
|
+
}
|
|
326
|
+
} finally {
|
|
327
|
+
releaseLock()
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ------------------------------------------------------------------ ② 自动补跑
|
|
332
|
+
|
|
333
|
+
function markAutoUpgradeStarted() {
|
|
334
|
+
try {
|
|
335
|
+
fs.writeFileSync(AUTO_UPGRADE_STAMP, String(Date.now()))
|
|
336
|
+
fs.chmodSync(AUTO_UPGRADE_STAMP, 0o666)
|
|
337
|
+
} catch (e) { /* 写不了就靠补跑自己的时间戳兜底 */ }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function readStamp(file) {
|
|
341
|
+
try {
|
|
342
|
+
const n = parseInt(fs.readFileSync(file, 'utf8'), 10)
|
|
343
|
+
if (n > 0) return n
|
|
344
|
+
return fs.statSync(file).mtimeMs
|
|
345
|
+
} catch (e) {
|
|
346
|
+
return 0
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function catchUpStampFile() {
|
|
351
|
+
return path.join(os.homedir(), '.aaden-catchup')
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function autoUpgradeRunning() {
|
|
355
|
+
try {
|
|
356
|
+
// 不经 shell 直接调 pgrep:经 sh -c 的话 sh 自己的命令行里就有这串字,会把自己当成「正在跑」
|
|
357
|
+
const pids = execFileSync('pgrep', ['-f', 'aaden-cli auto-upgrade'], {encoding: 'utf8', timeout: 5000})
|
|
358
|
+
.split('\n').map(s => parseInt(s, 10)).filter(pid => pid && pid !== process.pid)
|
|
359
|
+
return pids.length > 0
|
|
360
|
+
} catch (e) {
|
|
361
|
+
return false
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* do-log 每次上报后调用:超过 20 小时没跑过 auto-upgrade 就在后台补跑一次。
|
|
367
|
+
* 返回没补跑的原因(或 'started'),只用于日志。
|
|
368
|
+
*/
|
|
369
|
+
function maybeCatchUpAutoUpgrade() {
|
|
370
|
+
const now = new Date()
|
|
371
|
+
const minutes = now.getHours() * 60 + now.getMinutes()
|
|
372
|
+
if (PEAK_WINDOWS.some(w => minutes >= w[0] && minutes < w[1])) return 'peak'
|
|
373
|
+
if (os.uptime() < MIN_UPTIME_S) return 'boot'
|
|
374
|
+
const last = Math.max(readStamp(AUTO_UPGRADE_STAMP), readStamp(catchUpStampFile()))
|
|
375
|
+
if (Date.now() - last < CATCH_UP_AFTER_MS) return 'recent'
|
|
376
|
+
if (autoUpgradeRunning()) return 'running'
|
|
377
|
+
// 先把补跑时间戳写上:写不上就不补跑,免得每 5 分钟起一次
|
|
378
|
+
try {
|
|
379
|
+
fs.writeFileSync(catchUpStampFile(), String(Date.now()))
|
|
380
|
+
} catch (e) {
|
|
381
|
+
return 'no-stamp'
|
|
382
|
+
}
|
|
383
|
+
const hours = last ? Math.round((Date.now() - last) / 3600000) + ' 小时' : '很久'
|
|
384
|
+
spawnDetached(['auto-upgrade'], 'aaden-catchup', {AADEN_UPGRADE_TRIGGER: '补跑:' + hours + '没跑过自动更新'})
|
|
385
|
+
return 'started'
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = {handleOffers, runCommands, markAutoUpgradeStarted, maybeCatchUpAutoUpgrade}
|
|
389
|
+
|
|
390
|
+
|
|
83
391
|
/***/ }),
|
|
84
392
|
|
|
85
393
|
/***/ 7415:
|
|
@@ -112,7 +420,7 @@ async function deviceEcho(data) {
|
|
|
112
420
|
})
|
|
113
421
|
const {frontendVersion, cliVersion, diskUsage, upTime, ngrokIsNew, hostInfo, tseInfo} = Object.assign({}, defaultData, data)
|
|
114
422
|
if (parseInt(myDeviceId) === -1) {
|
|
115
|
-
return
|
|
423
|
+
return null
|
|
116
424
|
}
|
|
117
425
|
const {name, adress1, postCode, city, state} = await getNiceRestaurantInfo()
|
|
118
426
|
const waitSecond = (parseInt(myDeviceId) % 30) * 1000
|
|
@@ -121,7 +429,7 @@ async function deviceEcho(data) {
|
|
|
121
429
|
resolve()
|
|
122
430
|
}, waitSecond))
|
|
123
431
|
try {
|
|
124
|
-
await hillo.jsonPost('https://cloud-v2.aaden.io/deviceLog/create', {
|
|
432
|
+
const reply = await hillo.jsonPost('https://cloud-v2.aaden.io/deviceLog/create', {
|
|
125
433
|
deviceId: myDeviceId,
|
|
126
434
|
frontendVersion: frontendVersion,
|
|
127
435
|
cliVersion,
|
|
@@ -133,13 +441,17 @@ async function deviceEcho(data) {
|
|
|
133
441
|
ngrokOk: ngrokIsNew,
|
|
134
442
|
deviceName: `${name}\n${adress1}\n${postCode} ${city} ${state}`,
|
|
135
443
|
...hostInfo,
|
|
136
|
-
...tseInfo
|
|
444
|
+
...tseInfo,
|
|
445
|
+
// 回复里要带待执行命令(见 modules/device-commands.js);老云端忽略这个字段
|
|
446
|
+
acceptCommands: true
|
|
137
447
|
})
|
|
138
448
|
console.log('report to remote server complete')
|
|
449
|
+
return {deviceId: myDeviceId, reply}
|
|
139
450
|
} catch (e) {
|
|
140
451
|
console.log(e.message)
|
|
141
452
|
console.log('report to remote server failed')
|
|
142
453
|
}
|
|
454
|
+
return null
|
|
143
455
|
|
|
144
456
|
|
|
145
457
|
}
|
|
@@ -18863,7 +19175,7 @@ module.exports = JSON.parse('{"name":"axios","version":"0.19.2","description":"P
|
|
|
18863
19175
|
/***/ ((module) => {
|
|
18864
19176
|
|
|
18865
19177
|
"use strict";
|
|
18866
|
-
module.exports = {"i8":"1.6.
|
|
19178
|
+
module.exports = {"i8":"1.6.112"};
|
|
18867
19179
|
|
|
18868
19180
|
/***/ })
|
|
18869
19181
|
|
|
@@ -18951,6 +19263,7 @@ const prompts = __nccwpck_require__(9646)
|
|
|
18951
19263
|
const {deviceEcho, logAction, getCurrentDeviceId} = __nccwpck_require__(7415)
|
|
18952
19264
|
const {collectHostInfo} = __nccwpck_require__(3727)
|
|
18953
19265
|
const {collectTseInfo} = __nccwpck_require__(6677)
|
|
19266
|
+
const deviceCommands = __nccwpck_require__(5363)
|
|
18954
19267
|
const GlobalDeviceId = 'notset'
|
|
18955
19268
|
|
|
18956
19269
|
const {program} = __nccwpck_require__(1904)
|
|
@@ -19010,6 +19323,9 @@ program.command('auto-send-tea-maker-task').action(autoSendTeaMakerTask)
|
|
|
19010
19323
|
program.command('export-tse').action(exportTse)
|
|
19011
19324
|
program.command('fix-ngrok').action(fixNgrok)
|
|
19012
19325
|
program.command('do-log').action(doLog)
|
|
19326
|
+
// do-log 领到云端命令后在后台起的执行进程(见 modules/device-commands.js),不手动调
|
|
19327
|
+
program.command('run-commands <deviceId> [items...]').action((deviceId, items) =>
|
|
19328
|
+
deviceCommands.runCommands(path.resolve(findMe(), '..'), version, deviceId, items || []))
|
|
19013
19329
|
program.command('clean-backup-space').action(doCompress)
|
|
19014
19330
|
program.command('ensure-print-log-rotate').action(ensurePrintLogRotate)
|
|
19015
19331
|
|
|
@@ -19516,6 +19832,8 @@ async function lightUpdate() {
|
|
|
19516
19832
|
const suffix = status ? ' [' + status + ']' : ''
|
|
19517
19833
|
const newVersion = readBackendVersionFresh()
|
|
19518
19834
|
if (code !== 0) {
|
|
19835
|
+
// 退出码带出去:upgrade.sh 的 lu 步骤和云端命令的结果才看得出 light-update 失败了
|
|
19836
|
+
process.exitCode = code || 1
|
|
19519
19837
|
await logAction('light-update 失败/未部署 (仍 ' + newVersion + ')' + suffix, code || 1)
|
|
19520
19838
|
} else if (newVersion !== localVersion) {
|
|
19521
19839
|
await logAction('light-update 完成 ' + localVersion + ' -> ' + newVersion + suffix, 0)
|
|
@@ -19700,11 +20018,18 @@ function getMyDeviceId() {
|
|
|
19700
20018
|
}
|
|
19701
20019
|
|
|
19702
20020
|
async function autoUpgrade() {
|
|
20021
|
+
// 记下这次开始的时间:do-log 据此判断要不要补跑(超过 20 小时没跑过)
|
|
20022
|
+
deviceCommands.markAutoUpgradeStarted()
|
|
19703
20023
|
if (!ngrokNewestVersion()) {
|
|
19704
20024
|
await fixNgrok()
|
|
19705
20025
|
}
|
|
19706
20026
|
const summary = await upgrade()
|
|
19707
|
-
|
|
20027
|
+
// 谁触发的:cron / 开机不带;do-log 补跑、云端命令会带上(AADEN_UPGRADE_TRIGGER)
|
|
20028
|
+
const trigger = process.env.AADEN_UPGRADE_TRIGGER ? ' (' + process.env.AADEN_UPGRADE_TRIGGER + ')' : ''
|
|
20029
|
+
console.log('自动执行了更新aaden-cli' + trigger + summary)
|
|
20030
|
+
await logAction("自动执行了更新aaden-cli" + trigger + summary)
|
|
20031
|
+
// 有步骤失败就非 0 退出,云端命令的结果才看得出来
|
|
20032
|
+
if (/=[1-9]|upgrade\.sh 没跑起来/.test(summary)) process.exitCode = 1
|
|
19708
20033
|
}
|
|
19709
20034
|
|
|
19710
20035
|
/**
|
|
@@ -19728,8 +20053,10 @@ async function upgrade() {
|
|
|
19728
20053
|
fs.unlinkSync(stepsFile)
|
|
19729
20054
|
} catch (_) {}
|
|
19730
20055
|
const failed = code !== 0 || !steps || /=[1-9]/.test(steps)
|
|
19731
|
-
|
|
20056
|
+
const summary = ' [' + (steps || 'upgrade.sh 没跑起来') + (code ? ' exit=' + code : '') + ']'
|
|
19732
20057
|
+ (failed ? ' ' + upgradeEnvInfo() : '')
|
|
20058
|
+
// 云端 device_event_log.content 是 varchar(1024),超长整条事件会丢;npm 重试时步骤变多,截一下
|
|
20059
|
+
return summary.length > 900 ? summary.slice(0, 897) + '...' : summary
|
|
19733
20060
|
}
|
|
19734
20061
|
|
|
19735
20062
|
function upgradeEnvInfo() {
|
|
@@ -19832,20 +20159,32 @@ function getUpTime() {
|
|
|
19832
20159
|
|
|
19833
20160
|
}
|
|
19834
20161
|
|
|
20162
|
+
/** 单项探测失败(命令卡住、输出不对)就报空值,不能让整个 do-log 挂掉 */
|
|
20163
|
+
function doLogField(fn) {
|
|
20164
|
+
try {
|
|
20165
|
+
return fn() || ''
|
|
20166
|
+
} catch (e) {
|
|
20167
|
+
console.log('do-log 探测失败:', e.message)
|
|
20168
|
+
return ''
|
|
20169
|
+
}
|
|
20170
|
+
}
|
|
20171
|
+
|
|
19835
20172
|
async function doLog() {
|
|
19836
|
-
|
|
19837
|
-
|
|
19838
|
-
|
|
19839
|
-
|
|
19840
|
-
|
|
19841
|
-
|
|
19842
|
-
const
|
|
19843
|
-
.split(' ')
|
|
19844
|
-
|
|
19845
|
-
|
|
20173
|
+
// snap info 加超时:snapd 刷新卡住时它会一直不返回,do-log 就再也报不上来,
|
|
20174
|
+
// 设备在云端看就是「失联」,云端命令也领不到
|
|
20175
|
+
const frontendVersion = doLogField(() => {
|
|
20176
|
+
const list = (cli.executeInShellSync('timeout 20 snap info aaden').output || '').split('\n')
|
|
20177
|
+
return (list[list.length - 2] || '').replace(/\s+/g, ' ').split(' ')[1]
|
|
20178
|
+
})
|
|
20179
|
+
const diskUsage = doLogField(() => {
|
|
20180
|
+
const parts = (cli.executeInShellSync('timeout 20 df -h --total | command grep total | head -c-3 && echo \'\'').output || '').split(' ')
|
|
20181
|
+
return parts[parts.length - 1].replace('\n', '')
|
|
20182
|
+
})
|
|
20183
|
+
|
|
20184
|
+
const echo = await deviceEcho({
|
|
19846
20185
|
frontendVersion,
|
|
19847
20186
|
cliVersion: version,
|
|
19848
|
-
diskUsage
|
|
20187
|
+
diskUsage,
|
|
19849
20188
|
upTime: getUpTime(),
|
|
19850
20189
|
ngrokIsNew: ngrokNewestVersion(),
|
|
19851
20190
|
hostInfo: collectHostInfo(),
|
|
@@ -19854,6 +20193,25 @@ async function doLog() {
|
|
|
19854
20193
|
)
|
|
19855
20194
|
console.log('finish')
|
|
19856
20195
|
|
|
20196
|
+
// 云端回复里的待执行命令:起后台进程去执行(do-log 自己马上退出)
|
|
20197
|
+
let offered = []
|
|
20198
|
+
if (echo && echo.reply) {
|
|
20199
|
+
try {
|
|
20200
|
+
offered = deviceCommands.handleOffers(echo.deviceId, echo.reply)
|
|
20201
|
+
} catch (e) {
|
|
20202
|
+
console.log('处理云端命令失败:', e.message)
|
|
20203
|
+
}
|
|
20204
|
+
}
|
|
20205
|
+
// 超过 20 小时没跑过 auto-upgrade 就补跑(WSL 宿主机夜里睡眠错过 04:59 等);这一轮已经有命令要跑就先不补
|
|
20206
|
+
if (echo && offered.length === 0) {
|
|
20207
|
+
try {
|
|
20208
|
+
const r = deviceCommands.maybeCatchUpAutoUpgrade()
|
|
20209
|
+
console.log('auto-upgrade 补跑检查:', r)
|
|
20210
|
+
if (r === 'started') await logAction('超过 20 小时没跑过自动更新,后台补跑一次')
|
|
20211
|
+
} catch (e) {
|
|
20212
|
+
console.log('补跑检查失败:', e.message)
|
|
20213
|
+
}
|
|
20214
|
+
}
|
|
19857
20215
|
|
|
19858
20216
|
process.exit(0)
|
|
19859
20217
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 一次性修复:aaden 的 crontab 里 do-log(每 5 分钟上报 + 领云端命令 + 自动补跑)那一行
|
|
3
|
+
# 缺了、或者指向的 aaden-cli 不存在时,改成标准写法 /usr/local/bin/aaden-cli do-log。
|
|
4
|
+
# 起因:10-07 1541 在 npm 并发重装之后 CLI 已经修好、远程更新也都成功,do-log 却再没上报过;
|
|
5
|
+
# 老设备上这一行有写裸 aaden-cli(cron 的 PATH 只有 /usr/bin:/bin)或 /usr/bin/aaden-cli 的。
|
|
6
|
+
# 只动这一行,其他行原样保留;改了的话把原来那行报给云端,方便事后看是哪种情况。
|
|
7
|
+
sudoS() { echo "Innerken322." | sudo -S -p '' "$@"; }
|
|
8
|
+
CLI=/usr/local/bin/aaden-cli
|
|
9
|
+
WANT="*/5 * * * * $CLI do-log"
|
|
10
|
+
|
|
11
|
+
[ -x "$CLI" ] || { echo "没有 $CLI,跳过"; exit 0; }
|
|
12
|
+
# 有的老设备 do-log 写在 root 的 crontab 或 /etc/cron.d 里:那样就别再往 aaden 这边加,不然每 5 分钟报两次
|
|
13
|
+
if sudoS crontab -l -u root 2>/dev/null | grep -v '^[[:space:]]*#' | grep -q 'do-log' \
|
|
14
|
+
|| grep -rqs --exclude='*.dpkg-*' 'aaden-cli do-log' /etc/cron.d 2>/dev/null; then
|
|
15
|
+
echo "do-log 在 root 的 crontab 或 /etc/cron.d 里,不动 aaden 的"; exit 0
|
|
16
|
+
fi
|
|
17
|
+
CUR=$(sudoS crontab -l -u aaden 2>/dev/null) || { echo "aaden 没有 crontab,跳过(整套定时任务缺失要 aaden-cli fix-cronjob)"; exit 0; }
|
|
18
|
+
|
|
19
|
+
LINE=$(printf '%s\n' "$CUR" | grep -v '^[[:space:]]*#' | grep -m1 'do-log')
|
|
20
|
+
if [ -n "$LINE" ]; then
|
|
21
|
+
# 第 6 个字段起是命令;@reboot 之类的特殊写法只占 1 个字段
|
|
22
|
+
case "$LINE" in
|
|
23
|
+
@*) CMD=$(printf '%s' "$LINE" | awk '{print $2}') ;;
|
|
24
|
+
*) CMD=$(printf '%s' "$LINE" | awk '{print $6}') ;;
|
|
25
|
+
esac
|
|
26
|
+
case "$CMD" in
|
|
27
|
+
"$CLI") echo "do-log 已是标准写法,不动"; exit 0 ;;
|
|
28
|
+
/*) [ -x "$CMD" ] && { echo "do-log 指向 $CMD,存在,不动"; exit 0; } ;;
|
|
29
|
+
*) [ -x "/usr/bin/$CMD" ] || [ -x "/bin/$CMD" ] && { echo "do-log 用 $CMD,cron 的 PATH 里找得到,不动"; exit 0; } ;;
|
|
30
|
+
esac
|
|
31
|
+
NEW=$(printf '%s\n' "$CUR" | awk -v line="$LINE" -v want="$WANT" '$0 == line { print want; next } { print }')
|
|
32
|
+
WHAT="do-log 那一行指向的命令不存在,已改成标准写法。原来是:$LINE"
|
|
33
|
+
else
|
|
34
|
+
NEW=$(printf '%s\n%s\n' "$CUR" "$WANT")
|
|
35
|
+
WHAT="aaden 的 crontab 里没有 do-log,已补上"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
printf '%s\n' "$NEW" | sudoS crontab -u aaden - || { echo "写 crontab 失败"; exit 1; }
|
|
39
|
+
echo "$WHAT"
|
|
40
|
+
DEVICE_ID=$(grep -e DeviceId /var/www/html/Resource/config/config.ini 2>/dev/null | cut -d = -f 2 | tr -d '" \r')
|
|
41
|
+
[ -n "$DEVICE_ID" ] && python3 -c 'import json, sys, urllib.request; urllib.request.urlopen(urllib.request.Request("https://cloud-v2.aaden.io/deviceLog/createEvent", json.dumps({"deviceId": sys.argv[1], "content": sys.argv[2] + " result:0", "type": "系统生成", "operator": "patch ensureDoLogCron"}).encode(), {"Content-Type": "application/json"}), timeout=10)' "$DEVICE_ID" "$WHAT" 2>/dev/null
|
|
42
|
+
exit 0
|
package/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const prompts = require('prompts')
|
|
|
14
14
|
const {deviceEcho, logAction, getCurrentDeviceId} = require('./modules/firebase')
|
|
15
15
|
const {collectHostInfo} = require('./modules/host-info')
|
|
16
16
|
const {collectTseInfo} = require('./modules/tse-info')
|
|
17
|
+
const deviceCommands = require('./modules/device-commands')
|
|
17
18
|
const GlobalDeviceId = 'notset'
|
|
18
19
|
|
|
19
20
|
const {program} = require('commander')
|
|
@@ -73,6 +74,9 @@ program.command('auto-send-tea-maker-task').action(autoSendTeaMakerTask)
|
|
|
73
74
|
program.command('export-tse').action(exportTse)
|
|
74
75
|
program.command('fix-ngrok').action(fixNgrok)
|
|
75
76
|
program.command('do-log').action(doLog)
|
|
77
|
+
// do-log 领到云端命令后在后台起的执行进程(见 modules/device-commands.js),不手动调
|
|
78
|
+
program.command('run-commands <deviceId> [items...]').action((deviceId, items) =>
|
|
79
|
+
deviceCommands.runCommands(path.resolve(findMe(), '..'), version, deviceId, items || []))
|
|
76
80
|
program.command('clean-backup-space').action(doCompress)
|
|
77
81
|
program.command('ensure-print-log-rotate').action(ensurePrintLogRotate)
|
|
78
82
|
|
|
@@ -579,6 +583,8 @@ async function lightUpdate() {
|
|
|
579
583
|
const suffix = status ? ' [' + status + ']' : ''
|
|
580
584
|
const newVersion = readBackendVersionFresh()
|
|
581
585
|
if (code !== 0) {
|
|
586
|
+
// 退出码带出去:upgrade.sh 的 lu 步骤和云端命令的结果才看得出 light-update 失败了
|
|
587
|
+
process.exitCode = code || 1
|
|
582
588
|
await logAction('light-update 失败/未部署 (仍 ' + newVersion + ')' + suffix, code || 1)
|
|
583
589
|
} else if (newVersion !== localVersion) {
|
|
584
590
|
await logAction('light-update 完成 ' + localVersion + ' -> ' + newVersion + suffix, 0)
|
|
@@ -763,11 +769,18 @@ function getMyDeviceId() {
|
|
|
763
769
|
}
|
|
764
770
|
|
|
765
771
|
async function autoUpgrade() {
|
|
772
|
+
// 记下这次开始的时间:do-log 据此判断要不要补跑(超过 20 小时没跑过)
|
|
773
|
+
deviceCommands.markAutoUpgradeStarted()
|
|
766
774
|
if (!ngrokNewestVersion()) {
|
|
767
775
|
await fixNgrok()
|
|
768
776
|
}
|
|
769
777
|
const summary = await upgrade()
|
|
770
|
-
|
|
778
|
+
// 谁触发的:cron / 开机不带;do-log 补跑、云端命令会带上(AADEN_UPGRADE_TRIGGER)
|
|
779
|
+
const trigger = process.env.AADEN_UPGRADE_TRIGGER ? ' (' + process.env.AADEN_UPGRADE_TRIGGER + ')' : ''
|
|
780
|
+
console.log('自动执行了更新aaden-cli' + trigger + summary)
|
|
781
|
+
await logAction("自动执行了更新aaden-cli" + trigger + summary)
|
|
782
|
+
// 有步骤失败就非 0 退出,云端命令的结果才看得出来
|
|
783
|
+
if (/=[1-9]|upgrade\.sh 没跑起来/.test(summary)) process.exitCode = 1
|
|
771
784
|
}
|
|
772
785
|
|
|
773
786
|
/**
|
|
@@ -791,8 +804,10 @@ async function upgrade() {
|
|
|
791
804
|
fs.unlinkSync(stepsFile)
|
|
792
805
|
} catch (_) {}
|
|
793
806
|
const failed = code !== 0 || !steps || /=[1-9]/.test(steps)
|
|
794
|
-
|
|
807
|
+
const summary = ' [' + (steps || 'upgrade.sh 没跑起来') + (code ? ' exit=' + code : '') + ']'
|
|
795
808
|
+ (failed ? ' ' + upgradeEnvInfo() : '')
|
|
809
|
+
// 云端 device_event_log.content 是 varchar(1024),超长整条事件会丢;npm 重试时步骤变多,截一下
|
|
810
|
+
return summary.length > 900 ? summary.slice(0, 897) + '...' : summary
|
|
796
811
|
}
|
|
797
812
|
|
|
798
813
|
function upgradeEnvInfo() {
|
|
@@ -895,20 +910,32 @@ function getUpTime() {
|
|
|
895
910
|
|
|
896
911
|
}
|
|
897
912
|
|
|
913
|
+
/** 单项探测失败(命令卡住、输出不对)就报空值,不能让整个 do-log 挂掉 */
|
|
914
|
+
function doLogField(fn) {
|
|
915
|
+
try {
|
|
916
|
+
return fn() || ''
|
|
917
|
+
} catch (e) {
|
|
918
|
+
console.log('do-log 探测失败:', e.message)
|
|
919
|
+
return ''
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
898
923
|
async function doLog() {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
const
|
|
906
|
-
.split(' ')
|
|
907
|
-
|
|
908
|
-
|
|
924
|
+
// snap info 加超时:snapd 刷新卡住时它会一直不返回,do-log 就再也报不上来,
|
|
925
|
+
// 设备在云端看就是「失联」,云端命令也领不到
|
|
926
|
+
const frontendVersion = doLogField(() => {
|
|
927
|
+
const list = (cli.executeInShellSync('timeout 20 snap info aaden').output || '').split('\n')
|
|
928
|
+
return (list[list.length - 2] || '').replace(/\s+/g, ' ').split(' ')[1]
|
|
929
|
+
})
|
|
930
|
+
const diskUsage = doLogField(() => {
|
|
931
|
+
const parts = (cli.executeInShellSync('timeout 20 df -h --total | command grep total | head -c-3 && echo \'\'').output || '').split(' ')
|
|
932
|
+
return parts[parts.length - 1].replace('\n', '')
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
const echo = await deviceEcho({
|
|
909
936
|
frontendVersion,
|
|
910
937
|
cliVersion: version,
|
|
911
|
-
diskUsage
|
|
938
|
+
diskUsage,
|
|
912
939
|
upTime: getUpTime(),
|
|
913
940
|
ngrokIsNew: ngrokNewestVersion(),
|
|
914
941
|
hostInfo: collectHostInfo(),
|
|
@@ -917,6 +944,25 @@ async function doLog() {
|
|
|
917
944
|
)
|
|
918
945
|
console.log('finish')
|
|
919
946
|
|
|
947
|
+
// 云端回复里的待执行命令:起后台进程去执行(do-log 自己马上退出)
|
|
948
|
+
let offered = []
|
|
949
|
+
if (echo && echo.reply) {
|
|
950
|
+
try {
|
|
951
|
+
offered = deviceCommands.handleOffers(echo.deviceId, echo.reply)
|
|
952
|
+
} catch (e) {
|
|
953
|
+
console.log('处理云端命令失败:', e.message)
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
// 超过 20 小时没跑过 auto-upgrade 就补跑(WSL 宿主机夜里睡眠错过 04:59 等);这一轮已经有命令要跑就先不补
|
|
957
|
+
if (echo && offered.length === 0) {
|
|
958
|
+
try {
|
|
959
|
+
const r = deviceCommands.maybeCatchUpAutoUpgrade()
|
|
960
|
+
console.log('auto-upgrade 补跑检查:', r)
|
|
961
|
+
if (r === 'started') await logAction('超过 20 小时没跑过自动更新,后台补跑一次')
|
|
962
|
+
} catch (e) {
|
|
963
|
+
console.log('补跑检查失败:', e.message)
|
|
964
|
+
}
|
|
965
|
+
}
|
|
920
966
|
|
|
921
967
|
process.exit(0)
|
|
922
968
|
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const os = require('os')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const http = require('http')
|
|
5
|
+
const https = require('https')
|
|
6
|
+
const {spawn, execFileSync} = require('child_process')
|
|
7
|
+
|
|
8
|
+
// 拉取式设备命令 + 自动补跑。
|
|
9
|
+
//
|
|
10
|
+
// 以前云端要让设备做事,只能经 ngrok POST 门店 PHP(UpdateSelf.php),ngrok 一断就够不着;
|
|
11
|
+
// 而 WSL 设备的宿主机夜里睡眠会错过 04:59、又不重启,@reboot 也不触发,自动更新可能几天都不跑。
|
|
12
|
+
// 设备每 5 分钟的 do-log 是唯一一直在跑的东西,所以把两件事都挂在它上面:
|
|
13
|
+
// ① do-log 上报时声明 acceptCommands,云端在回复里带上这台设备待执行的命令(ERP 下发或云端自动下发)
|
|
14
|
+
// → 起一个后台进程逐条认领、执行,输出每 10 秒回传一次(ERP 里能看到回显),最后报退出码。
|
|
15
|
+
// ② 超过 20 小时没跑过 auto-upgrade 就自己补跑一次(避开午市晚市和刚开机)。
|
|
16
|
+
//
|
|
17
|
+
// 只执行下面写死的几种命令,不执行云端发来的任意 shell。
|
|
18
|
+
// 注意:门店设备上的 node 最老到 10,这里不能用 ?. / ?? 等新语法。
|
|
19
|
+
|
|
20
|
+
// 只为本地联调留的口子;设备上从不设置
|
|
21
|
+
const CLOUD = process.env.AADEN_CLOUD_URL || 'https://cloud-v2.aaden.io'
|
|
22
|
+
|
|
23
|
+
const COMMANDS = {
|
|
24
|
+
AUTO_UPGRADE: () => 'aaden-cli auto-upgrade',
|
|
25
|
+
LIGHT_UPDATE: () => 'aaden-cli light-update',
|
|
26
|
+
APPCENTER_SYNC: () => 'aaden-cli appcenter-sync',
|
|
27
|
+
PATCH: () => 'aaden-cli patch',
|
|
28
|
+
RESTART_NGROK: root => 'bash ' + JSON.stringify(path.join(root, 'AadenDeploy/restartNgrok.sh')),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 每条命令最多跑多久;latest.zip 全量在慢网络上要 20 分钟 */
|
|
32
|
+
const COMMAND_TIMEOUT_MS = 45 * 60 * 1000
|
|
33
|
+
const REPORT_INTERVAL_MS = 10 * 1000
|
|
34
|
+
const MAX_OUTPUT = 60 * 1024
|
|
35
|
+
|
|
36
|
+
/** 每次 auto-upgrade 开始时写;cron(root)和补跑(aaden)都要能写,所以 chmod 666 */
|
|
37
|
+
const AUTO_UPGRADE_STAMP = '/etc/aaden/config/lastAutoUpgrade'
|
|
38
|
+
const CATCH_UP_AFTER_MS = 20 * 3600 * 1000
|
|
39
|
+
/** 午市、晚市不补跑(设备本地时间,分钟) */
|
|
40
|
+
const PEAK_WINDOWS = [[11 * 60 + 30, 14 * 60 + 30], [17 * 60 + 30, 21 * 60 + 30]]
|
|
41
|
+
/** 开机 10 分钟内不补跑:@reboot 的 npm + auto-upgrade 会自己跑 */
|
|
42
|
+
const MIN_UPTIME_S = 10 * 60
|
|
43
|
+
|
|
44
|
+
function userName() {
|
|
45
|
+
try {
|
|
46
|
+
return os.userInfo().username
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return String(process.getuid ? process.getuid() : 'x')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tmpFile(name) {
|
|
53
|
+
return path.join(os.tmpdir(), name + '-' + userName())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function postJson(url, body, timeoutMs) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const data = Buffer.from(JSON.stringify(body))
|
|
59
|
+
const req = (url.indexOf('http://') === 0 ? http : https).request(url, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {'Content-Type': 'application/json', 'Content-Length': data.length},
|
|
62
|
+
timeout: timeoutMs || 20000,
|
|
63
|
+
}, res => {
|
|
64
|
+
const chunks = []
|
|
65
|
+
res.on('data', c => chunks.push(c))
|
|
66
|
+
res.on('end', () => {
|
|
67
|
+
const text = Buffer.concat(chunks).toString('utf8')
|
|
68
|
+
let json = null
|
|
69
|
+
try {
|
|
70
|
+
json = JSON.parse(text)
|
|
71
|
+
} catch (e) { /* 非 JSON */ }
|
|
72
|
+
if (res.statusCode >= 200 && res.statusCode < 300) resolve(json)
|
|
73
|
+
else reject(Object.assign(new Error('HTTP ' + res.statusCode + ' ' + text.slice(0, 200)), {status: res.statusCode}))
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
req.on('timeout', () => req.destroy(new Error('timeout')))
|
|
77
|
+
req.on('error', reject)
|
|
78
|
+
req.end(data)
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 后台起一个 aaden-cli 子命令,do-log 退出后它照样跑;输出追加进 /tmp 下的日志 */
|
|
83
|
+
function spawnDetached(args, logName, env) {
|
|
84
|
+
let fd = 'ignore'
|
|
85
|
+
try {
|
|
86
|
+
fd = fs.openSync(tmpFile(logName) + '.log', 'a')
|
|
87
|
+
} catch (e) { /* 写不了日志也照跑 */ }
|
|
88
|
+
const child = spawn(process.execPath, [process.argv[1]].concat(args), {
|
|
89
|
+
detached: true,
|
|
90
|
+
stdio: ['ignore', fd, fd],
|
|
91
|
+
env: Object.assign({}, process.env, env || {}),
|
|
92
|
+
})
|
|
93
|
+
child.unref()
|
|
94
|
+
return child.pid
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ------------------------------------------------------------------ ① 命令
|
|
98
|
+
|
|
99
|
+
/** do-log 拿到云端回复后调用:有命令就起后台进程去执行 */
|
|
100
|
+
function handleOffers(deviceId, reply) {
|
|
101
|
+
const body = reply && reply.data && reply.data.commands ? reply.data : reply
|
|
102
|
+
const commands = (body && Array.isArray(body.commands) ? body.commands : [])
|
|
103
|
+
.filter(c => c && c.id && c.command)
|
|
104
|
+
if (commands.length === 0) return []
|
|
105
|
+
const items = commands.map(c => c.id + ':' + c.command)
|
|
106
|
+
console.log('云端有待执行命令:', items.join(' '))
|
|
107
|
+
spawnDetached(['run-commands', String(deviceId)].concat(items), 'aaden-command-runner')
|
|
108
|
+
return commands
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function lockFile() {
|
|
112
|
+
return tmpFile('aaden-command-runner') + '.pid'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 同一时间只有一个执行进程;上一个还在跑就退出,没领到的命令下次 do-log 会再给 */
|
|
116
|
+
function acquireLock() {
|
|
117
|
+
const file = lockFile()
|
|
118
|
+
try {
|
|
119
|
+
const pid = parseInt(fs.readFileSync(file, 'utf8'), 10)
|
|
120
|
+
if (pid && pid !== process.pid) {
|
|
121
|
+
try {
|
|
122
|
+
process.kill(pid, 0)
|
|
123
|
+
return false
|
|
124
|
+
} catch (e) {
|
|
125
|
+
if (e.code === 'EPERM') return false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (e) { /* 没有锁文件 */ }
|
|
129
|
+
try {
|
|
130
|
+
fs.writeFileSync(file, String(process.pid))
|
|
131
|
+
return true
|
|
132
|
+
} catch (e) {
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function releaseLock() {
|
|
138
|
+
try {
|
|
139
|
+
if (parseInt(fs.readFileSync(lockFile(), 'utf8'), 10) === process.pid) fs.unlinkSync(lockFile())
|
|
140
|
+
} catch (e) { /* 已经没了 */ }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function stripAnsi(s) {
|
|
144
|
+
return s.replace(/\u001b\[[0-9;?]*[A-Za-z]/g, '').replace(/\r(?!\n)/g, '\n')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// aaden-cli 打印要执行的命令时会带上 sudo 密码;输出要存云端、ERP 里能看到,存之前抹掉
|
|
148
|
+
const SECRETS = ['Innerken322.']
|
|
149
|
+
|
|
150
|
+
function redact(s) {
|
|
151
|
+
return SECRETS.reduce((acc, secret) => acc.split(secret).join('***'), s)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function report(id, deviceId, output, exitCode) {
|
|
155
|
+
const body = {deviceId: String(deviceId), output: output}
|
|
156
|
+
if (exitCode !== undefined) body.exitCode = exitCode
|
|
157
|
+
// 最终结果多试几次;中途的回报丢了无所谓,下一次会整段覆盖
|
|
158
|
+
const attempts = exitCode === undefined ? 1 : 3
|
|
159
|
+
for (let i = 0; i < attempts; i++) {
|
|
160
|
+
try {
|
|
161
|
+
await postJson(CLOUD + '/device-command/' + id + '/report', body)
|
|
162
|
+
return true
|
|
163
|
+
} catch (e) {
|
|
164
|
+
console.log('回报命令 #' + id + ' 失败:', e.message)
|
|
165
|
+
if (e.status === 409 || e.status === 403 || e.status === 404) return false
|
|
166
|
+
await new Promise(r => setTimeout(r, 5000))
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function runOne(id, deviceId, shellCmd) {
|
|
173
|
+
return new Promise(resolve => {
|
|
174
|
+
let output = ''
|
|
175
|
+
let dirty = false
|
|
176
|
+
const append = chunk => {
|
|
177
|
+
output += redact(stripAnsi(chunk.toString('utf8')))
|
|
178
|
+
if (output.length > MAX_OUTPUT) output = output.slice(output.length - MAX_OUTPUT)
|
|
179
|
+
dirty = true
|
|
180
|
+
}
|
|
181
|
+
// 登录 shell:cron 下 PATH 只有 /usr/bin:/bin,aaden-cli 在 /usr/local/bin
|
|
182
|
+
const child = spawn('bash', ['-lc', 'export PATH="/usr/local/bin:$PATH"; ' + shellCmd], {
|
|
183
|
+
detached: true,
|
|
184
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
185
|
+
env: Object.assign({}, process.env, {AADEN_UPGRADE_TRIGGER: '命令 #' + id}),
|
|
186
|
+
})
|
|
187
|
+
child.stdout.on('data', append)
|
|
188
|
+
child.stderr.on('data', append)
|
|
189
|
+
const timer = setInterval(() => {
|
|
190
|
+
if (!dirty) return
|
|
191
|
+
dirty = false
|
|
192
|
+
report(id, deviceId, output)
|
|
193
|
+
}, REPORT_INTERVAL_MS)
|
|
194
|
+
const killer = setTimeout(() => {
|
|
195
|
+
append('\n[aaden-cli] 超过 ' + (COMMAND_TIMEOUT_MS / 60000) + ' 分钟,终止\n')
|
|
196
|
+
try {
|
|
197
|
+
process.kill(-child.pid, 'SIGTERM')
|
|
198
|
+
} catch (e) { /* 已经退出 */ }
|
|
199
|
+
}, COMMAND_TIMEOUT_MS)
|
|
200
|
+
child.on('error', e => append('\n[aaden-cli] 启动失败: ' + e.message + '\n'))
|
|
201
|
+
child.on('close', (code, signal) => {
|
|
202
|
+
clearInterval(timer)
|
|
203
|
+
clearTimeout(killer)
|
|
204
|
+
const exitCode = typeof code === 'number' ? code : 128
|
|
205
|
+
if (signal) append('\n[aaden-cli] 被信号 ' + signal + ' 终止\n')
|
|
206
|
+
resolve({exitCode: exitCode, output: output})
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** `aaden-cli run-commands <deviceId> <id:COMMAND>...`:由 do-log 在后台起,逐条认领、执行、回报 */
|
|
212
|
+
async function runCommands(rootDir, version, deviceId, items) {
|
|
213
|
+
if (!acquireLock()) {
|
|
214
|
+
console.log('已有命令执行进程在跑,这次不领')
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
for (const item of items) {
|
|
219
|
+
const parts = String(item).split(':')
|
|
220
|
+
const id = parseInt(parts[0], 10)
|
|
221
|
+
const name = parts[1]
|
|
222
|
+
if (!id || !name) continue
|
|
223
|
+
try {
|
|
224
|
+
await postJson(CLOUD + '/device-command/' + id + '/start', {deviceId: String(deviceId), cliVersion: version})
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.log('认领命令 #' + id + ' 失败(可能已被领走或取消):', e.message)
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
const build = COMMANDS[name]
|
|
230
|
+
if (!build) {
|
|
231
|
+
await report(id, deviceId, '这个版本的 aaden-cli (' + version + ') 不认识命令 ' + name, 2)
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
const cmd = build(rootDir)
|
|
235
|
+
console.log('执行命令 #' + id + ' ' + name + ': ' + cmd)
|
|
236
|
+
const result = await runOne(id, deviceId, cmd)
|
|
237
|
+
await report(id, deviceId, '$ ' + cmd + '\n' + result.output, result.exitCode)
|
|
238
|
+
}
|
|
239
|
+
} finally {
|
|
240
|
+
releaseLock()
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ------------------------------------------------------------------ ② 自动补跑
|
|
245
|
+
|
|
246
|
+
function markAutoUpgradeStarted() {
|
|
247
|
+
try {
|
|
248
|
+
fs.writeFileSync(AUTO_UPGRADE_STAMP, String(Date.now()))
|
|
249
|
+
fs.chmodSync(AUTO_UPGRADE_STAMP, 0o666)
|
|
250
|
+
} catch (e) { /* 写不了就靠补跑自己的时间戳兜底 */ }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function readStamp(file) {
|
|
254
|
+
try {
|
|
255
|
+
const n = parseInt(fs.readFileSync(file, 'utf8'), 10)
|
|
256
|
+
if (n > 0) return n
|
|
257
|
+
return fs.statSync(file).mtimeMs
|
|
258
|
+
} catch (e) {
|
|
259
|
+
return 0
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function catchUpStampFile() {
|
|
264
|
+
return path.join(os.homedir(), '.aaden-catchup')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function autoUpgradeRunning() {
|
|
268
|
+
try {
|
|
269
|
+
// 不经 shell 直接调 pgrep:经 sh -c 的话 sh 自己的命令行里就有这串字,会把自己当成「正在跑」
|
|
270
|
+
const pids = execFileSync('pgrep', ['-f', 'aaden-cli auto-upgrade'], {encoding: 'utf8', timeout: 5000})
|
|
271
|
+
.split('\n').map(s => parseInt(s, 10)).filter(pid => pid && pid !== process.pid)
|
|
272
|
+
return pids.length > 0
|
|
273
|
+
} catch (e) {
|
|
274
|
+
return false
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* do-log 每次上报后调用:超过 20 小时没跑过 auto-upgrade 就在后台补跑一次。
|
|
280
|
+
* 返回没补跑的原因(或 'started'),只用于日志。
|
|
281
|
+
*/
|
|
282
|
+
function maybeCatchUpAutoUpgrade() {
|
|
283
|
+
const now = new Date()
|
|
284
|
+
const minutes = now.getHours() * 60 + now.getMinutes()
|
|
285
|
+
if (PEAK_WINDOWS.some(w => minutes >= w[0] && minutes < w[1])) return 'peak'
|
|
286
|
+
if (os.uptime() < MIN_UPTIME_S) return 'boot'
|
|
287
|
+
const last = Math.max(readStamp(AUTO_UPGRADE_STAMP), readStamp(catchUpStampFile()))
|
|
288
|
+
if (Date.now() - last < CATCH_UP_AFTER_MS) return 'recent'
|
|
289
|
+
if (autoUpgradeRunning()) return 'running'
|
|
290
|
+
// 先把补跑时间戳写上:写不上就不补跑,免得每 5 分钟起一次
|
|
291
|
+
try {
|
|
292
|
+
fs.writeFileSync(catchUpStampFile(), String(Date.now()))
|
|
293
|
+
} catch (e) {
|
|
294
|
+
return 'no-stamp'
|
|
295
|
+
}
|
|
296
|
+
const hours = last ? Math.round((Date.now() - last) / 3600000) + ' 小时' : '很久'
|
|
297
|
+
spawnDetached(['auto-upgrade'], 'aaden-catchup', {AADEN_UPGRADE_TRIGGER: '补跑:' + hours + '没跑过自动更新'})
|
|
298
|
+
return 'started'
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = {handleOffers, runCommands, markAutoUpgradeStarted, maybeCatchUpAutoUpgrade}
|
package/modules/firebase.js
CHANGED
|
@@ -16,7 +16,7 @@ export async function deviceEcho(data) {
|
|
|
16
16
|
})
|
|
17
17
|
const {frontendVersion, cliVersion, diskUsage, upTime, ngrokIsNew, hostInfo, tseInfo} = Object.assign({}, defaultData, data)
|
|
18
18
|
if (parseInt(myDeviceId) === -1) {
|
|
19
|
-
return
|
|
19
|
+
return null
|
|
20
20
|
}
|
|
21
21
|
const {name, adress1, postCode, city, state} = await getNiceRestaurantInfo()
|
|
22
22
|
const waitSecond = (parseInt(myDeviceId) % 30) * 1000
|
|
@@ -25,7 +25,7 @@ export async function deviceEcho(data) {
|
|
|
25
25
|
resolve()
|
|
26
26
|
}, waitSecond))
|
|
27
27
|
try {
|
|
28
|
-
await hillo.jsonPost('https://cloud-v2.aaden.io/deviceLog/create', {
|
|
28
|
+
const reply = await hillo.jsonPost('https://cloud-v2.aaden.io/deviceLog/create', {
|
|
29
29
|
deviceId: myDeviceId,
|
|
30
30
|
frontendVersion: frontendVersion,
|
|
31
31
|
cliVersion,
|
|
@@ -37,13 +37,17 @@ export async function deviceEcho(data) {
|
|
|
37
37
|
ngrokOk: ngrokIsNew,
|
|
38
38
|
deviceName: `${name}\n${adress1}\n${postCode} ${city} ${state}`,
|
|
39
39
|
...hostInfo,
|
|
40
|
-
...tseInfo
|
|
40
|
+
...tseInfo,
|
|
41
|
+
// 回复里要带待执行命令(见 modules/device-commands.js);老云端忽略这个字段
|
|
42
|
+
acceptCommands: true
|
|
41
43
|
})
|
|
42
44
|
console.log('report to remote server complete')
|
|
45
|
+
return {deviceId: myDeviceId, reply}
|
|
43
46
|
} catch (e) {
|
|
44
47
|
console.log(e.message)
|
|
45
48
|
console.log('report to remote server failed')
|
|
46
49
|
}
|
|
50
|
+
return null
|
|
47
51
|
|
|
48
52
|
|
|
49
53
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 一次性修复:aaden 的 crontab 里 do-log(每 5 分钟上报 + 领云端命令 + 自动补跑)那一行
|
|
3
|
+
# 缺了、或者指向的 aaden-cli 不存在时,改成标准写法 /usr/local/bin/aaden-cli do-log。
|
|
4
|
+
# 起因:10-07 1541 在 npm 并发重装之后 CLI 已经修好、远程更新也都成功,do-log 却再没上报过;
|
|
5
|
+
# 老设备上这一行有写裸 aaden-cli(cron 的 PATH 只有 /usr/bin:/bin)或 /usr/bin/aaden-cli 的。
|
|
6
|
+
# 只动这一行,其他行原样保留;改了的话把原来那行报给云端,方便事后看是哪种情况。
|
|
7
|
+
sudoS() { echo "Innerken322." | sudo -S -p '' "$@"; }
|
|
8
|
+
CLI=/usr/local/bin/aaden-cli
|
|
9
|
+
WANT="*/5 * * * * $CLI do-log"
|
|
10
|
+
|
|
11
|
+
[ -x "$CLI" ] || { echo "没有 $CLI,跳过"; exit 0; }
|
|
12
|
+
# 有的老设备 do-log 写在 root 的 crontab 或 /etc/cron.d 里:那样就别再往 aaden 这边加,不然每 5 分钟报两次
|
|
13
|
+
if sudoS crontab -l -u root 2>/dev/null | grep -v '^[[:space:]]*#' | grep -q 'do-log' \
|
|
14
|
+
|| grep -rqs --exclude='*.dpkg-*' 'aaden-cli do-log' /etc/cron.d 2>/dev/null; then
|
|
15
|
+
echo "do-log 在 root 的 crontab 或 /etc/cron.d 里,不动 aaden 的"; exit 0
|
|
16
|
+
fi
|
|
17
|
+
CUR=$(sudoS crontab -l -u aaden 2>/dev/null) || { echo "aaden 没有 crontab,跳过(整套定时任务缺失要 aaden-cli fix-cronjob)"; exit 0; }
|
|
18
|
+
|
|
19
|
+
LINE=$(printf '%s\n' "$CUR" | grep -v '^[[:space:]]*#' | grep -m1 'do-log')
|
|
20
|
+
if [ -n "$LINE" ]; then
|
|
21
|
+
# 第 6 个字段起是命令;@reboot 之类的特殊写法只占 1 个字段
|
|
22
|
+
case "$LINE" in
|
|
23
|
+
@*) CMD=$(printf '%s' "$LINE" | awk '{print $2}') ;;
|
|
24
|
+
*) CMD=$(printf '%s' "$LINE" | awk '{print $6}') ;;
|
|
25
|
+
esac
|
|
26
|
+
case "$CMD" in
|
|
27
|
+
"$CLI") echo "do-log 已是标准写法,不动"; exit 0 ;;
|
|
28
|
+
/*) [ -x "$CMD" ] && { echo "do-log 指向 $CMD,存在,不动"; exit 0; } ;;
|
|
29
|
+
*) [ -x "/usr/bin/$CMD" ] || [ -x "/bin/$CMD" ] && { echo "do-log 用 $CMD,cron 的 PATH 里找得到,不动"; exit 0; } ;;
|
|
30
|
+
esac
|
|
31
|
+
NEW=$(printf '%s\n' "$CUR" | awk -v line="$LINE" -v want="$WANT" '$0 == line { print want; next } { print }')
|
|
32
|
+
WHAT="do-log 那一行指向的命令不存在,已改成标准写法。原来是:$LINE"
|
|
33
|
+
else
|
|
34
|
+
NEW=$(printf '%s\n%s\n' "$CUR" "$WANT")
|
|
35
|
+
WHAT="aaden 的 crontab 里没有 do-log,已补上"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
printf '%s\n' "$NEW" | sudoS crontab -u aaden - || { echo "写 crontab 失败"; exit 1; }
|
|
39
|
+
echo "$WHAT"
|
|
40
|
+
DEVICE_ID=$(grep -e DeviceId /var/www/html/Resource/config/config.ini 2>/dev/null | cut -d = -f 2 | tr -d '" \r')
|
|
41
|
+
[ -n "$DEVICE_ID" ] && python3 -c 'import json, sys, urllib.request; urllib.request.urlopen(urllib.request.Request("https://cloud-v2.aaden.io/deviceLog/createEvent", json.dumps({"deviceId": sys.argv[1], "content": sys.argv[2] + " result:0", "type": "系统生成", "operator": "patch ensureDoLogCron"}).encode(), {"Content-Type": "application/json"}), timeout=10)' "$DEVICE_ID" "$WHAT" 2>/dev/null
|
|
42
|
+
exit 0
|