xref: /dragonfly/usr.bin/dsynth/ncurses.c (revision 7c4f4eee)
1 /*
2  * Copyright (c) 2019 The DragonFly Project.  All rights reserved.
3  *
4  * This code is derived from software contributed to The DragonFly Project
5  * by Matthew Dillon <dillon@backplane.com>
6  *
7  * This code uses concepts and configuration based on 'synth', by
8  * John R. Marino <draco@marino.st>, which was written in ada.
9  *
10  * Redistribution and use in source and binary forms, with or without
11  * modification, are permitted provided that the following conditions
12  * are met:
13  *
14  * 1. Redistributions of source code must retain the above copyright
15  *    notice, this list of conditions and the following disclaimer.
16  * 2. Redistributions in binary form must reproduce the above copyright
17  *    notice, this list of conditions and the following disclaimer in
18  *    the documentation and/or other materials provided with the
19  *    distribution.
20  * 3. Neither the name of The DragonFly Project nor the names of its
21  *    contributors may be used to endorse or promote products derived
22  *    from this software without specific, prior written permission.
23  *
24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
27  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
28  * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
29  * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
30  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
32  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
33  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
34  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
35  * SUCH DAMAGE.
36  */
37 #include "dsynth.h"
38 
39 #include <curses.h>
40 
41 /*
42  * ncurses - LINES, COLS are the main things we care about
43  */
44 static WINDOW *CWin;
45 static WINDOW *CMon;
46 static const char *Line0 = " Total -       Built -      Ignored -      "
47 			   "Load -      Pkg/hour -               ";
48 static const char *Line1 = "  Left -      Failed -      Skipped -      "
49 			   "Swap -       Impulse -      --:--:-- ";
50 static const char *LineB = "==========================================="
51 			   "====================================";
52 static const char *LineI = " ID  Duration  Build Phase      Origin     "
53 			   "                               Lines";
54 
55 static int LastReduce;
56 static off_t MonitorLogOff;
57 static int MonitorLogLines;
58 static int MonitorBufBeg;
59 static int MonitorBufEnd;
60 static int MonitorBufScan;
61 static int MonitorBufDiscardMode;
62 static char MonitorBuf[1024];
63 
64 static int guireadline(int fd, char **bufp);
65 
66 #define TOTAL_COL	7
67 #define BUILT_COL	21
68 #define IGNORED_COL	36
69 #define LOAD_COL	48
70 #define GPKGRATE_COL	64
71 #define REDUCE_COL	71
72 
73 #define LEFT_COL	7
74 #define FAILED_COL	21
75 #define SKIPPED_COL	36
76 #define SWAP_COL	48
77 #define IMPULSE_COL	64
78 #define TIME_COL	71
79 
80 #define ID_COL		1
81 #define DURATION_COL	5
82 #define BUILD_PHASE_COL	15
83 #define ORIGIN_COL	32
84 #define LINES_COL	73
85 
86 /*
87  * The row that the worker list starts on, and the row that the log starts
88  * on.
89  */
90 #define WORKER_START	5
91 #define LOG_START	(WORKER_START + MaxWorkers + 1)
92 
93 static void NCursesReset(void);
94 
95 static void
96 NCursesInit(void)
97 {
98 	if (UseNCurses == 0)
99 		return;
100 
101 	CWin = initscr();
102 	NCursesReset();
103 
104 	intrflush(stdscr, FALSE);
105 	nonl();
106 	noecho();
107 	cbreak();
108 
109 	start_color();
110 	use_default_colors();
111 	init_pair(1, COLOR_RED, -1);
112 	init_pair(2, COLOR_GREEN, -1);
113 	init_pair(3, -1, -1);
114 }
115 
116 static void
117 NCursesReset(void)
118 {
119 	int i;
120 
121 	if (UseNCurses == 0)
122 		return;
123 
124 	if (CMon) {
125 		delwin(CMon);
126 		CMon = NULL;
127 	}
128 
129 	werase(CWin);
130 	curs_set(0);
131 	redrawwin(CWin);
132 	wrefresh(CWin);
133 	mvwprintw(CWin, 0, 0, "%s", Line0);
134 	mvwprintw(CWin, 1, 0, "%s", Line1);
135 	mvwprintw(CWin, 2, 0, "%s", LineB);
136 	mvwprintw(CWin, 3, 0, "%s", LineI);
137 	mvwprintw(CWin, 4, 0, "%s", LineB);
138 
139 	for (i = 0; i < MaxWorkers; ++i) {
140 		mvwprintw(CWin, WORKER_START + i, ID_COL, "%02d", i);
141 		mvwprintw(CWin, WORKER_START + i, DURATION_COL, "--:--:--");
142 		mvwprintw(CWin, WORKER_START + i, BUILD_PHASE_COL, "Idle");
143 		mvwprintw(CWin, WORKER_START + i, ORIGIN_COL, "%39.39s", "");
144 		mvwprintw(CWin, WORKER_START + i, LINES_COL, "%6.6s", "");
145 	}
146 	mvwprintw(CWin, WORKER_START + MaxWorkers, 0, "%s", LineB);
147 	wrefresh(CWin);
148 
149 	CMon = subwin(CWin, 0, 0, LOG_START, 0);
150 	scrollok(CMon, 1);
151 	MonitorLogLines = LINES - LOG_START;
152 	MonitorLogOff = 0;
153 	MonitorBufBeg = 0;
154 	MonitorBufEnd = 0;
155 	MonitorBufScan = 0;
156 	MonitorBufDiscardMode = 0;
157 	nodelay(CMon, 1);
158 
159 	LastReduce = -1;
160 }
161 
162 static void
163 NCursesUpdateTop(topinfo_t *info)
164 {
165 	if (UseNCurses == 0)
166 		return;
167 
168 	mvwprintw(CWin, 0, TOTAL_COL, "%-5d", info->total);
169 	mvwprintw(CWin, 0, BUILT_COL, "%-5d", info->successful);
170 	mvwprintw(CWin, 0, IGNORED_COL, "%-5d", info->ignored);
171 	if (info->dload[0] > 999.9)
172 		mvwprintw(CWin, 0, LOAD_COL, "%5.0f", info->dload[0]);
173 	else
174 		mvwprintw(CWin, 0, LOAD_COL, "%5.1f", info->dload[0]);
175 	mvwprintw(CWin, 0, GPKGRATE_COL, "%-5d", info->pkgrate);
176 
177 	/*
178 	 * If dynamic worker reduction is active include a field,
179 	 * Otherwise blank the field.
180 	 */
181 	if (LastReduce != DynamicMaxWorkers) {
182 		LastReduce = DynamicMaxWorkers;
183 		if (MaxWorkers == LastReduce)
184 			mvwprintw(CWin, 0, REDUCE_COL, "        ");
185 		else
186 			mvwprintw(CWin, 0, REDUCE_COL, "Limit %-2d",
187 				  LastReduce);
188 	}
189 
190 	mvwprintw(CWin, 1, LEFT_COL, "%-5d", info->remaining);
191 	mvwprintw(CWin, 1, FAILED_COL, "%-5d", info->failed);
192 	mvwprintw(CWin, 1, SKIPPED_COL, "%-5d", info->skipped);
193 	if (info->noswap)
194 		mvwprintw(CWin, 1, SWAP_COL, "-   ");
195 	else
196 		mvwprintw(CWin, 1, SWAP_COL, "%5.1f", info->dswap);
197 	mvwprintw(CWin, 1, IMPULSE_COL, "%-5d", info->pkgimpulse);
198 	if (info->h > 99)
199 		mvwprintw(CWin, 1, TIME_COL-1, "%3d:%02d:%02d",
200 			  info->h, info->m, info->s);
201 	else
202 		mvwprintw(CWin, 1, TIME_COL, "%02d:%02d:%02d",
203 			  info->h, info->m, info->s);
204 }
205 
206 static void
207 NCursesUpdateLogs(void)
208 {
209 	int fd = dlog00_fd();
210 	char *ptr;
211 	char c;
212 	ssize_t n;
213 	int w;
214 
215 	if (UseNCurses == 0)
216 		return;
217 
218 	for (;;) {
219 		n = guireadline(fd, &ptr);
220 		if (n < 0)
221 			break;
222 		if (n == 0)
223 			continue;
224 
225 		/*
226 		 * Scroll down
227 		 */
228 		if (n > COLS)
229 			w = COLS;
230 		else
231 			w = n;
232 		c = ptr[w];
233 		ptr[w] = 0;
234 
235 		/*
236 		 * Filter out these logs from the display (they remain in
237 		 * the 00*.log file) to reduce clutter.
238 		 */
239 		if (strncmp(ptr, "[XXX] Load=", 11) == 0)
240 			continue;
241 
242 		/*
243 		 * Output possibly colored log line
244 		 */
245 		wscrl(CMon, -1);
246 		if (strstr(ptr, "] SUCCESS ")) {
247 			wattrset(CMon, COLOR_PAIR(2));
248 		} else if (strstr(ptr, "] FAILURE ")) {
249 			wattrset(CMon, COLOR_PAIR(1));
250 		}
251 		mvwprintw(CMon, 0, 0, "%s", ptr);
252 		ptr[w] = c;
253 		wattrset(CMon, COLOR_PAIR(3));
254 	}
255 }
256 
257 static void
258 NCursesUpdate(worker_t *work)
259 {
260 	const char *phase;
261 	const char *origin;
262 	time_t t;
263 	int i = work->index;
264 	int h;
265 	int m;
266 	int s;
267 
268 	if (UseNCurses == 0)
269 		return;
270 
271 	phase = "Unknown";
272 	origin = "";
273 
274 	switch(work->state) {
275 	case WORKER_NONE:
276 		phase = "None";
277 		/* fall through */
278 	case WORKER_IDLE:
279 		if (work->state == WORKER_IDLE)
280 			phase = "Idle";
281 		/* fall through */
282 	case WORKER_FAILED:
283 		if (work->state == WORKER_FAILED)
284 			phase = "Failed";
285 		/* fall through */
286 	case WORKER_EXITING:
287 		if (work->state == WORKER_EXITING)
288 			phase = "Exiting";
289 		mvwprintw(CWin, WORKER_START + i, DURATION_COL,
290 			  "--:--:--");
291 		mvwprintw(CWin, WORKER_START + i, BUILD_PHASE_COL,
292 			  "%-16.16s", phase);
293 		mvwprintw(CWin, WORKER_START + i, ORIGIN_COL,
294 			  "%-39.39s", "");
295 		mvwprintw(CWin, WORKER_START + i, LINES_COL,
296 			  "%-6.6s", "");
297 		return;
298 	case WORKER_PENDING:
299 		phase = "Pending";
300 		break;
301 	case WORKER_RUNNING:
302 		phase = "Running";
303 		break;
304 	case WORKER_DONE:
305 		phase = "Done";
306 		break;
307 	case WORKER_FROZEN:
308 		phase = "FROZEN";
309 		break;
310 	default:
311 		break;
312 	}
313 
314 	t = time(NULL) - work->start_time;
315 	s = t % 60;
316 	m = t / 60 % 60;
317 	h = t / 60 / 60;
318 
319 	if (work->state == WORKER_RUNNING)
320 		phase = getphasestr(work->phase);
321 
322 	if (work->pkg)
323 		origin = work->pkg->portdir;
324 	else
325 		origin = "";
326 
327 	mvwprintw(CWin, WORKER_START + i, DURATION_COL,
328 		  "%02d:%02d:%02d", h, m, s);
329 	mvwprintw(CWin, WORKER_START + i, BUILD_PHASE_COL,
330 		  "%-16.16s", phase);
331 	mvwprintw(CWin, WORKER_START + i, ORIGIN_COL,
332 		  "%-39.39s", origin);
333 	mvwprintw(CWin, WORKER_START + i, LINES_COL,
334 		  "%6d", work->lines);
335 }
336 
337 static void
338 NCursesSync(void)
339 {
340 	int c;
341 
342 	if (UseNCurses == 0)
343 		return;
344 
345 	while ((c = wgetch(CMon)) != ERR) {
346 		if (c == KEY_RESIZE)
347 			NCursesReset();
348 	}
349 	wrefresh(CWin);
350 	wrefresh(CMon);
351 }
352 
353 static void
354 NCursesDone(void)
355 {
356 	if (UseNCurses == 0)
357 		return;
358 
359 	endwin();
360 }
361 
362 static int
363 guireadline(int fd, char **bufp)
364 {
365 	int r;
366 	int n;
367 
368 	/*
369 	 * Reset buffer as an optimization to avoid unnecessary
370 	 * shifts.
371 	 */
372 	*bufp = NULL;
373 	if (MonitorBufBeg == MonitorBufEnd) {
374 		MonitorBufBeg = 0;
375 		MonitorBufEnd = 0;
376 		MonitorBufScan = 0;
377 	}
378 
379 	/*
380 	 * Look for newline, handle discard mode
381 	 */
382 again:
383 	for (n = MonitorBufScan; n < MonitorBufEnd; ++n) {
384 		if (MonitorBuf[n] == '\n') {
385 			*bufp = MonitorBuf + MonitorBufBeg;
386 			r = n - MonitorBufBeg;
387 			MonitorBufBeg = n + 1;
388 			MonitorBufScan = n + 1;
389 
390 			if (MonitorBufDiscardMode == 0)
391 				return r;
392 			MonitorBufDiscardMode = 0;
393 			goto again;
394 		}
395 	}
396 
397 	/*
398 	 * Handle overflow
399 	 */
400 	if (n == sizeof(MonitorBuf)) {
401 		if (MonitorBufBeg) {
402 			/*
403 			 * Shift the buffer to make room and read more data.
404 			 */
405 			bcopy(MonitorBuf + MonitorBufBeg,
406 			      MonitorBuf,
407 			      n - MonitorBufBeg);
408 			MonitorBufEnd -= MonitorBufBeg;
409 			MonitorBufScan -= MonitorBufBeg;
410 			n -= MonitorBufBeg;
411 			MonitorBufBeg = 0;
412 		} else if (MonitorBufDiscardMode) {
413 			/*
414 			 * Overflow.  If in discard mode just throw it all
415 			 * away.  Stay in discard mode.
416 			 */
417 			MonitorBufBeg = 0;
418 			MonitorBufEnd = 0;
419 			MonitorBufScan = 0;
420 		} else {
421 			/*
422 			 * Overflow.  If not in discard mode return a truncated
423 			 * line and enter discard mode.
424 			 *
425 			 * The caller will temporarily set ptr[r] = 0 so make
426 			 * sure that does not overflow our buffer as we are not
427 			 * at a newline.
428 			 *
429 			 * (MonitorBufBeg is 0);
430 			 */
431 			*bufp = MonitorBuf + MonitorBufBeg;
432 			r = n - 1;
433 			MonitorBufBeg = n;
434 			MonitorBufScan = n;
435 			MonitorBufDiscardMode = 1;
436 
437 			return r;
438 		}
439 	}
440 
441 	/*
442 	 * Read more data.  If there is no data pending then return -1,
443 	 * otherwise loop up to see if more complete line(s) are available.
444 	 */
445 	r = pread(fd,
446 		  MonitorBuf + MonitorBufEnd,
447 		  sizeof(MonitorBuf) - MonitorBufEnd,
448 		  MonitorLogOff);
449 	if (r <= 0)
450 		return -1;
451 	MonitorLogOff += r;
452 	MonitorBufEnd += r;
453 	goto again;
454 }
455 
456 runstats_t NCursesRunStats = {
457 	.init = NCursesInit,
458 	.done = NCursesDone,
459 	.reset = NCursesReset,
460 	.update = NCursesUpdate,
461 	.updateTop = NCursesUpdateTop,
462 	.updateLogs = NCursesUpdateLogs,
463 	.sync = NCursesSync
464 };
465