@andersbakken/fisk 3.5.7 → 3.6.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.
@@ -1,767 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const option = require('@jhanssen/options')({ prefix: 'fisk/monitor',
4
- applicationPath: false,
5
- additionalFiles: [ "fisk/monitor.conf.override" ] });
6
- const WebSocket = require('ws');
7
- const fs = require('fs');
8
- const blessed = require('blessed');
9
- const humanize = require('humanize-duration');
10
-
11
- function humanizeDuration(age)
12
- {
13
- let units;
14
- if (age < 60000) {
15
- units = [ "s" ];
16
- } else if (age < 60 * 60000) {
17
- units = [ "m", "s" ];
18
- } else if (age < 24 * 60 * 60000) {
19
- units = [ "h", "m" ];
20
- } else if (age < 7 * 24 * 60 * 60000) {
21
- units = [ "d", "h" ];
22
- } else {
23
- units = [ "y", "mo", "w", "d" ];
24
- }
25
- const options = { units: units, round: true };
26
- return humanize(age, options);
27
- }
28
-
29
- const screen = blessed.screen({
30
- smartCSR: true
31
- });
32
-
33
- const builderContainer = blessed.box({
34
- top: '0%',
35
- left: '0%',
36
- width: '50%',
37
- height: '100%-3',
38
- border: {
39
- type: 'line'
40
- }
41
- });
42
-
43
- screen.on("resize", () => {
44
- // log("resize", builderContainer.width, builderContainer.height);
45
-
46
- updateBuilderBox();
47
- updateClientBox();
48
-
49
- screen.render();
50
- });
51
-
52
- const builderHeader = blessed.box({
53
- top: '0%',
54
- left: '0%',
55
- width: '100%-2',
56
- height: '0%+1',
57
- tags: true,
58
- style: {
59
- border: {
60
- fg: '#f0f0f0'
61
- }
62
- }
63
- });
64
-
65
- var prompt = blessed.prompt({
66
- parent: screen,
67
- top: 'center',
68
- left: 'center',
69
- height: 'shrink',
70
- width: 'shrink',
71
- keys: true,
72
- style: {
73
- fg: "white"
74
- },
75
- vi: true,
76
- mouse: true,
77
- tags: true,
78
- border: 'line',
79
- hidden: true
80
- });
81
-
82
- const builderBox = blessed.list({
83
- top: '0%+1',
84
- left: '0%',
85
- width: '100%-2',
86
- height: '100%-3',
87
- tags: true,
88
- scrollable: true,
89
- scrollbar: true,
90
- alwaysScroll: true,
91
- mouse: true,
92
- keys: true,
93
- vi: true,
94
- style: {},
95
- search: callback => {
96
- prompt.input('Search:', '', (err, value) => {
97
- if (err)
98
- return undefined;
99
- return callback(null, value);
100
- });
101
- }
102
- });
103
- builderBox.headerBox = builderHeader;
104
-
105
- const clientContainer = blessed.box({
106
- top: '0%',
107
- left: '50%',
108
- width: '50%',
109
- height: '100%-3',
110
- border: {
111
- type: 'line'
112
- }
113
- });
114
-
115
- const clientHeader = blessed.box({
116
- top: '0%',
117
- left: '0%',
118
- width: '100%-2',
119
- height: '0%+1',
120
- tags: true,
121
- style: {
122
- border: {
123
- fg: '#f0f0f0'
124
- }
125
- }
126
- });
127
-
128
- const clientBox = blessed.list({
129
- top: '0%+1',
130
- left: '0%',
131
- width: '100%-2',
132
- height: '100%-3',
133
- tags: true,
134
- scrollable: true,
135
- scrollbar: true,
136
- mouse: true,
137
- alwaysScroll: true,
138
- keys: true,
139
- vi: true,
140
- style: {}
141
- });
142
- clientBox.headerBox = clientHeader;
143
-
144
- const notificationBox = blessed.box({
145
- top: '100%-3',
146
- left: '0%',
147
- width: '100%',
148
- height: '0%+3',
149
- tags: true,
150
- border: {
151
- type: 'line'
152
- },
153
- style: {
154
- fg: 'white',
155
- bg: 'cyan',
156
- border: {
157
- fg: '#f0f0f0'
158
- }
159
- }
160
- });
161
-
162
- builderContainer.append(builderHeader);
163
- builderContainer.append(builderBox);
164
- clientContainer.append(clientHeader);
165
- clientContainer.append(clientBox);
166
- screen.append(builderContainer);
167
- screen.append(clientContainer);
168
- screen.append(notificationBox);
169
-
170
- let builderDialogBox, clientDialogBox;
171
-
172
- function hideDialogBoxes()
173
- {
174
- let ret = false;
175
- if (builderDialogBox) {
176
- builderDialogBox.detach();
177
- builderDialogBox = undefined;
178
- ret = true;
179
- }
180
-
181
- if (clientDialogBox) {
182
- clientDialogBox.detach();
183
- clientDialogBox = undefined;
184
- ret = true;
185
- }
186
- return ret;
187
- }
188
-
189
- builderBox.on("select", ev => {
190
- let render = hideDialogBoxes();
191
- activate(builderBox);
192
- if (ev) {
193
- let builderKey = /^ *([^ ]*)/.exec(ev.content)[1];
194
- let builder = builders.get(builderKey);
195
- if (builder) {
196
- builderBox.current = builderKey;
197
- let str = "";
198
- for (let key in builder) {
199
- let value = builder[key];
200
- if (Array.isArray(value)) {
201
- str += `{bold}${key}{/bold}: ${value[0]}\n`;
202
- for (let i=1; i<value.length; ++i) {
203
- let pad = "".padStart(key.length + 2, ' ');
204
- str += pad + value[i].padStart(key.length + 2, ' ') + "\n";
205
- }
206
- } else {
207
- str += `{bold}${key}{/bold}: ${value}\n`;
208
- }
209
- }
210
- builderDialogBox = blessed.box({
211
- top: 'center',
212
- left: 'center',
213
- width: '80%',
214
- height: '50%',
215
- content: str,
216
- tags: true,
217
- border: {
218
- type: 'line'
219
- },
220
- style: {
221
- fg: 'white',
222
- bg: '#0f0f0f',
223
- border: {
224
- fg: '#f0f0f0'
225
- }
226
- }
227
- });
228
- screen.append(builderDialogBox);
229
- render = true;
230
- }
231
- }
232
- if (render)
233
- screen.render();
234
- });
235
-
236
- clientBox.on("select", ev => {
237
- let render = hideDialogBoxes();
238
- activate(clientBox);
239
- if (ev) {
240
- // log("got ev", Object.keys(ev), ev.index, ev.$, ev.data);
241
- let clientKey = /^ *([^ ]*)/.exec(ev.content)[1];
242
- let jobs = jobsForClient.get(clientKey);
243
- // let client = clients.get(clientKey);
244
- if (jobs) {
245
- clientBox.current = clientKey;
246
- let str = "";
247
- let data = [ [ "Source file", "Builder", "Start time" ] ];
248
- let widest = [ data[0][0].length + 1, data[0][1].length + 1 ];
249
- const now = Date.now();
250
- for (let [jobKey, jobValue] of jobs) {
251
- if (jobKey == "total")
252
- continue;
253
- widest[0] = Math.max(jobValue.sourceFile.length + 1, widest[0]);
254
- data.push([ jobValue.sourceFile, jobValue.builder.ip + ":" + jobValue.builder.port, humanizeDuration(now - jobValue.time)]);
255
- widest[1] = Math.max(widest[1], data[data.length - 1][1].length + 1);
256
- }
257
-
258
- data.sort((a, b) => a[2] - b[2]);
259
-
260
- data.forEach((line, idx) => {
261
- if (!idx)
262
- str += "{bold}";
263
- str += line[0].padEnd(widest[0]) + " " + line[1].padEnd(widest[1]) + " " + line[2] + " ago\n";
264
- if (!idx)
265
- str += "{/bold}";
266
- });
267
- clientDialogBox = blessed.box({
268
- top: 'center',
269
- left: 'center',
270
- width: '80%',
271
- height: '50%',
272
- content: str,
273
- tags: true,
274
- border: {
275
- type: 'line'
276
- },
277
- style: {
278
- fg: 'white',
279
- bg: '#0f0f0f',
280
- border: {
281
- fg: '#f0f0f0'
282
- }
283
- }
284
- });
285
- screen.append(clientDialogBox);
286
- render = true;
287
- }
288
- }
289
- if (render)
290
- screen.render();
291
- });
292
-
293
- let currentFocus = undefined;
294
- function activate(box)
295
- {
296
- if (currentFocus == box)
297
- return;
298
-
299
- if (currentFocus) {
300
- currentFocus.style = {
301
- selected: {
302
- bg: '#606060',
303
- bold: true
304
- },
305
- item: {
306
- bg: '#404040'
307
- },
308
- fg: 'white',
309
- bg: '#404040',
310
- border: {
311
- fg: '#f0f0f0'
312
- },
313
- scrollbar: {
314
- bg: '#770000'
315
- }
316
- };
317
- currentFocus.headerBox.style.fg = 'white';
318
- currentFocus.headerBox.style.bg = '#004400';
319
- }
320
-
321
- currentFocus = box;
322
- currentFocus.style = {
323
- selected: {
324
- bg: 'blue',
325
- bold: true
326
- },
327
- item: {
328
- bg: "black"
329
- },
330
- fg: 'white',
331
- bg: 'black',
332
- border: {
333
- fg: '#f0f0f0'
334
- },
335
- scrollbar: {
336
- bg: 'red'
337
- }
338
- };
339
- currentFocus.headerBox.style.fg = 'white';
340
- currentFocus.headerBox.style.bg = '#00ff00';
341
- currentFocus.focus();
342
- screen.render();
343
- }
344
-
345
- activate(builderBox);
346
- activate(clientBox);
347
-
348
- function focusRight()
349
- {
350
- if (currentFocus == builderBox) {
351
- activate(clientBox);
352
- }
353
- }
354
-
355
- function focusLeft()
356
- {
357
- if (currentFocus == clientBox) {
358
- activate(builderBox);
359
- }
360
- }
361
-
362
- // Quit on Escape, q, or Control-C.
363
- screen.key(['C-c'], (ch, key) => {
364
- return process.exit();
365
- });
366
- screen.key(['escape', 'q'], (ch, key) => {
367
- if (builderDialogBox) {
368
- builderDialogBox.detach();
369
- builderDialogBox = undefined;
370
- screen.render();
371
- } else if (clientDialogBox) {
372
- clientDialogBox.detach();
373
- clientDialogBox = undefined;
374
- screen.render();
375
- } else {
376
- process.exit();
377
- }
378
- });
379
- screen.key(['right', 'l'], (ch, key) => {
380
- focusRight();
381
- });
382
- screen.key(['left', 'h'], (ch, key) => {
383
- focusLeft();
384
- });
385
-
386
- builderBox.on('click', () => {
387
- activate(builderBox);
388
- });
389
- clientBox.on('click', () => {
390
- activate(clientBox);
391
- });
392
-
393
- screen.render();
394
-
395
- let notificationInterval;
396
- let notifications = [];
397
-
398
- function notify(msg)
399
- {
400
- if (notificationInterval) {
401
- if (notifications.length == 5)
402
- notifications.splice(0, 1);
403
- notifications.push(msg);
404
- return;
405
- }
406
-
407
- const notifyNow = msg => {
408
- notificationBox.setContent(msg);
409
- screen.render();
410
- };
411
-
412
- notificationInterval = setInterval(() => {
413
- if (notifications.length == 0) {
414
- clearInterval(notificationInterval);
415
- notificationInterval = undefined;
416
- notifyNow();
417
- return;
418
- }
419
-
420
- notifyNow(notifications.shift());
421
- }, 2000);
422
-
423
- notifyNow(msg);
424
- }
425
-
426
- let scheduler = option("scheduler", "ws://localhost:8097");
427
- if (scheduler.indexOf('://') == -1)
428
- scheduler = "ws://" + scheduler;
429
- if (!/:[0-9]+$/.exec(scheduler))
430
- scheduler += ":8097";
431
-
432
- function log(...args)
433
- {
434
- const str = args.map(elem => typeof elem === "object" ? JSON.stringify(elem) : elem).join(" ");
435
- fs.appendFileSync("/tmp/fisk-monitor.log", str + "\n");
436
- }
437
-
438
- try {
439
- // fs.unlinkSync("/tmp/fisk-monitor.log");
440
- } catch (e) {
441
- }
442
-
443
- const builders = new Map();
444
- const jobs = new Map();
445
- const jobsForClient = new Map();
446
-
447
- function clearData()
448
- {
449
- builders.clear();
450
- jobs.clear();
451
- jobsForClient.clear();
452
-
453
- update();
454
- }
455
-
456
- function formatCell(str, num, prefix, suffix)
457
- {
458
- return (prefix || "") + (" " + str).padEnd(num, " ").substr(0, num) + (suffix || "");
459
- }
460
-
461
- let updateTimer;
462
- let timeout = 0;
463
-
464
- function updateBuilderBox()
465
- {
466
- const builderWidth = builderContainer.width - 3;
467
-
468
- let data = [];
469
- let maxWidth = [6, 8, 7, 7, 12, 20];
470
- let newest = Number.MAX_SAFE_INTEGER;
471
- const now = Date.now();
472
- let f = true;
473
- for (let [key, value] of builders) {
474
- const added = new Date(value.created).valueOf();
475
- const age = now - added;
476
- newest = Math.min(newest, age);
477
- const name = value.name || value.hostname || key;
478
- const labels = value.labels ? value.labels.join(" ") : "";
479
- const line = [ name, `${value.active}`, `${value.jobsPerformed}`, `${value.slots}`, `${humanizeDuration(age)}`, labels ];
480
- data.push(line);
481
-
482
- for (let i=0; i<line.length; ++i) {
483
- maxWidth[i] = Math.max(maxWidth[i], line[i].length + 2);
484
- }
485
- }
486
- data.sort((a, b) => {
487
- let an = parseInt(a[1]);
488
- let bn = parseInt(b[1]);
489
- if (an != bn)
490
- return bn - an;
491
- an = parseInt(a[2]);
492
- bn = parseInt(b[2]);
493
- if (an != bn)
494
- return bn - an;
495
- return a[0].localeCompare(b[0]);
496
- });
497
-
498
- let used = 0;
499
- for (let i = 0; i < maxWidth.length; ++i) {
500
- if (used + maxWidth[i] > builderWidth)
501
- maxWidth[i] = builderWidth - used;
502
- used += maxWidth[i];
503
- }
504
- let header = "";
505
- header += formatCell("Host", maxWidth[0], "{bold}", "{/bold}");
506
- header += formatCell("Active", maxWidth[1], "{bold}", "{/bold}");
507
- header += formatCell("Total", maxWidth[2], "{bold}", "{/bold}");
508
- header += formatCell("Slots", maxWidth[3], "{bold}", "{/bold}");
509
- header += formatCell("Uptime", maxWidth[4], "{bold}", "{/bold}");
510
- header += formatCell("Labels", maxWidth[5], "{bold}", "{/bold}");
511
-
512
- builderHeader.setContent(header);
513
-
514
- let item = builderBox.getItem(builderBox.selected);
515
- let selectedBuilder;
516
- if (item) {
517
- selectedBuilder = /^ *([^ ]*)/.exec(item.content)[1];
518
- }
519
- let current;
520
- let items = data.map((item, idx) => {
521
- if (item[0] == selectedBuilder) {
522
- current = idx;
523
- }
524
- return formatCell(item[0], maxWidth[0]) + formatCell(item[1], maxWidth[1]) + formatCell(item[2], maxWidth[2]) + formatCell(item[3], maxWidth[3]) + formatCell(item[4], maxWidth[4]) + formatCell(item[5], maxWidth[5]);
525
- });
526
- builderBox.setItems(items);
527
- if (current != undefined) {
528
- builderBox.selected = current;
529
- }
530
- if (currentFocus != builderBox) {
531
- builderBox.scrollTo(0);
532
- }
533
-
534
- if (newest < 60000) {
535
- setTimeout(update, 1000);
536
- } else {
537
- setTimeout(update, 60000);
538
- }
539
-
540
- }
541
-
542
- function updateClientBox()
543
- {
544
- const clientWidth = clientContainer.width - 3;
545
-
546
- let data = [];
547
- let maxWidth = [6, 6, 7];
548
- for (let [key, value] of jobsForClient) {
549
- const line = [key, `${value.size - 1}`, `${value.get("total")}`];
550
- data.push(line);
551
-
552
- maxWidth[0] = Math.max(maxWidth[0], line[0].length + 2);
553
- maxWidth[1] = Math.max(maxWidth[1], line[1].length + 2);
554
- maxWidth[2] = Math.max(maxWidth[2], line[2].length + 2);
555
- }
556
-
557
- data.sort((a, b) => a[0].localeCompare(b[0]));
558
-
559
- let used = 0;
560
- for (let i of [1, 2, 0]) {
561
- if (used + maxWidth[i] > clientWidth)
562
- maxWidth[i] = clientWidth - used;
563
- used += maxWidth[i];
564
- }
565
-
566
- let header = "";
567
- header += formatCell("Name", maxWidth[0], "{bold}", "{/bold}");
568
- header += formatCell("Jobs", maxWidth[1], "{bold}", "{/bold}");
569
- header += formatCell("Total", maxWidth[2], "{bold}", "{/bold}");
570
- clientHeader.setContent(header);
571
-
572
- let item = clientBox.getItem(clientBox.selected);
573
- let selectedClient;
574
- if (item) {
575
- selectedClient = /^ *([^ ]*)/.exec(item.content)[1];
576
- }
577
- let current;
578
- let items = data.map((item, idx) => {
579
- if (item[0] == selectedClient) {
580
- current = idx;
581
- }
582
- return formatCell(item[0], maxWidth[0]) + formatCell(item[1], maxWidth[1]) + formatCell(item[2], maxWidth[2]);
583
- });
584
-
585
- clientBox.setItems(items);
586
- if (current != undefined) {
587
- clientBox.selected = current;
588
- }
589
- if (currentFocus != clientBox) {
590
- clientBox.scrollTo(0);
591
- }
592
- }
593
-
594
- function update()
595
- {
596
- //let data = [];
597
- if (updateTimer)
598
- return;
599
- updateTimer = setTimeout(() => {
600
- updateTimer = undefined;
601
- timeout = 500;
602
-
603
- updateBuilderBox();
604
- updateClientBox();
605
-
606
- screen.render();
607
- }, timeout);
608
- }
609
-
610
- function builderAdded(msg)
611
- {
612
- msg.active = 0;
613
- delete msg.type;
614
- builders.set(msg.ip + ":" + msg.port, msg);
615
- // console.log("Got builder", msg);
616
- update();
617
- }
618
-
619
- function builderRemoved(msg)
620
- {
621
- const builderKey = msg.ip + ":" + msg.port;
622
-
623
- for (let [jobKey, jobValue] of jobs) {
624
- if (jobValue.builder) {
625
- const jobBuilderKey = `${jobValue.builder.ip}:${jobValue.builder.port}`;
626
- if (builderKey === jobBuilderKey) {
627
- deleteJob(jobValue);
628
- }
629
- }
630
- }
631
-
632
- builders.delete(builderKey);
633
- update();
634
- }
635
-
636
- function clientName(client)
637
- {
638
- if ("name" in client) {
639
- if (client.name === client.hostname) {
640
- return "dev:" + (client.user || "nobody") + "@" + client.hostname;
641
- } else if (client.name.length > 0 && client.name[0] === '-') {
642
- return "dev:" + (client.user || "nobody") + client.name;
643
- }
644
- try {
645
- const o = JSON.parse(client.name);
646
- if (typeof o === "object" && "name" in o)
647
- return o.name;
648
- } catch (e) {
649
- }
650
- return client.name;
651
- }
652
- return client.ip;
653
- }
654
-
655
- function jobStarted(job)
656
- {
657
- // log(job);
658
- const builderKey = `${job.builder.ip}:${job.builder.port}`;
659
- const builder = builders.get(builderKey);
660
- if (!builder)
661
- return;
662
-
663
- const clientKey = clientName(job.client);
664
- let client = jobsForClient.get(clientKey);
665
- job.time = Date.now();
666
- // log("got job started", clientKey);
667
- if (!client) {
668
- client = new Map([["total", 1]]);
669
- jobsForClient.set(clientKey, client);
670
- } else {
671
- client.set("total", client.get("total") + 1);
672
- }
673
- delete job.type;
674
- client.set(job.id, job);
675
-
676
- jobs.set(job.id, job);
677
- ++builder.jobsPerformed;
678
- ++builder.active;
679
- update();
680
- }
681
-
682
- function deleteJob(job)
683
- {
684
- const clientKey = clientName(job.client);
685
- let client = jobsForClient.get(clientKey);
686
- if (client) {
687
- client.delete(job.id);
688
- if (client.size == 1) {
689
- jobsForClient.delete(clientKey);
690
- }
691
- }
692
- }
693
-
694
- function jobFinished(job)
695
- {
696
- const activejob = jobs.get(job.id);
697
- if (!activejob)
698
- return;
699
- jobs.delete(job.id);
700
-
701
- deleteJob(activejob);
702
-
703
- const key = `${activejob.builder.ip}:${activejob.builder.port}`;
704
- const builder = builders.get(key);
705
- if (!builder)
706
- return;
707
- --builder.active;
708
- update();
709
- }
710
-
711
- let ws;
712
-
713
- function send(msg)
714
- {
715
- if (typeof msg != "string") {
716
- ws.send(JSON.stringify(msg));
717
- } else {
718
- ws.send(msg);
719
- }
720
- }
721
-
722
- function connect()
723
- {
724
- const url = `${scheduler}/monitor`;
725
- notify(`connect ${url}`);
726
- ws = new WebSocket(url);
727
- ws.on("open", () => {
728
- notify("open");
729
- send({ type: "sendInfo" });
730
- });
731
- ws.on("error", err => {
732
- notify(`client websocket error ${err.message}`);
733
- });
734
- ws.on("message", msg => {
735
- //notify(`msg ${msg}`);
736
- let obj;
737
- try {
738
- obj = JSON.parse(msg);
739
- } catch (e) {
740
- notify(`msg parse error: ${msg}, ${e}`);
741
- }
742
- switch (obj.type) {
743
- case "builderAdded":
744
- builderAdded(obj);
745
- break;
746
- case "builderRemoved":
747
- builderRemoved(obj);
748
- break;
749
- case "jobStarted":
750
- jobStarted(obj);
751
- break;
752
- case "jobFinished":
753
- case "jobAborted":
754
- jobFinished(obj);
755
- break;
756
- default:
757
- //log(obj);
758
- break;
759
- }
760
- });
761
- ws.on("close", () => {
762
- clearData();
763
- setTimeout(connect, 1000);
764
- });
765
- }
766
-
767
- connect();