multi-tasks 3.1.9 → 3.2.1

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,69 +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.
37
-
38
- ### API:
39
-
40
- The API has three parts: the entry functions, the config options, and the `helper` object injected into `processTask`.
41
-
42
- **Entry functions:**
43
-
44
- - **`multiTasks(config)`** — run tasks; auto-resumes if the task folder already exists or `initialTasks` points to one.
45
- - **`multiTasks.resume(config)`** — usually you don't need this: the simplest way to resume is to just run `multiTasks(config)` again, it auto-resumes when the task folder already exists. This method resumes an interrupted run from the task folder alone; pass a config object with `taskFolder` (the full task folder path). The whole config (including `processTask`) is restored from `task_config.json` in that folder; any field you pass overrides the snapshot, and overrides are written back into it.
46
- - **`multiTasks.retry_fails(config)`** — same as `resume`, and also retries the failed tasks.
47
- - **`multiTasks.restart(config)`** — wipe the task folder's progress and re-run from scratch; pass a config object with `taskFolder`. The config (including `processTask` and `initialTasks`) is restored from `task_config.json`; any field you pass overrides the snapshot, and overrides are written back into it. **All previous progress and results are deleted.**
48
-
49
- **Config options:**
50
-
51
- - **`initialTasks`** — array of task objects, or a folder path string to resume from.
52
- - **`processTask(task, helper)`** — required; return a value or a Promise.
53
- - **`taskRootFolder`** — the task run folder (named by `taskId`) is created under this directory.
54
- - **`taskId`** — task folder name under `taskRootFolder`.
55
- - **`numberOfWorkers`** — how many worker processes run in parallel; a number, or a percentage string of CPU cores like `"50%"` (default).
56
- - **`taskTimeout`** — optional, milliseconds; an overdue task fails with a timeout error.
57
- - **`maxTaskRetries`** — optional, max times a failed task is auto-retried.
58
- - **`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
- - **`shouldTerminate(info)`** — return `true` to terminate the whole process.
60
- - **`setSysListener(event, payload, meta)`** — optional, handle custom system events (sent via `helper.emitSys`) on the master; `meta` has `fromWorkerId` and `fromWorkerPid`.
61
- - **`onFinish(report, helper)`** — called once after all workers exit (only when `autoCloseAfterCompletion` is `true`). `report` summarizes the run: `{ taskId, taskFolder, startTimestamp, endTimestamp, cost, total, succeeded, failed, failedTasks, workers }` — `cost` is the run duration in ms (a resumed run measures only the current round); `failedTasks` lists the failed tasks' subids; `workers` is `{ count, crashed }` — the configured worker count and how many workers crashed and were replaced. `helper` lets you walk every persisted task result:
62
- - **`helper.foreachSuccResult((count, task, result) => {})`** — iterate all succeeded results (`results/succ`); `count` is a 0-based index, `task` is the original task object (`null` if its task file is missing or unparseable), `result` is the value returned by `processTask` (`undefined` when the task returned nothing).
63
- - **`helper.foreachErrorResult((count, task, err) => {})`** — iterate all failed results (`results/errors`); `err` is the persisted error object as-is (`{ex}` / `{subTask, exception}` / `{type: 'timeout', ...}` / `{type: 'worker_crash', ...}`).
64
- - Iteration follows filesystem order (not sorted). `onFinish` runs synchronously at the very end and the master process exits shortly after it returns — don't `await` inside it.
65
-
66
- **Other options:**
67
-
68
- - **`taskFolder`** — full task folder path; only used by `resume`/`retry_fails`/`restart`.
69
-
70
- **Task helper** (the `helper` object passed as the second argument of `processTask`):
71
-
72
- - **`helper.createNewTasks(tasks)`** — create new tasks dynamically while processing.
73
- - **`helper.emit(event, payload, option?)`** — broadcast an event to all workers (relayed by the master, best-effort); `option.includingMe` defaults to `true` — pass `{includingMe: false}` to exclude the sender. Payload must be JSON-serializable.
74
- - **`helper.setListener(event, handler)`** — listen for broadcast events; one handler per event per worker process (re-registering replaces it), so calling it inside `processTask` is always safe; `handler(payload, meta)` gets `meta.fromWorkerId` and `meta.fromWorkerPid`. Broadcasts are runtime-only messages — not persisted, not replayed on resume.
75
- - **`helper.emitSys(event, payload?)`** — send a system event to the master (consumed by the master itself, not relayed). Built-in event: `'TERMINATE_ALL_WORKERS'` force-kills all workers and exits the master; any other event goes to `config.setSysListener`.
13
+ Tip: monitor running tasks in real time — see [Real-Time Monitoring](#real-time-monitoring).
76
14
 
77
15
  ### How to use:
78
16
 
@@ -295,8 +233,65 @@ multiTasks.restart({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //deletes
295
233
 
296
234
  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
235
 
236
+ ### Real-Time Monitoring
237
+
238
+ Monitor your running tasks with [multi-tasks-monitor](https://www.npmjs.com/package/multi-tasks-monitor) — see its npm page for full details.
239
+
240
+ 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:
241
+
242
+ ```text
243
+ ****************************************************************
244
+ [Master]: monitor this task with:
245
+ npx multi-tasks-monitor --task-dir "D:\tasks\multitasks20260806..." --port 3777
246
+ ****************************************************************
247
+ ```
248
+
249
+ For installation, command-line options, and more, see the [multi-tasks-monitor](https://www.npmjs.com/package/multi-tasks-monitor) page.
250
+
251
+ ### API:
252
+
253
+ The API has three parts: the entry functions, the config options, and the `helper` object injected into `processTask`.
254
+
255
+ **Entry functions:**
256
+
257
+ - **`multiTasks(config)`** — run tasks; auto-resumes if the task folder already exists or `initialTasks` points to one.
258
+ - **`multiTasks.resume(config)`** — usually you don't need this: the simplest way to resume is to just run `multiTasks(config)` again, it auto-resumes when the task folder already exists. This method resumes an interrupted run from the task folder alone; pass a config object with `taskFolder` (the full task folder path). The whole config (including `processTask`) is restored from `task_config.json` in that folder; any field you pass overrides the snapshot, and overrides are written back into it.
259
+ - **`multiTasks.retry_fails(config)`** — same as `resume`, and also retries the failed tasks.
260
+ - **`multiTasks.restart(config)`** — wipe the task folder's progress and re-run from scratch; pass a config object with `taskFolder`. The config (including `processTask` and `initialTasks`) is restored from `task_config.json`; any field you pass overrides the snapshot, and overrides are written back into it. **All previous progress and results are deleted.**
261
+
262
+ **Config options:**
263
+
264
+ - **`initialTasks`** — array of task objects, or a folder path string to resume from.
265
+ - **`processTask(task, helper)`** — required; return a value or a Promise.
266
+ - **`taskRootFolder`** — the task run folder (named by `taskId`) is created under this directory.
267
+ - **`taskId`** — task folder name under `taskRootFolder`.
268
+ - **`numberOfWorkers`** — how many worker processes run in parallel; a number, or a percentage string of CPU cores like `"50%"` (default).
269
+ - **`taskTimeout`** — optional, milliseconds; an overdue task fails with a timeout error.
270
+ - **`maxTaskRetries`** — optional, max times a failed task is auto-retried.
271
+ - **`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.
272
+ - **`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.
273
+ - **`shouldTerminate(info)`** — return `true` to terminate the whole process.
274
+ - **`setSysListener(event, payload, meta)`** — optional, handle custom system events (sent via `helper.emitSys`) on the master; `meta` has `fromWorkerId` and `fromWorkerPid`.
275
+ - **`onFinish(report, helper)`** — called once after all workers exit (only when `autoCloseAfterCompletion` is `true`). `report` summarizes the run: `{ taskId, taskFolder, startTimestamp, endTimestamp, cost, total, succeeded, failed, failedTasks, workers }` — `cost` is the run duration in ms (a resumed run measures only the current round); `failedTasks` lists the failed tasks' subids; `workers` is `{ count, crashed }` — the configured worker count and how many workers crashed and were replaced. `helper` lets you walk every persisted task result:
276
+ - **`helper.foreachSuccResult((count, task, result) => {})`** — iterate all succeeded results (`results/succ`); `count` is a 0-based index, `task` is the original task object (`null` if its task file is missing or unparseable), `result` is the value returned by `processTask` (`undefined` when the task returned nothing).
277
+ - **`helper.foreachErrorResult((count, task, err) => {})`** — iterate all failed results (`results/errors`); `err` is the persisted error object as-is (`{ex}` / `{subTask, exception}` / `{type: 'timeout', ...}` / `{type: 'worker_crash', ...}`).
278
+ - Iteration follows filesystem order (not sorted). `onFinish` runs synchronously at the very end and the master process exits shortly after it returns — don't `await` inside it.
279
+
280
+ **Other options:**
281
+
282
+ - **`taskFolder`** — full task folder path; only used by `resume`/`retry_fails`/`restart`.
283
+
284
+ **Task helper** (the `helper` object passed as the second argument of `processTask`):
285
+
286
+ - **`helper.createNewTasks(tasks)`** — create new tasks dynamically while processing.
287
+ - **`helper.emit(event, payload, option?)`** — broadcast an event to all workers (relayed by the master, best-effort); `option.includingMe` defaults to `true` — pass `{includingMe: false}` to exclude the sender. Payload must be JSON-serializable.
288
+ - **`helper.setListener(event, handler)`** — listen for broadcast events; one handler per event per worker process (re-registering replaces it), so calling it inside `processTask` is always safe; `handler(payload, meta)` gets `meta.fromWorkerId` and `meta.fromWorkerPid`. Broadcasts are runtime-only messages — not persisted, not replayed on resume.
289
+ - **`helper.emitSys(event, payload?)`** — send a system event to the master (consumed by the master itself, not relayed). Built-in event: `'TERMINATE_ALL_WORKERS'` force-kills all workers and exits the master; any other event goes to `config.setSysListener`.
290
+
298
291
  ### Changelog:
299
292
 
293
+ - 3.2.1 Fix retry bug: user-defined taskCount/index no longer overwritten on task retries
294
+ - 3.2.0 Add progressBar (on by default); console output diverted to run.log; new startup banner
300
295
  - 3.1.9 Print the multi-tasks-monitor command on startup
301
296
  - 3.1.8 Small refinements
302
297
  - 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.1",
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);
@@ -306,11 +341,13 @@ function subWorker(){
306
341
  if(message.subTask){//new task
307
342
  let {config, subTask, startTimestamp, taskCount} = message;
308
343
  TaskMgr.load(config);
309
- subTask.index = taskCount;//for v1 backward
310
- subTask.taskCount = taskCount;
344
+ //į”¨æˆˇäģģåŠĄč‡Ēå¸Ļįš„ index/taskCount äŋį•™åŽŸå€ŧ(é‡č¯•/įģ­čˇ‘后äģģåŠĄčēĢäģŊ不变),
345
+ //æœĒ提䞛æ—ļ才åĄĢå…¨åą€æ´žå‘čŽĄæ•°å™¨(v1 å…ŧ厚)
346
+ if(typeof subTask.index === 'undefined') subTask.index = taskCount;
347
+ if(typeof subTask.taskCount === 'undefined') subTask.taskCount = taskCount;
311
348
  subTask.workerId = worker.id;
312
349
  subTask.workerPid = process.pid;
313
- console.log(`[Task Start]: "${subTask.subid}"`, `pid=${process.pid}`, getDateTimeTxt());
350
+ runLog.write(`[Task Start]: "${subTask.subid}"`, `pid=${process.pid}`, getDateTimeTxt());
314
351
  let subTaskLog = {
315
352
  workerId: worker.id,
316
353
  workerPid: process.pid,
@@ -318,14 +355,15 @@ function subWorker(){
318
355
  startTimestamp
319
356
  };
320
357
  runTask(config, subTask, subTaskLog).then((tasklog)=>{
358
+ if(tasklog.retried) return;//é‡č¯•äģģåŠĄåˇ˛å›ž new į­‰é‡čˇ‘, æœĒ厌成, 不打 [Task Finished]
321
359
  let {cost, startTimestamp, endTimestamp} = tasklog;
322
360
 
323
- console.log(`[Task Finished] "${config.masterTaskId}"."${subTask.subid}".`, getDateTimeTxt(), `cost=${cost}ms`)
361
+ runLog.write(`[Task Finished] "${config.masterTaskId}"."${subTask.subid}".`, getDateTimeTxt(), `cost=${cost}ms`)
324
362
  }).then(()=>{
325
363
  worker.send(MSG_REQUEST_NEW_TASK);//request new task
326
364
  });
327
365
  }else{
328
- console.log('Can not find: message.subTask');
366
+ runLog.write('Can not find: message.subTask');
329
367
  throw new Error('can not find subTask!')
330
368
  }
331
369
  });
@@ -366,19 +404,19 @@ function runTask(config, subTask, subTaskLog){
366
404
  cost
367
405
  };
368
406
  let hasError = errors.length > 0;
369
- if(hasError)console.warn('[Task Error]', subTask.subid, errors);
407
+ if(hasError)runLog.warn('[Task Error]', subTask.subid, errors);
370
408
  let retried = false;
371
409
  if(hasError && typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
372
410
  let retryCount = TaskMgr.incrementTaskRetry(subTask.subid);
373
411
  if(retryCount <= config.maxTaskRetries){
374
- console.warn(`[Task Retry] "${subTask.subid}" failed, retry (${retryCount}/${config.maxTaskRetries})`);
412
+ runLog.warn(`[Task Retry] "${subTask.subid}" failed, retry (${retryCount}/${config.maxTaskRetries})`);
375
413
  retried = true;
376
414
  //į­‰ rename čŊį›˜å† resolve/č¯ˇæą‚æ–°äģģåŠĄ: fire-and-forget įš„åŧ‚æ­Ĩ rename äŧšéš worker čĸĢ
377
415
  //autoClose æ€æŽ‰č€ŒæļˆäēĄ, äģģåŠĄæ–‡äģļæģžį•™ running čĸĢ静éģ˜ä¸ĸåŧƒ(é̘负čŊŊåļ发ä¸ĸäģģåŠĄįš„æ šå› , å‹ŋ回退)
378
416
  try{
379
417
  await TaskMgr.mvTaskPromise(subTask.subid, 'running', 'new');
380
418
  }catch(newPath){
381
- console.warn(`[Task Retry] "${subTask.subid}" failed to move back to new, left in running (recoverable via resume)`);
419
+ runLog.warn(`[Task Retry] "${subTask.subid}" failed to move back to new, left in running (recoverable via resume)`);
382
420
  };
383
421
  };
384
422
  };
@@ -387,7 +425,8 @@ function runTask(config, subTask, subTaskLog){
387
425
  resolve({
388
426
  startTimestamp,
389
427
  endTimestamp,
390
- cost
428
+ cost,
429
+ retried: true
391
430
  });
392
431
  return;
393
432
  };
@@ -399,7 +438,8 @@ function runTask(config, subTask, subTaskLog){
399
438
  resolve({
400
439
  startTimestamp,
401
440
  endTimestamp,
402
- cost
441
+ cost,
442
+ retried: false
403
443
  });
404
444
  });
405
445
  });
@@ -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
  };