multi-tasks 3.1.9 → 3.2.0

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 CHANGED
@@ -10,30 +10,7 @@ Multi-tasks is a toolkit to manage long-term and large-scale parallel computing
10
10
  npm install multi-tasks
11
11
  ```
12
12
 
13
- ## 🎉 What's New — Real-Time Monitoring Has Arrived! 🎉
14
-
15
- **1. Install** the monitor:
16
-
17
- ```bash
18
- npm install multi-tasks-monitor
19
- ```
20
-
21
- **2. Start your multi-tasks run first** — the monitor reads the task folder, which only exists once your run has started. Then either:
22
-
23
- - Easiest (multi-tasks 3.1.9+): your startup log prints the complete command — full task folder path included. Copy it into another terminal and run:
24
-
25
- ```text
26
- ****************************************************************
27
- [Master]: monitor this task with:
28
- npx multi-tasks-monitor --task-dir "D:\tasks\multitasks20260806..." --port 3777
29
- ****************************************************************
30
- ```
31
-
32
- - Or just run `npx multi-tasks-monitor` and pick the task folder on the page with "Select Directory".
33
-
34
- **3. Watch the monitor page.** The live dashboard opens in your browser automatically — and you can revisit http://localhost:3777 anytime. 🚀
35
-
36
- Running multi-tasks older than 3.1.9, or want the full command-line options? See the [multi-tasks-monitor](https://www.npmjs.com/package/multi-tasks-monitor) page.
13
+ Tip: monitor running tasks in real time see [Real-Time Monitoring](#real-time-monitoring).
37
14
 
38
15
  ### API:
39
16
 
@@ -55,6 +32,7 @@ The API has three parts: the entry functions, the config options, and the `helpe
55
32
  - **`numberOfWorkers`** — how many worker processes run in parallel; a number, or a percentage string of CPU cores like `"50%"` (default).
56
33
  - **`taskTimeout`** — optional, milliseconds; an overdue task fails with a timeout error.
57
34
  - **`maxTaskRetries`** — optional, max times a failed task is auto-retried.
35
+ - **`progressBar`** — `true` by default: shows a live single-line progress bar in the terminal (set `false` to disable); logs (including your own `console.*` calls) always go to `<taskFolder>/.sys/run.log`, and print to the console as well only when the bar is off or stdout is not a TTY.
58
36
  - **`autoCloseAfterCompletion`** — set `true` to kill workers and exit the process when no tasks are left; default is `false`, i.e. workers stay alive waiting for dynamically created tasks.
59
37
  - **`shouldTerminate(info)`** — return `true` to terminate the whole process.
60
38
  - **`setSysListener(event, payload, meta)`** — optional, handle custom system events (sent via `helper.emitSys`) on the master; `meta` has `fromWorkerId` and `fromWorkerPid`.
