cron-up 26.10.30 → 26.10.31
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/README.md +111 -176
- package/package.json +1 -1
- package/src/args.mjs +5 -14
- package/src/cli.mjs +12 -29
- package/src/config.mjs +90 -66
- package/src/constants.mjs +24 -44
- package/src/list.mjs +49 -22
- package/src/logs.mjs +36 -24
- package/src/overview.mjs +108 -109
- package/src/patrol.mjs +445 -429
- package/src/registry.mjs +78 -80
- package/src/service.mjs +89 -166
- package/src/sessions.mjs +34 -17
- package/src/tasks.mjs +73 -51
package/src/overview.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 无子命令时的总览页:一眼看清服务、配置、任务、预热会话与需要留意的
|
|
2
2
|
// 事。它回答「现在一切正常吗」:除汇总现状外,还把巡检视角的异常集中成
|
|
3
3
|
// 一个「需要留意」清单。
|
|
4
4
|
|
|
@@ -6,6 +6,10 @@ import fs from 'node:fs';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
|
|
8
8
|
import { deps, packageVersion } from './internals.mjs';
|
|
9
|
+
import {
|
|
10
|
+
INTERVAL_SECONDS,
|
|
11
|
+
LEAD_SECONDS,
|
|
12
|
+
} from './constants.mjs';
|
|
9
13
|
import {
|
|
10
14
|
loadConfig,
|
|
11
15
|
loadState,
|
|
@@ -15,7 +19,7 @@ import {
|
|
|
15
19
|
isCooling,
|
|
16
20
|
cooldownEtaMinutes,
|
|
17
21
|
} from './config.mjs';
|
|
18
|
-
import { taskFileExists } from './tasks.mjs';
|
|
22
|
+
import { taskFileExists, taskKey } from './tasks.mjs';
|
|
19
23
|
import { serviceLine, launcherHealth } from './service.mjs';
|
|
20
24
|
import {
|
|
21
25
|
humanDelta,
|
|
@@ -30,21 +34,28 @@ import {
|
|
|
30
34
|
sameMinute,
|
|
31
35
|
} from './display.mjs';
|
|
32
36
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
function
|
|
37
|
+
// entry 此刻是否算「正在承接任务」:script 活(启动中或已认亲),或用户
|
|
38
|
+
// --resume 接管后 sid 仍活。冷却/死条目不报「没有会话」。
|
|
39
|
+
function entryHolds(ent, details, ws) {
|
|
40
|
+
if (!ent) return false;
|
|
41
|
+
if (deps.trackedAlive(ent)) return true;
|
|
42
|
+
return typeof ent.sessionId === 'string'
|
|
43
|
+
&& (details.get(ws) ?? []).some((d) => d.sessionId === ent.sessionId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 总览页任务盘点。返回 [告警, 已在告警里说明冷却的 entry 键集合, 本轮发
|
|
47
|
+
// 现的工作区集合](第三个供会话部分识别「已移出巡检视野」的会话)。
|
|
48
|
+
function overviewTasks(cfg, cols, details, state) {
|
|
36
49
|
const alerts = [];
|
|
37
50
|
const coolingWarned = new Set();
|
|
38
51
|
const discovered = new Set();
|
|
39
52
|
const now = new Date();
|
|
40
|
-
const
|
|
41
|
-
const leadMs = cfg.leadSeconds * 1000;
|
|
53
|
+
const leadMs = LEAD_SECONDS * 1000;
|
|
42
54
|
|
|
43
55
|
let nWs = 0;
|
|
44
56
|
let nTasks = 0;
|
|
45
|
-
let unrenewed = 0;
|
|
46
57
|
let soonest = null;
|
|
47
|
-
for (const [ws] of deps.discover(cfg.roots
|
|
58
|
+
for (const [ws] of deps.discover(cfg.roots)) {
|
|
48
59
|
discovered.add(ws);
|
|
49
60
|
const tasks = deps.readTasks(ws);
|
|
50
61
|
if (tasks === null) {
|
|
@@ -58,13 +69,9 @@ function overviewTasks(cfg, cols, consumers, state) {
|
|
|
58
69
|
if (tasks.length === 0) continue;
|
|
59
70
|
nWs += 1;
|
|
60
71
|
nTasks += tasks.length;
|
|
72
|
+
const liveSids = deps.liveSessionIds(details, ws);
|
|
61
73
|
let bad = 0;
|
|
62
|
-
let wanted = false;
|
|
63
74
|
for (const t of tasks) {
|
|
64
|
-
// 与 renew 同一条字段规则:显式周期任务却没有 truthy permanent,仍
|
|
65
|
-
// 受 7 天过期约束。从原始任务计数,与 cron 合法性无关,坏 cron 也照
|
|
66
|
-
// 提醒。
|
|
67
|
-
if (t.recurring && !t.permanent) unrenewed += 1;
|
|
68
75
|
const v = taskView(t, now, leadMs);
|
|
69
76
|
if (!v.valid) {
|
|
70
77
|
bad += 1;
|
|
@@ -75,33 +82,34 @@ function overviewTasks(cfg, cols, consumers, state) {
|
|
|
75
82
|
if (fire && (!soonest || fire.getTime() < soonest[0].getTime())) {
|
|
76
83
|
soonest = [fire, ws, v.summary, v.nxt];
|
|
77
84
|
}
|
|
78
|
-
if (v.wanted)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
if (!v.wanted) continue;
|
|
86
|
+
// 窗口内任务:创建者会话还开着(任务铁定投进它)或我们的预热已在
|
|
87
|
+
// 承接,都不是异常;否则按 entry 的冷却/失败状态给一条可执行的告
|
|
88
|
+
// 警。
|
|
89
|
+
const key = taskKey(t);
|
|
90
|
+
const ent = state[ws]?.[key];
|
|
91
|
+
if (deps.creatorSessionAlive(t, liveSids) || entryHolds(ent, details, ws)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (isCooling(ent, Math.floor(now.getTime() / 1000))) {
|
|
95
|
+
const cur = Math.floor(now.getTime() / 1000);
|
|
89
96
|
const eta = cooldownEtaMinutes(ent, cur);
|
|
90
97
|
alerts.push(
|
|
91
|
-
`${ws}
|
|
98
|
+
`${ws} 任务 ${key} 即将执行(或错过待补执行),但预热连续失败 `
|
|
92
99
|
+ `${ent.fails ?? '?'} 次正处冷却中,约 ${eta} 分钟后才会重试;`
|
|
93
|
-
+ `请运行 cron-up logs ${path.basename(ws)} 排查`);
|
|
94
|
-
coolingWarned.add(ws);
|
|
100
|
+
+ `请运行 cron-up logs ${path.basename(ws)}--${key} 排查`);
|
|
101
|
+
coolingWarned.add(`${ws}\0${key}`);
|
|
95
102
|
} else {
|
|
96
|
-
|
|
97
|
-
// 「下轮会自动启动」更该活下来;满额后的去向由「冷却中」那条合并
|
|
98
|
-
// 告警解释,这里不重复。
|
|
99
|
-
const fails = ent.fails ?? 0;
|
|
103
|
+
const fails = ent?.fails ?? 0;
|
|
100
104
|
const streak = fails > 0 ? `,近期已失败 ${fails} 次` : '';
|
|
101
|
-
alerts.push(`${ws}
|
|
102
|
-
+ '
|
|
105
|
+
alerts.push(`${ws} 任务 ${key} 即将执行(或错过待补执行)${streak},`
|
|
106
|
+
+ '但当前没有承接会话,下轮巡检会自动启动预热');
|
|
103
107
|
}
|
|
104
108
|
}
|
|
109
|
+
if (bad > 0) {
|
|
110
|
+
alerts.push(`${ws} 有 ${bad} 个任务的 cron 无法解析,`
|
|
111
|
+
+ `巡检会跳过${bad > 1 ? '它们' : '它'}`);
|
|
112
|
+
}
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
deps.print(nTasks > 0
|
|
@@ -116,52 +124,64 @@ function overviewTasks(cfg, cols, consumers, state) {
|
|
|
116
124
|
+ `${setTag}) ${path.basename(ws)} · ${summary}`;
|
|
117
125
|
deps.print(clip(line, cols));
|
|
118
126
|
}
|
|
119
|
-
if (!cfg.autoRenew && unrenewed > 0) {
|
|
120
|
-
alerts.push(
|
|
121
|
-
`自动续期已关闭:${unrenewed} 个周期任务尚未标记 permanent,创建满 7 `
|
|
122
|
-
+ '天后会末次执行并被删除。运行 cron-up renew 立即续期,或在配置中'
|
|
123
|
-
+ '开启 autoRenew');
|
|
124
|
-
}
|
|
125
127
|
return [alerts, coolingWarned, discovered];
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
function overviewSessions(cols, state, suppressCooling, discovered) {
|
|
130
|
+
function overviewSessions(cols, state, details, suppressCooling, discovered) {
|
|
129
131
|
const alerts = [];
|
|
130
132
|
const cur = Math.floor(Date.now() / 1000);
|
|
131
133
|
const live = [];
|
|
134
|
+
const detached = [];
|
|
132
135
|
const cooling = [];
|
|
133
136
|
const dead = [];
|
|
134
|
-
for (const [ws,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
for (const [ws, bucket] of Object.entries(state)) {
|
|
138
|
+
for (const [taskId, ent] of Object.entries(bucket)) {
|
|
139
|
+
const sidAlive = typeof ent.sessionId === 'string'
|
|
140
|
+
&& (details.get(ws) ?? []).some((d) => d.sessionId === ent.sessionId);
|
|
141
|
+
if (isCooling(ent, cur)) {
|
|
142
|
+
cooling.push([ws, taskId, ent]);
|
|
143
|
+
} else if (deps.trackedAlive(ent)) {
|
|
144
|
+
live.push([ws, taskId, ent]);
|
|
145
|
+
} else if (sidAlive) {
|
|
146
|
+
detached.push([ws, taskId, ent]);
|
|
147
|
+
} else {
|
|
148
|
+
dead.push([ws, taskId]);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
138
151
|
}
|
|
139
152
|
let tail = '';
|
|
140
153
|
if (cooling.length) tail += `,冷却 ${cooling.length} 个`;
|
|
154
|
+
if (detached.length) tail += `,终端接管 ${detached.length} 个`;
|
|
141
155
|
if (dead.length) tail += `,已失效 ${dead.length} 个`;
|
|
156
|
+
const describe = ([ws, taskId, ent]) =>
|
|
157
|
+
`${path.basename(ws)}/${taskId}(pid ${ent.pid},已运行 `
|
|
158
|
+
+ `${sessionAgeZh(ent.startedAt ?? cur, cur)})`;
|
|
142
159
|
if (live.length) {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
deps.print(`${head}${clip(desc, cols - fixed)})${tail}`);
|
|
160
|
+
const head = '预热会话:';
|
|
161
|
+
const avail = cols - dispWidth(head) - dispWidth(tail)
|
|
162
|
+
- (tail ? dispWidth('()') : 0);
|
|
163
|
+
const desc = clip(live.map(describe).join('、'), Math.max(0, avail));
|
|
164
|
+
deps.print(`${head}${live.length} 个:${desc}${tail ? `(${tail.slice(1)})` : ''}`);
|
|
149
165
|
} else {
|
|
150
|
-
deps.print(
|
|
166
|
+
deps.print(`预热会话:无${tail ? `(${tail.slice(1)})` : ''}`);
|
|
167
|
+
}
|
|
168
|
+
for (const [ws, taskId] of detached) {
|
|
169
|
+
deps.print(` · ${path.basename(ws)}/${taskId}:已在终端 --resume 接管,`
|
|
170
|
+
+ '巡检不再另拉;该会话关闭后任务恢复自动预热');
|
|
151
171
|
}
|
|
152
|
-
for (const [ws, ent] of cooling) {
|
|
153
|
-
if (suppressCooling.has(ws)) continue;
|
|
172
|
+
for (const [ws, taskId, ent] of cooling) {
|
|
173
|
+
if (suppressCooling.has(`${ws}\0${taskId}`)) continue;
|
|
154
174
|
const eta = cooldownEtaMinutes(ent, cur);
|
|
155
|
-
alerts.push(`${ws}
|
|
156
|
-
+
|
|
175
|
+
alerts.push(`${ws} 任务 ${taskId} 预热连续失败 ${ent.fails ?? '?'} 次,`
|
|
176
|
+
+ `冷却中,约 ${eta} 分钟后重试`);
|
|
157
177
|
}
|
|
158
178
|
if (discovered !== null) {
|
|
159
|
-
for (const [ws, ent] of live) {
|
|
179
|
+
for (const [ws, taskId, ent] of live) {
|
|
160
180
|
if (!discovered.has(ws)) {
|
|
161
181
|
alerts.push(
|
|
162
|
-
`${ws}
|
|
163
|
-
+ '
|
|
164
|
-
+ '
|
|
182
|
+
`${ws} 任务 ${taskId} 的预热会话(pid ${ent.pid})仍在运行,但该`
|
|
183
|
+
+ '目录已不在巡检范围内(任务文件被删或已移出 roots),cron-up 不'
|
|
184
|
+
+ '会再回收它;不需要时请自行结束');
|
|
165
185
|
}
|
|
166
186
|
}
|
|
167
187
|
}
|
|
@@ -169,11 +189,10 @@ function overviewSessions(cols, state, suppressCooling, discovered) {
|
|
|
169
189
|
}
|
|
170
190
|
|
|
171
191
|
const COMMANDS = [
|
|
172
|
-
['list', '
|
|
192
|
+
['list', '逐个列出任务的内容摘要、设定/预计触发时间与预热状态'],
|
|
173
193
|
['logs [目录片段] [-f]', '查看巡检日志或后台会话记录'],
|
|
174
194
|
['run', '立即手动巡检一轮'],
|
|
175
|
-
['
|
|
176
|
-
['install --force', '更新配置并重新安装(全部参数见 --help)'],
|
|
195
|
+
['install --roots ~/a,~/b', '更新扫描目录并重新安装(不带参数保留现有配置)'],
|
|
177
196
|
];
|
|
178
197
|
|
|
179
198
|
export async function cmdOverview(args) {
|
|
@@ -181,7 +200,7 @@ export async function cmdOverview(args) {
|
|
|
181
200
|
const v = packageVersion();
|
|
182
201
|
deps.print(v === null
|
|
183
202
|
? 'cron-up —— 为定时任务提前备好交互会话'
|
|
184
|
-
: `cron-up ${v} ——
|
|
203
|
+
: `cron-up ${v} —— 为定时任务提前备好交互会话(一任务一会话)`);
|
|
185
204
|
|
|
186
205
|
const cfgPath = args?.config ?? deps.paths.configPath;
|
|
187
206
|
let cfg = null;
|
|
@@ -196,76 +215,56 @@ export async function cmdOverview(args) {
|
|
|
196
215
|
deps.print(serviceLine(info, fs.existsSync(deps.paths.plistPath)));
|
|
197
216
|
|
|
198
217
|
const alerts = [];
|
|
199
|
-
// plist 写死的 node/入口路径失效(Node 版本目录被清)时 launchd 每轮
|
|
200
|
-
// 都静默拉不起来,这是总览唯一能发现它的地方。
|
|
201
218
|
const health = launcherHealth();
|
|
202
219
|
if (health) alerts.push(health);
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
const [consumers, registryAlert] =
|
|
206
|
-
await deps.scanSessions({ lineage: cfg?.stampOwner === true });
|
|
220
|
+
// 祖先链恒拍:总览要同时给出逐任务的活创建者/预热承接判定。
|
|
221
|
+
const [, registryAlert, details] = await deps.scanSessions();
|
|
207
222
|
if (registryAlert) alerts.push(registryAlert);
|
|
208
223
|
const state = loadState();
|
|
209
224
|
const curOv = Math.floor(Date.now() / 1000);
|
|
210
225
|
const heartbeat = loadHeartbeat();
|
|
211
|
-
let coolingWarned = new Set();
|
|
212
|
-
let discovered = null;
|
|
213
226
|
|
|
214
227
|
if (cfgError !== null) {
|
|
215
|
-
//
|
|
216
|
-
//
|
|
228
|
+
// 页面不能死在半路:损坏配置是「需要留意」的一种;巡检本身仍会以退
|
|
229
|
+
// 出码 2 失败。
|
|
217
230
|
deps.print(`配置:无法生效(${cfgPath})`);
|
|
218
231
|
alerts.push(`配置无法生效(巡检将以退出码 2 失败):${cfgError.message}`);
|
|
219
232
|
} else if (cfg === null) {
|
|
220
233
|
deps.print('配置:缺失,运行 cron-up install 生成');
|
|
221
234
|
} else {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (actualIv == null) {
|
|
229
|
-
deps.print(`巡检:配置为每 ${cfg.intervalSeconds} 秒一轮(服务未加载),`
|
|
230
|
-
+ `提前 ${cfg.leadSeconds} 秒启动会话${renewTag}${minIdTag}${retainTag}${stampTag}`);
|
|
231
|
-
} else {
|
|
232
|
-
deps.print(`巡检:每 ${actualIv} 秒一轮,提前 ${cfg.leadSeconds} 秒启动`
|
|
233
|
-
+ `会话${renewTag}${minIdTag}${retainTag}${stampTag}`);
|
|
234
|
-
if (actualIv !== cfg.intervalSeconds) {
|
|
235
|
-
alerts.push(`launchd 实际间隔 ${actualIv} 秒与配置 ${cfg.intervalSeconds} `
|
|
236
|
-
+ '秒不一致,重新运行 cron-up install 后生效');
|
|
237
|
-
}
|
|
235
|
+
deps.print(`巡检:每 ${INTERVAL_SECONDS} 秒一轮,提前 ${LEAD_SECONDS} `
|
|
236
|
+
+ '秒为每个窗口内任务启动专属预热会话');
|
|
237
|
+
if (info && info.interval !== null && info.interval !== undefined
|
|
238
|
+
&& info.interval !== INTERVAL_SECONDS) {
|
|
239
|
+
alerts.push(`launchd 实际间隔 ${info.interval} 秒与当前版本的 `
|
|
240
|
+
+ `${INTERVAL_SECONDS} 秒不一致,重新运行 cron-up install 后生效`);
|
|
238
241
|
}
|
|
239
|
-
const suffix = `(深度 ${cfg.maxDepth})`;
|
|
240
242
|
const rootsText = cfg.roots.join('、');
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
+ suffix);
|
|
245
|
-
// 心跳回答「巡检最近真的在跑吗」:launchd 已加载但启动脚本运行期失
|
|
246
|
-
// 效时,静态健康检查(launcherHealth)全绿而轮次早已停摆,只有沉默的
|
|
247
|
-
// 心跳能戳穿。服务未加载(info 为 null)时不告警,服务行已说明未安装。
|
|
243
|
+
deps.print(`扫描:${clip(rootsText, cols - dispWidth('扫描:'))}`);
|
|
244
|
+
// 心跳回答「巡检最近真的在跑吗」:服务已加载但启动链运行期失效时,
|
|
245
|
+
// 只有沉默的心跳能戳穿。
|
|
248
246
|
if (heartbeat) {
|
|
249
247
|
deps.print(`上次巡检:${elapsedZh(curOv - heartbeat.ranAt)}前`);
|
|
250
|
-
if (info && heartbeatStale(heartbeat,
|
|
248
|
+
if (info && heartbeatStale(heartbeat, INTERVAL_SECONDS, curOv)) {
|
|
251
249
|
alerts.push(`巡检已沉默 ${elapsedZh(curOv - heartbeat.ranAt)}:launchd `
|
|
252
|
-
+
|
|
253
|
-
+ '
|
|
254
|
-
+ '日志,必要时重跑 cron-up install');
|
|
250
|
+
+ `显示服务已加载,但最近没有成功跑完的轮次,启动链可能已失效;`
|
|
251
|
+
+ '请运行 cron-up logs 查看巡检日志,必要时重跑 cron-up install');
|
|
255
252
|
}
|
|
256
253
|
}
|
|
257
254
|
alerts.push(...validateConfig(cfg));
|
|
258
255
|
deps.print('');
|
|
259
|
-
const [taskAlerts,
|
|
260
|
-
cfg, cols,
|
|
256
|
+
const [taskAlerts, coolingWarned, found] = overviewTasks(
|
|
257
|
+
cfg, cols, details, state);
|
|
261
258
|
alerts.push(...taskAlerts);
|
|
262
|
-
|
|
263
|
-
|
|
259
|
+
// 会话区需要上面产出的冷却抑制集合。
|
|
260
|
+
alerts.push(...overviewSessions(cols, state, details, coolingWarned, found));
|
|
264
261
|
}
|
|
265
262
|
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
|
|
263
|
+
// 未安装/配置损坏时也可能有残留预热会话,照常展示(没有 discovered
|
|
264
|
+
// 上下文,不做「移出 roots」告警)。
|
|
265
|
+
if (cfg === null || cfgError !== null) {
|
|
266
|
+
alerts.push(...overviewSessions(cols, state, details, new Set(), null));
|
|
267
|
+
}
|
|
269
268
|
|
|
270
269
|
if (alerts.length) {
|
|
271
270
|
deps.print('需要留意:');
|