@@ -295,8 +273,24 @@ multiTasks.restart({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //deletes
295
273
 
296
274
  Unlike `retry_fails` (which re-runs only the failed tasks), `restart` re-runs **all the initial tasks** — tasks created dynamically via `helper.createNewTasks` are not restored and will not be re-run. Fields you pass (e.g. `processTask`) override the snapshot and are written back into it; the same self-contained-function note as in Resuming applies. (**Breaking change:** the name `restart` was the old name of `retry_fails` in v2 and in the published v3.0–v3.1.x releases; this is a different API — calling it no longer retries failed tasks, it wipes the folder and re-runs everything.)
297
275
 
276
+ ### Real-Time Monitoring
277
+
278
+ Monitor your running tasks with [multi-tasks-monitor](https://www.npmjs.com/package/multi-tasks-monitor) — see its npm page for full details.
279
+
280
+ Quick start (requires multi-tasks >=3.2.0): once your multi-tasks run has started, the startup log prints the monitor command with the task folder path already filled in — just copy-paste it into another terminal:
281
+
282
+ ```text
283
+ ****************************************************************
284
+ [Master]: monitor this task with:
285
+ npx multi-tasks-monitor --task-dir "D:\tasks\multitasks20260806..." --port 3777
286
+ ****************************************************************
287
+ ```
288
+
289
+ For installation, command-line options, and more, see the [multi-tasks-monitor](https://www.npmjs.com/package/multi-tasks-monitor) page.
290
+
298
291
  ### Changelog:
299
292
 
293
+ - 3.2.0 Add progressBar (on by default); console output diverted to run.log; new startup banner
300
294
  - 3.1.9 Print the multi-tasks-monitor command on startup
301
295
  - 3.1.8 Small refinements
302
296
  - 3.1.7 onFinish: iterate all succ/failed results via helper.foreachSuccResult/foreachErrorResult
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multi-tasks",
3
- "version": "3.1.9",
3
+ "version": "3.2.0",
4
4
  "description": "Multi-process task scheduling based on Node.js cluster, with crash resume and failed-task restart support",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -0,0 +1,118 @@
1
+ //progressBar master 端控制器(零依赖): 持有渲染器实例与忙碌表, 封装 TTY 判定与节流渲染,
2
+ //Multithread 只调语义化方法(setup/workerStarted/taskAssigned/...), 不碰渲染细节。
3
+ //纯 master 端运行时状态, 不进快照不进 IPC; 统计数据(getStats/getRemaining)由调用方注入, 本模块不依赖 TaskMgr。
4
+ //console 劫持与 run.log 已与 progressBar 解耦(永久生效, 见 utils/runLog), 不再是本模块的职责。
5
+ //单行进度条(progressView): worker 增删不体现在渲染上, 这里只维护 busyWorkers 计数 running。
6
+ const progressView = require('./progressView');
7
+
8
+ let RENDER_INTERVAL = 100;//渲染节流: 高频小任务场景避免每次事件都 readdirSync(getFinalStats) 造成 O(N²) 盘扫
9
+
10
+ let view = null;//progressView 渲染器实例, 未开启为 null
11
+ let progressOn = false;//progressBar 且 stdout 为 TTY 时为真(非 TTY 回退普通日志)
12
+ let busyWorkers = {};//worker.id -> subid, master 端忙碌表
13
+ let getStats = null;//() => TaskMgr.getFinalStats(), setup 注入
14
+ let getRemaining = null;//() => new 目录任务数, setup 注入
15
+ let lastRenderAt = 0;
16
+ let renderTimer = null;
17
+
18
+ const reset = () => {//master start 入口/同进程再跑一轮时调用: 清掉上一轮全部残留(含未发的节流帧)
19
+ if (renderTimer) {
20
+ clearTimeout(renderTimer);
21
+ renderTimer = null;
22
+ };
23
+ view = null;
24
+ progressOn = false;
25
+ busyWorkers = {};
26
+ lastRenderAt = 0;
27
+ };
28
+
29
+ //开启判定 + 初始化: 返回是否真正开启(显式 progressBar:false 关闭, 缺省默认开启;
30
+ //非 TTY 一律回退普通日志, 仅显式 true 时打回退警告, 默认开启静默回退)
31
+ const setup = ({ config, stream, getStats: statsFn, getRemaining: remainingFn }) => {
32
+ reset();
33
+ if (config.progressBar === false) {
34
+ return false;
35
+ };
36
+ if (!stream.isTTY) {//ANSI 控制码写进重定向输出是垃圾: 回退为现有控制台日志
37
+ if (config.progressBar === true) {
38
+ console.warn('[Master]: progressBar needs a TTY stdout, fallback to console logs');
39
+ };
40
+ return false;
41
+ };
42
+ progressOn = true;
43
+ getStats = statsFn;
44
+ getRemaining = remainingFn;
45
+ view = progressView.create({ stream, barWidth: 30 });
46
+ return true;
47
+ };
48
+
49
+ const isOn = () => {
50
+ return progressOn;
51
+ };
52
+
53
+ const doRender = () => {
54
+ //完成数按盘上结果目录统计: 重试/超时/崩溃各种路径天然正确, 无需在协议里额外计数
55
+ let stats = getStats();
56
+ let done = stats.succeeded + stats.failed;
57
+ let running = Object.keys(busyWorkers).length;
58
+ lastRenderAt = Date.now();
59
+ view.render({ done, running, remaining: getRemaining() });
60
+ };
61
+
62
+ const render = () => {
63
+ if (!view) {
64
+ return;
65
+ };
66
+ if (renderTimer) {
67
+ return;//已挂着一个尾随渲染, 合并掉中间帧
68
+ };
69
+ let wait = RENDER_INTERVAL - (Date.now() - lastRenderAt);
70
+ if (wait <= 0) {
71
+ doRender();
72
+ return;
73
+ };
74
+ renderTimer = setTimeout(() => {
75
+ renderTimer = null;
76
+ if (view) {
77
+ doRender();
78
+ };
79
+ }, wait);
80
+ renderTimer.unref();//不阻碍 master 退出(最后一帧可能因进程退出丢失, 可接受)
81
+ };
82
+
83
+ const workerStarted = (id, pid) => {//单行进度条不展示 worker 行: 仅保留语义入口, 无渲染动作
84
+ };
85
+
86
+ const taskAssigned = (id, subid) => {
87
+ busyWorkers[id] = subid;
88
+ render();
89
+ };
90
+
91
+ const taskCompleted = (id) => {
92
+ if (busyWorkers[id]) {
93
+ delete busyWorkers[id];
94
+ };
95
+ };
96
+
97
+ const workerExited = (id) => {
98
+ delete busyWorkers[id];
99
+ render();
100
+ };
101
+
102
+ const finish = () => {//收尾: 换行定格进度条行(master 退出前调用); 未开启为 no-op
103
+ if (view) {
104
+ view.end();
105
+ };
106
+ };
107
+
108
+ module.exports = {
109
+ setup,
110
+ reset,
111
+ isOn,
112
+ render,
113
+ workerStarted,
114
+ taskAssigned,
115
+ taskCompleted,
116
+ workerExited,
117
+ finish,
118
+ };
@@ -0,0 +1,35 @@
1
+ //终端进度条渲染器(零依赖): 单行进度条, 每帧 \r 回行首 + 清行 + 重写, 不上移光标、不换行。
2
+ //纯渲染, 不碰 cluster/fs。单行形态对"两帧之间有外部写入共享终端"鲁棒:
3
+ //旧帧最多留一行滚动残留, 下一帧在当前行自愈(多行版靠 ESC[nA 数行, 外部写入挤偏光标会让旧帧永久滞留)。
4
+ const ESC = '';
5
+
6
+ const create = ({ stream, barWidth }) => {
7
+ if (!stream) { throw new Error('progressView needs a stream'); };
8
+ const width = barWidth || 30;
9
+
10
+ const buildBarLine = ({ done, running, remaining }) => {
11
+ let total = done + running + remaining;
12
+ let filled = total > 0 ? Math.floor(width * done / total) : width;
13
+ let bar = '█'.repeat(filled) + '░'.repeat(width - filled);
14
+ let pct = (total > 0 ? 100 * done / total : 100).toFixed(2).padStart(6);//两位小数, 定宽防行长抖动
15
+ return `[${bar}] ${pct}% ${done}/${total} done, ${running} running`;
16
+ };
17
+
18
+ const render = ({ done, running, remaining }) => {
19
+ //行比终端列宽还长会折行、\r 回不到帧首: 按列宽截断
20
+ let cols = stream.columns || 80;
21
+ let line = buildBarLine({ done, running, remaining });
22
+ if (line.length > cols) {
23
+ line = line.slice(0, cols);
24
+ };
25
+ stream.write(`\r${ESC}[2K${line}`);
26
+ };
27
+
28
+ const end = () => {//收尾换行: 进度条行定格, 后续输出(shell 提示符等)落在下一行
29
+ stream.write('\n');
30
+ };
31
+
32
+ return { render, end };
33
+ };
34
+
35
+ module.exports = { create };
@@ -0,0 +1,136 @@
1
+ //日志分流(永久性机制, 与 progressBar 无关): 库的运行日志与 hook 后的 console.* 一律经 O_APPEND
2
+ //写流(异步, 不阻塞事件循环) append 到任务目录的 run.log; echo 模式(progressBar 未开/非 TTY 回退)
3
+ //同时透传原 console 上屏, 非 echo 模式(progressBar 进度区接管终端)只写文件不上屏。
4
+ //master 与 worker 各进程各持一个写流 append 同一文件。
5
+ //best-effort: 写流出错(目录不存在/权限等)标记后整轮丢弃; 进程退出可能丢未 flush 的缓冲尾部。
6
+ const fs = require('fs');
7
+ const util = require('util');
8
+
9
+ let state = { enabled: false, filePath: null, stream: null, broken: false, hooked: false, echo: false };
10
+ let originalConsole = null;
11
+
12
+ const init = (filePath) => {
13
+ state.enabled = true;
14
+ state.filePath = filePath;
15
+ state.stream = null;//惰性创建: 首个 write 时才 open, 避免空跑也建文件
16
+ state.broken = false;
17
+ };
18
+
19
+ const reset = () => {//同进程再跑一轮 multiTasks 时复位, 否则日志会继续写上一轮的 run.log
20
+ unhookConsole();
21
+ if (state.stream) {
22
+ state.stream.destroy();
23
+ };
24
+ state.enabled = false;
25
+ state.filePath = null;
26
+ state.stream = null;
27
+ state.broken = false;
28
+ state.echo = false;
29
+ };
30
+
31
+ const isEnabled = () => {
32
+ return state.enabled;
33
+ };
34
+
35
+ //拦截 console: 把 console.log/warn/error 替换为写 run.log 的实现,
36
+ //用户代码(processTask/onFinish 等)里的 console 输出同样进文件。
37
+ //echo 为真时同时调原实现上屏(progressBar 未开/非 TTY 回退); 为假时只写文件(进度区接管终端)。
38
+ //reset/unhookConsole 恢复原实现(必须在 enabled 变 false 之前恢复, 否则 write 透传 console 会递归)。
39
+ const hookConsole = ({ echo } = {}) => {
40
+ if (!state.enabled || state.hooked) {
41
+ return;
42
+ };
43
+ state.hooked = true;
44
+ state.echo = !!echo;
45
+ originalConsole = { log: console.log, warn: console.warn, error: console.error };
46
+ console.log = write;
47
+ console.warn = warn;
48
+ console.error = error;
49
+ };
50
+
51
+ const unhookConsole = () => {
52
+ if (!state.hooked) {
53
+ return;
54
+ };
55
+ console.log = originalConsole.log;
56
+ console.warn = originalConsole.warn;
57
+ console.error = originalConsole.error;
58
+ originalConsole = null;
59
+ state.hooked = false;
60
+ state.echo = false;
61
+ };
62
+
63
+ const formatArg = (arg) => {
64
+ if (arg instanceof Error) {
65
+ return arg.stack || String(arg);
66
+ };
67
+ if (typeof arg === 'string') {
68
+ return arg;
69
+ };
70
+ //对象/数组走 util.inspect, 避免写进 run.log 变成 [object Object] 丢信息
71
+ return typeof arg === 'object' && arg !== null ? util.inspect(arg) : String(arg);
72
+ };
73
+
74
+ const appendToFile = (args) => {
75
+ if (state.broken) {
76
+ return;
77
+ };
78
+ try {
79
+ if (!state.stream) {
80
+ state.stream = fs.createWriteStream(state.filePath, { flags: 'a' });
81
+ state.stream.on('error', () => {//写盘失败不影响任务运行, 标记后整轮丢弃(避免循环重建)
82
+ state.broken = true;
83
+ state.stream.destroy();
84
+ state.stream = null;
85
+ });
86
+ };
87
+ state.stream.write(args.map(formatArg).join(' ') + '\n');
88
+ } catch (e) {
89
+ //best-effort: 日志写盘失败不影响任务运行
90
+ };
91
+ };
92
+
93
+ const write = (...args) => {
94
+ if (!state.enabled) {
95
+ console.log(...args);
96
+ return;
97
+ };
98
+ appendToFile(args);
99
+ if (state.echo && originalConsole) {
100
+ originalConsole.log(...args);
101
+ };
102
+ };
103
+
104
+ const warn = (...args) => {
105
+ if (!state.enabled) {
106
+ console.warn(...args);
107
+ return;
108
+ };
109
+ appendToFile(args);
110
+ if (state.echo && originalConsole) {
111
+ originalConsole.warn(...args);
112
+ };
113
+ };
114
+
115
+ const error = (...args) => {
116
+ if (!state.enabled) {
117
+ console.error(...args);
118
+ return;
119
+ };
120
+ appendToFile(args);
121
+ if (state.echo && originalConsole) {
122
+ originalConsole.error(...args);
123
+ };
124
+ };
125
+
126
+ //master/worker 端入口调用: 有分流路径即 init 并 hook console(永久性机制, 不再判 TTY/progressBar);
127
+ //echo 由调用方按 progressBar 是否接管终端决定。无路径时什么都不做(维持 console)。判断收敛在此, 调用点不写 if。
128
+ const initAndHook = (filePath, { echo } = {}) => {
129
+ if (!filePath) {
130
+ return;
131
+ };
132
+ init(filePath);
133
+ hookConsole({ echo });
134
+ };
135
+
136
+ module.exports = { init, initAndHook, reset, isEnabled, write, warn, error, hookConsole, unhookConsole };
@@ -0,0 +1,50 @@
1
+ //启动信息横幅渲染器(纯函数, 不碰 console/fs/cluster): 仅标题行 ASCII 框装饰,
2
+ //框宽只由标题行内容决定——长路径只出现在平铺字段行, 不会撑破框、不会折行。
3
+ //renderBanner: 终端上屏用(劫持前 console 直出); renderLogHeader: run.log 文件头部用(含 MODE 行);
4
+ //writeLogHeader: 判 sysLogFile 后写 runLog——判断收敛在方法内部, 调用点不写 if;
5
+ //无路径时(内部 API multiTasks.start 自动生成目录名)no-op, 不透传 console。
6
+ const runLog = require('./runLog');
7
+
8
+ const VERSION = require('../package.json').version;
9
+
10
+ const KEY_WIDTH = 17;//键列宽: 需容纳最长键 'monitor start cmd', 改动时同步调
11
+
12
+ const renderTitleBox = (mode) => {
13
+ const left = `multi-tasks v${VERSION}`;
14
+ const right = `MODE: ${mode}`;
15
+ const innerWidth = left.length + right.length + 4;//左右内容间至少 4 空格
16
+ const middle = `| ${left}${' '.repeat(innerWidth - left.length - right.length)}${right} |`;
17
+ const border = `+${'-'.repeat(innerWidth + 2)}+`;
18
+ return [border, middle, border];
19
+ };
20
+
21
+ const renderFieldLines = (info) => {
22
+ const rows = [
23
+ ['taskId', info.taskId],
24
+ ['taskFolder', info.taskFolder],
25
+ ['workers', `${info.numOfWorkers} (numCPUs=${info.numCPUs})`],
26
+ ['master PID', `${info.pid}`],
27
+ ['new tasks', `${info.newTasks}`],
28
+ ['run.log', info.sysLogFile || '(not available)'],
29
+ ['started at', info.startedAt],
30
+ ['monitor start cmd', `npx multi-tasks-monitor --task-dir "${info.taskFolder}" --port 3777`],
31
+ ];
32
+ return rows.map(([key, value]) => `${key.padEnd(KEY_WIDTH)} : ${value}`);
33
+ };
34
+
35
+ const renderBanner = (info) => {
36
+ return [...renderTitleBox(info.mode), ...renderFieldLines(info)].join('\n');
37
+ };
38
+
39
+ const renderLogHeader = (info) => {
40
+ return [`${'MODE'.padEnd(KEY_WIDTH)} : ${info.mode}`, ...renderFieldLines(info)].join('\n');
41
+ };
42
+
43
+ const writeLogHeader = (info) => {
44
+ if (!info.sysLogFile) {
45
+ return;
46
+ };
47
+ runLog.write(renderLogHeader(info));
48
+ };
49
+
50
+ module.exports = { renderBanner, renderLogHeader, writeLogHeader };
@@ -9,6 +9,9 @@ const TaskMgr = require('./TaskMgr');
9
9
  const WorkerMgr = require('./WorkerMgr');
10
10
  const helper = require('./helper');
11
11
  const { resolveNumberOfWorkers } = require('../utils/resolveWorkers');
12
+ const runLog = require('../utils/runLog');
13
+ const progressCtl = require('../utils/progressCtl');
14
+ const startupBanner = require('../utils/startupBanner');
12
15
 
13
16
  let fmtdigit = (n)=>{
14
17
  return n > 9 ? "" + n: "0" + n;
@@ -34,7 +37,7 @@ let killAllWorkers = ()=>{
34
37
  for (const id in cluster.workers) {
35
38
  if (cluster.workers.hasOwnProperty(id)) {
36
39
  const worker = cluster.workers[id];
37
- console.log(`Killing worker ${worker.process.pid}`);
40
+ runLog.write(`Killing worker ${worker.process.pid}`);
38
41
  killWorker(worker);
39
42
  }
40
43
  }
@@ -49,13 +52,12 @@ const start = (config)=>{
49
52
  let isMaster = (cluster.isPrimary || cluster.isMaster);
50
53
 
51
54
  if (isMaster) {//init
52
- console.log('[Master]: Start!!');
53
- let PID = process.pid;
55
+ //同进程再跑一轮 multiTasks 时复位 progressBar 控制器状态,
56
+ //否则残留会让新一轮(不带 progressBar)继续渲染旧进度区
57
+ progressCtl.reset();
58
+ runLog.reset();//runLog 已脱离 progressCtl: 不复位则新一轮日志继续写旧 run.log、console 仍被旧 hook 拦截
54
59
  config.numberOfWorkers = resolveNumberOfWorkers(config.numberOfWorkers);//支持百分比字符串; 须在 fork 循环读值前折算, 非法值在此抛错
55
60
  let numOfWorkers = config.numberOfWorkers;
56
- console.log(` numCPUs=${numCPUs}`);
57
- console.log(` numOfWorkers=${numOfWorkers}`);
58
- console.log(` PID=${PID}`);
59
61
 
60
62
  const AsyncQueue = require(`../utils/asyncQueue`);
61
63
  const queue = new AsyncQueue();
@@ -82,7 +84,7 @@ const start = (config)=>{
82
84
  if(typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
83
85
  let retryCount = TaskMgr.incrementTaskRetry(subid);
84
86
  if(retryCount <= config.maxTaskRetries){
85
- console.warn(`[Master]: task "${subid}" timeout, retry (${retryCount}/${config.maxTaskRetries})`);
87
+ runLog.warn(`[Master]: task "${subid}" timeout, retry (${retryCount}/${config.maxTaskRetries})`);
86
88
  TaskMgr.mvTask(subid, 'running', 'new', ()=>{});
87
89
  return;
88
90
  };
@@ -91,7 +93,7 @@ const start = (config)=>{
91
93
  TaskMgr.mvTask(subid, 'running', 'finished_with_errors', ()=>{});
92
94
  }
93
95
  }catch(e){
94
- console.warn('[Master]: failed to finalize timeout task', subid, e);
96
+ runLog.warn('[Master]: failed to finalize timeout task', subid, e);
95
97
  }
96
98
  };
97
99
  let onTaskTimeout = (worker)=>{
@@ -100,7 +102,7 @@ const start = (config)=>{
100
102
  delete workerTaskTimers[worker.id];
101
103
  let {subTask, startTimestamp} = rec;
102
104
  let cost = new Date()*1 - startTimestamp;
103
- console.warn(`[Master]: task "${subTask.subid}" timeout after ${cost}ms, killing worker pid=${worker.process.pid}`);
105
+ runLog.warn(`[Master]: task "${subTask.subid}" timeout after ${cost}ms, killing worker pid=${worker.process.pid}`);
104
106
  worker.kill();//不 disconnect: exit 事件会按崩溃补员
105
107
  finalizeTimeoutTask(subTask, cost);
106
108
  };
@@ -110,15 +112,15 @@ const start = (config)=>{
110
112
  try{
111
113
  let retryCount = TaskMgr.incrementTaskRetry(subid);
112
114
  if(retryCount <= config.maxTaskRetries){
113
- console.warn(`[Master]: worker crashed, retry task "${subid}" (${retryCount}/${config.maxTaskRetries})`);
115
+ runLog.warn(`[Master]: worker crashed, retry task "${subid}" (${retryCount}/${config.maxTaskRetries})`);
114
116
  TaskMgr.mvTask(subid, 'running', 'new', ()=>{});
115
117
  }else{
116
- console.warn(`[Master]: task "${subid}" crashed ${retryCount} times, give up`);
118
+ runLog.warn(`[Master]: task "${subid}" crashed ${retryCount} times, give up`);
117
119
  TaskMgr.saveTaskResult(subid, true, [{type: 'worker_crash', retryCount: config.maxTaskRetries}]);
118
120
  TaskMgr.mvTask(subid, 'running', 'finished_with_errors', ()=>{});
119
121
  };
120
122
  }catch(e){
121
- console.warn('[Master]: failed to handle crashed task', subid, e);
123
+ runLog.warn('[Master]: failed to handle crashed task', subid, e);
122
124
  }
123
125
  };
124
126
 
@@ -127,8 +129,9 @@ const start = (config)=>{
127
129
  worker.on('message', function(message) {
128
130
  if(message && message.__sysEvent){//worker 系统事件: master 自己消费, 不转发
129
131
  if(message.event === 'TERMINATE_ALL_WORKERS'){
130
- console.warn(`[Master]: TERMINATE_ALL_WORKERS from worker ${worker.id}, killing all workers`);
132
+ runLog.warn(`[Master]: TERMINATE_ALL_WORKERS from worker ${worker.id}, killing all workers`);
131
133
  killAllWorkers();
134
+ progressCtl.finish();//单行进度条换行定格, 别让 shell 提示符盖住它
132
135
  return process.exit(0);
133
136
  }
134
137
  let meta = {fromWorkerId: worker.id, fromWorkerPid: worker.process.pid};
@@ -136,10 +139,10 @@ const start = (config)=>{
136
139
  try{
137
140
  USER_CONFIG.setSysListener(message.event, message.payload, meta);
138
141
  }catch(e){
139
- console.error(`[Master]: setSysListener for event "${message.event}" threw:`, e);
142
+ runLog.error(`[Master]: setSysListener for event "${message.event}" threw:`, e);
140
143
  }
141
144
  }else{
142
- console.warn(`[Master]: unknown sys event "${message.event}" from worker ${worker.id} (no setSysListener configured)`);
145
+ runLog.warn(`[Master]: unknown sys event "${message.event}" from worker ${worker.id} (no setSysListener configured)`);
143
146
  }
144
147
  return;
145
148
  }
@@ -151,13 +154,14 @@ const start = (config)=>{
151
154
  try{
152
155
  w.send({__broadcast: true, event: message.event, payload: message.payload, meta});
153
156
  }catch(e){
154
- console.warn(`[Master]: failed to relay broadcast to worker ${w.id}`, e);
157
+ runLog.warn(`[Master]: failed to relay broadcast to worker ${w.id}`, e);
155
158
  }
156
159
  }
157
160
  return;
158
161
  }
159
162
  if(message === MSG_REQUEST_NEW_TASK){
160
163
  clearTaskTimer(worker.id);//上一个任务已结束(无论成败), 撤销超时倒计时
164
+ progressCtl.taskCompleted(worker.id);//worker 行回到 idle; 完成数按盘上结果目录统计, 不在此计数
161
165
  queue.enqueue(async () => {
162
166
  //use queue to make sure no I/O conflict; update/pop/判空须在同一个串行任务里原子完成
163
167
  await TaskMgr.updateNewTasks();
@@ -177,7 +181,7 @@ const start = (config)=>{
177
181
  }
178
182
  if(!subTask){
179
183
  //no tasks left
180
- console.log('[Master]: no tasks left!');
184
+ runLog.write('[Master]: no tasks left!');
181
185
  if(config.autoCloseAfterCompletion){
182
186
  killWorker(worker);
183
187
  }else{//waiting for new tasks
@@ -185,6 +189,7 @@ const start = (config)=>{
185
189
  keepOpen: (typeof config.keepOpen === 'undefined') ? false : config.keepOpen
186
190
  });
187
191
  }
192
+ progressCtl.render();
188
193
  return;
189
194
  }
190
195
  let terminateData = {taskCount,subTask,config, id: worker.id};
@@ -206,6 +211,7 @@ const start = (config)=>{
206
211
  },()=>{}, {
207
212
  keepOpen: (typeof config.keepOpen === 'undefined') ? false : config.keepOpen
208
213
  });
214
+ progressCtl.taskAssigned(worker.id, subTask.subid);
209
215
  let hasTimeout = typeof config.taskTimeout === 'number' && config.taskTimeout > 0;
210
216
  let hasRetry = typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0;
211
217
  if(hasTimeout || hasRetry){
@@ -223,17 +229,37 @@ const start = (config)=>{
223
229
 
224
230
  let initPromise = config.__resume ? asMaster.resume(config) : asMaster.init(config);
225
231
  initPromise.then((updated_config)=>{
226
- console.log(` new tasks=${TaskMgr.getTasks('new').length}`);
227
232
  config = updated_config;
228
- console.log('');//以下: 配套监控工程命令提示, master 打印一次, 星号边框突出显示
229
- console.log('****************************************************************');
230
- console.log('[Master]: monitor this task with:');
231
- console.log(` npx multi-tasks-monitor --task-dir "${config.taskFolder}" --port 3777`);
232
- console.log('****************************************************************');
233
+ //一次性启动块(走 console, 在劫持前上屏): 标题框横幅 + 平铺关键字段, 模式由 __ 标记推导;
234
+ //init 成功后所有信息齐备(new tasks 数/masterTaskId/__sysLogFile), 一次打完
235
+ let startupInfo = {
236
+ mode: config.__restart ? 'RESTART' : (config.__retryFails ? 'RETRY_FAILS' : (config.__resume ? 'RESUME' : 'START')),
237
+ taskId: config.masterTaskId,
238
+ taskFolder: config.taskFolder,
239
+ numOfWorkers,
240
+ numCPUs,
241
+ pid: process.pid,
242
+ newTasks: TaskMgr.getTasks('new').length,
243
+ sysLogFile: config.__sysLogFile,
244
+ startedAt: getDateTimeTxt(),
245
+ };
246
+ console.log(startupBanner.renderBanner(startupInfo));
247
+ progressCtl.setup({//TTY 判定 + 渲染器创建都在控制器内; 统计口径注入, 控制器不依赖 TaskMgr
248
+ config,
249
+ stream: process.stdout,
250
+ getStats: () => TaskMgr.getFinalStats(),
251
+ getRemaining: () => TaskMgr.getTasks('new').length,
252
+ });
253
+ //console 劫持写 run.log 是永久机制, 与 progressBar 无关: 进度区接管终端(开且 TTY)时只写文件,
254
+ //否则 echo 同时透传 console 上屏; 判定收敛在 initAndHook 内, 无 __sysLogFile 时维持 console
255
+ runLog.initAndHook(config.__sysLogFile, { echo: !progressCtl.isOn() });
256
+ startupBanner.writeLogHeader(startupInfo);//同一组关键信息写 run.log 头部; 无 __sysLogFile 时内部 no-op
233
257
  for (let i = 0; i < numOfWorkers; i++) {
234
258
  let worker = cluster.fork();
235
259
  initWorker(worker);
260
+ progressCtl.workerStarted(worker.id, worker.process.pid);
236
261
  }
262
+ progressCtl.render();
237
263
  }).catch((e)=>{
238
264
  //init/resume 异步失败(rename/读盘错误等): 明确 FATAL + 非 0 退出,
239
265
  //否则 unhandled rejection 在 warn 模式下会不 fork 不退出、静默挂死
@@ -243,14 +269,17 @@ const start = (config)=>{
243
269
  cluster.on('exit', (worker, code, signal) => {
244
270
  let rec = workerTaskTimers[worker.id];//先取再清: 崩溃重试要用 rec.subTask
245
271
  clearTaskTimer(worker.id);//worker 退出(含崩溃)时清掉其超时计时器
272
+ progressCtl.workerExited(worker.id);
246
273
  if (code !== 0 && !worker.exitedAfterDisconnect) {
247
- console.log('Worker died unexpectedly starting a new one...');
274
+ runLog.write('Worker died unexpectedly starting a new one...');
248
275
  workerCrashCount++;
249
276
  if(rec && rec.subTask){
250
277
  handleCrashedTask(rec.subTask);
251
278
  };
252
279
  let worker = cluster.fork();
253
280
  initWorker(worker);
281
+ progressCtl.workerStarted(worker.id, worker.process.pid);
282
+ progressCtl.render();
254
283
  return;
255
284
  }
256
285
  let allkilled = true;
@@ -281,24 +310,30 @@ const start = (config)=>{
281
310
  delete USER_CONFIG.onFinish;
282
311
  }
283
312
  setTimeout(()=>{
284
- console.log('[Master]: all workers are done, process exit.')
313
+ runLog.write('[Master]: all workers are done, process exit.')
314
+ progressCtl.finish();//单行进度条换行定格, 别让 shell 提示符盖住它
285
315
  process.exit(0);
286
316
  }, 10);
287
317
  }
288
318
  });
289
319
  }else{
290
- subWorker();
320
+ subWorker(config);
291
321
  }
292
322
  };
293
- function subWorker(){
323
+ function subWorker(config){
294
324
  let worker = cluster.worker;
325
+ //worker 进程独立重跑入口, config.__sysLogFile 已由 workers/index.js 入口算好(与 master 同一路径):
326
+ //console 劫持写 run.log 永久生效, 与 progressBar 无关; progressBar 未显式关闭(缺省默认开启)且
327
+ //共享 stdout 为 TTY 时只写文件(与 master 判定一致), 否则 echo 同时上屏。判定收敛在 initAndHook 内,
328
+ //无路径时维持 console。入口即生效, 领不到任务的富余 worker 的 waiting 日志同样进 run.log。
329
+ runLog.initAndHook(config.__sysLogFile, { echo: !(config.progressBar !== false && process.stdout.isTTY) });
295
330
  worker.on('message', function(message) {
296
331
  if(message && message.__broadcast){//master 中转来的广播: 分发给 helper.on 注册的 handler
297
332
  helper.__dispatchBroadcast(message);
298
333
  return;
299
334
  }
300
335
  if(message === MSG_NO_TASK_FOUND){//wait for new task
301
- console.log(` (Worker #${worker.id}): waiting`);
336
+ runLog.write(` (Worker #${worker.id}): waiting`);
302
337
  setTimeout(()=>{
303
338
  worker.send(MSG_REQUEST_NEW_TASK);//request new task
304
339
  }, 5000 + Math.random()*5000);
@@ -310,7 +345,7 @@ function subWorker(){
310
345
  subTask.taskCount = taskCount;
311
346
  subTask.workerId = worker.id;
312
347
  subTask.workerPid = process.pid;
313
- console.log(`[Task Start]: "${subTask.subid}"`, `pid=${process.pid}`, getDateTimeTxt());
348
+ runLog.write(`[Task Start]: "${subTask.subid}"`, `pid=${process.pid}`, getDateTimeTxt());
314
349
  let subTaskLog = {
315
350
  workerId: worker.id,
316
351
  workerPid: process.pid,
@@ -320,12 +355,12 @@ function subWorker(){
320
355
  runTask(config, subTask, subTaskLog).then((tasklog)=>{
321
356
  let {cost, startTimestamp, endTimestamp} = tasklog;
322
357
 
323
- console.log(`[Task Finished] "${config.masterTaskId}"."${subTask.subid}".`, getDateTimeTxt(), `cost=${cost}ms`)
358
+ runLog.write(`[Task Finished] "${config.masterTaskId}"."${subTask.subid}".`, getDateTimeTxt(), `cost=${cost}ms`)
324
359
  }).then(()=>{
325
360
  worker.send(MSG_REQUEST_NEW_TASK);//request new task
326
361
  });
327
362
  }else{
328
- console.log('Can not find: message.subTask');
363
+ runLog.write('Can not find: message.subTask');
329
364
  throw new Error('can not find subTask!')
330
365
  }
331
366
  });
@@ -366,19 +401,19 @@ function runTask(config, subTask, subTaskLog){
366
401
  cost
367
402
  };
368
403
  let hasError = errors.length > 0;
369
- if(hasError)console.warn('[Task Error]', subTask.subid, errors);
404
+ if(hasError)runLog.warn('[Task Error]', subTask.subid, errors);
370
405
  let retried = false;
371
406
  if(hasError && typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
372
407
  let retryCount = TaskMgr.incrementTaskRetry(subTask.subid);
373
408
  if(retryCount <= config.maxTaskRetries){
374
- console.warn(`[Task Retry] "${subTask.subid}" failed, retry (${retryCount}/${config.maxTaskRetries})`);
409
+ runLog.warn(`[Task Retry] "${subTask.subid}" failed, retry (${retryCount}/${config.maxTaskRetries})`);
375
410
  retried = true;
376
411
  //等 rename 落盘再 resolve/请求新任务: fire-and-forget 的异步 rename 会随 worker 被
377
412
  //autoClose 杀掉而消亡, 任务文件滞留 running 被静默丢弃(高负载偶发丢任务的根因, 勿回退)
378
413
  try{
379
414
  await TaskMgr.mvTaskPromise(subTask.subid, 'running', 'new');
380
415
  }catch(newPath){
381
- console.warn(`[Task Retry] "${subTask.subid}" failed to move back to new, left in running (recoverable via resume)`);
416
+ runLog.warn(`[Task Retry] "${subTask.subid}" failed to move back to new, left in running (recoverable via resume)`);
382
417
  };
383
418
  };
384
419
  };
@@ -4,6 +4,7 @@ const makeDir = require('../utils/makedir');
4
4
  const randomsUtil = require('../utils/randoms');
5
5
  const { serializeConfig } = require('../utils/serializeFns');
6
6
  const { forEachResult } = require('../utils/foreachResults');
7
+ const runLog = require('../utils/runLog');
7
8
 
8
9
  let allSubTasks = {
9
10
  new:[],
@@ -35,6 +36,7 @@ const TaskMgr = {
35
36
  const progressRootFolder = pathutil.resolve(taskFolder, './progress');
36
37
  const resultsRootFolder = pathutil.resolve(taskFolder, './results');
37
38
  const logsRootFolder = pathutil.resolve(taskFolder, './logs');
39
+ const sysRootFolder = pathutil.resolve(taskFolder, './.sys');//库自身运行日志(run.log)等系统文件, 与任务数据/结果隔离
38
40
 
39
41
  const taskFolderNew = pathutil.resolve(progressRootFolder, './new');
40
42
  const taskFolderRunning = pathutil.resolve(progressRootFolder, './running');
@@ -44,6 +46,7 @@ const TaskMgr = {
44
46
  const taskFolderResult = pathutil.resolve(resultsRootFolder, './succ');
45
47
  const taskFolderResultFailed = pathutil.resolve(resultsRootFolder, './errors');
46
48
  makeDir.sync(logsRootFolder);
49
+ makeDir.sync(sysRootFolder);
47
50
  makeDir.sync(taskFolderNew);
48
51
  makeDir.sync(taskFolderRunning);
49
52
  makeDir.sync(taskFolderFinished);
@@ -57,6 +60,9 @@ const TaskMgr = {
57
60
  progressRootFolder,
58
61
  resultsRootFolder,
59
62
  logsRootFolder,
63
+ sysRootFolder,
64
+ //run.log 路径唯一兜底计算点: 入口(workers/index.js)没算过时在这里补(__ 前缀, 不进快照)
65
+ __sysLogFile: main_config.__sysLogFile || pathutil.resolve(sysRootFolder, 'run.log'),
60
66
 
61
67
  taskFolderNew,
62
68
  taskFolderRunning,
@@ -97,14 +103,14 @@ const TaskMgr = {
97
103
  try{
98
104
  fcontent = fs.readFileSync(fpath, 'utf-8');
99
105
  }catch(e){
100
- console.log('[WARN]', `file not found`, fpath)
106
+ runLog.warn('[WARN]', `file not found`, fpath)
101
107
  }
102
108
  if(fcontent){
103
109
  try{
104
110
  let task = JSON.parse(fcontent);
105
111
  allSubTasks[type].push(task);
106
112
  }catch(e){
107
- console.warn('[WARN]', 'failed to parse task file, skipped', fpath);
113
+ runLog.warn('[WARN]', 'failed to parse task file, skipped', fpath);
108
114
  }
109
115
  }
110
116
  });
@@ -199,7 +205,7 @@ const TaskMgr = {
199
205
  });
200
206
  },
201
207
  mvTask: (subid, from, to, callback)=> {
202
- console.log('Move task:', subid, `${from}>${to}`);
208
+ runLog.write('Move task:', subid, `${from}>${to}`);
203
209
  let {progressRootFolder} = main_config;
204
210
  let oldPath = pathutil.resolve(progressRootFolder, `./${from}/${subid}.task.json`);
205
211
  let newPath = pathutil.resolve(progressRootFolder, `./${to}/${subid}.task.json`);
@@ -212,7 +218,7 @@ const TaskMgr = {
212
218
  }
213
219
  });
214
220
  }catch(e){
215
- console.log(e);
221
+ runLog.warn(e);
216
222
  callback(newPath, false);
217
223
  }
218
224
  },
@@ -1,6 +1,8 @@
1
1
  let workers = {};
2
2
  let status = {};
3
3
 
4
+ const runLog = require('../utils/runLog');
5
+
4
6
  const mgr = {
5
7
  addWorker:(worker)=>{
6
8
  let id = `${worker.id}`;
@@ -8,7 +10,7 @@ const mgr = {
8
10
  throw new Error('FATAL: duplicated worker!');
9
11
  }
10
12
  status[id] = {isBusy: true};
11
- console.log('add worker', worker.id);
13
+ runLog.write('add worker', worker.id);
12
14
  workers[id] = worker;
13
15
  },
14
16
  getWorker:(id)=>{
@@ -77,8 +77,6 @@ const init = (config)=>{
77
77
  let randomNum = round((random() * pow(10,8)),8).toString(36);
78
78
  let masterTaskId = config.taskId ? config.taskId : `${taskName}${dateTimeText}--${randomNum}`;
79
79
 
80
- console.log(` masterTaskId===${masterTaskId}`);
81
-
82
80
  config.taskRootFolder = taskRootFolder;
83
81
  config.masterTaskId = masterTaskId;
84
82
 
@@ -93,7 +91,6 @@ const init = (config)=>{
93
91
  //to reload is for to reStart a collapsed task
94
92
  return new Promise((resolve)=>{
95
93
  TaskMgr.loadNewTasks().then(()=>{
96
- console.log('******start multi-thread')
97
94
  resolve(config);
98
95
  });
99
96
  });
package/workers/helper.js CHANGED
@@ -2,6 +2,7 @@ let fs = require('fs');
2
2
  let pathutil = require('path');
3
3
 
4
4
  let TaskMgr = require('./TaskMgr');
5
+ let runLog = require('../utils/runLog');
5
6
 
6
7
  let myconfig;
7
8
  let listeners = {};//event -> handler, worker 进程内模块级广播监听 registry(每个 event 一个 handler)
@@ -14,7 +15,7 @@ const createNewTasks = (tasks)=>{
14
15
  if (!Array.isArray(tasks)) tasks = [tasks];
15
16
  TaskMgr.load(myconfig);
16
17
  tasks.forEach((task)=>{
17
- console.log('Creating new task', task)
18
+ runLog.write('Creating new task', task)
18
19
  TaskMgr.createNewTask(task);
19
20
  });
20
21
  };
@@ -40,6 +41,10 @@ const setListener = (event, handler)=>{
40
41
  if (typeof handler !== 'function') throw new Error('helper.setListener: handler must be a function');
41
42
  listeners[event] = handler;
42
43
  };
44
+ //任务日志: 一律写 <taskFolder>/.sys/run.log; progressBar 未开/非 TTY 回退时 echo 上屏(效果等同 console.log)
45
+ const log = (...args)=>{
46
+ runLog.write(...args);
47
+ };
43
48
  //内部方法: 供 subWorker 收到 master 中转的广播时调用, 按 event 分发
44
49
  const __dispatchBroadcast = (message)=>{
45
50
  let {event, payload, meta} = message;
@@ -48,7 +53,7 @@ const __dispatchBroadcast = (message)=>{
48
53
  try{
49
54
  handler(payload, meta);
50
55
  }catch(e){
51
- console.error(`[Broadcast] handler for event "${event}" threw:`, e);
56
+ runLog.error(`[Broadcast] handler for event "${event}" threw:`, e);
52
57
  }
53
58
  };
54
59
  module.exports = {
@@ -57,5 +62,6 @@ module.exports = {
57
62
  emit,
58
63
  emitSys,
59
64
  setListener,
65
+ log,
60
66
  __dispatchBroadcast
61
67
  };
package/workers/index.js CHANGED
@@ -43,11 +43,21 @@ const _mergeSnapshotConfig = (config)=>{
43
43
  return merged;
44
44
  };
45
45
 
46
+ //run.log 路径在入口统一算好挂到 config(__ 前缀: 一次性派生值, 不进快照), 与 progressBar 无关;
47
+ //master/worker 各进程都重跑入口、算出同一路径, Multithread 直接使用, 不做任何路径推导。
48
+ const _computeSysLogFile = (config)=>{
49
+ let taskFolder = config.taskFolder || ((config.taskId && config.taskRootFolder) ? pathUtil.resolve(config.taskRootFolder, config.taskId) : null);
50
+ if(taskFolder){
51
+ config.__sysLogFile = pathUtil.resolve(taskFolder, '.sys', 'run.log');
52
+ };
53
+ };
54
+
46
55
  const start = (config)=>{
47
56
  config = backwardv1.v1tov2(config);
48
57
 
49
58
  config.taskRootFolder = _toAbsPath(config.taskRootFolder);
50
59
  config.taskFolder = _toAbsPath(config.taskFolder);
60
+ _computeSysLogFile(config);
51
61
 
52
62
  Multithread.start(config);
53
63
  };
@@ -65,6 +75,7 @@ const resume = (config)=>{
65
75
  };
66
76
  config.taskRootFolder = pathUtil.dirname(config.taskFolder);
67
77
  config.taskId = pathUtil.basename(config.taskFolder);
78
+ _computeSysLogFile(config);
68
79
 
69
80
  config = _mergeSnapshotConfig(config);
70
81
  Multithread.start(config);
@@ -80,6 +91,7 @@ const retry_fails = (config)=>{
80
91
  config.taskId = pathUtil.basename(taskFolder);
81
92
  config.__resume = true;
82
93
  config.__retryFails = true;
94
+ _computeSysLogFile(config);
83
95
  config = _mergeSnapshotConfig(config);
84
96
  Multithread.start(config);
85
97
  };
@@ -93,6 +105,7 @@ const restart = (config)=>{
93
105
  config.taskRootFolder = pathUtil.dirname(taskFolder);
94
106
  config.taskId = pathUtil.basename(taskFolder);
95
107
  config.__restart = true;
108
+ _computeSysLogFile(config);
96
109
  config = _mergeSnapshotConfig(config);
97
110
  Multithread.start(config);
98
111
  };