1ea37671dSMatthew Dillon /*
2ea37671dSMatthew Dillon * Copyright (c) 2019 The DragonFly Project. All rights reserved.
3ea37671dSMatthew Dillon *
4ea37671dSMatthew Dillon * This code is derived from software contributed to The DragonFly Project
5ea37671dSMatthew Dillon * by Matthew Dillon <dillon@backplane.com>
6ea37671dSMatthew Dillon *
7ea37671dSMatthew Dillon * This code uses concepts and configuration based on 'synth', by
8ea37671dSMatthew Dillon * John R. Marino <draco@marino.st>, which was written in ada.
9ea37671dSMatthew Dillon *
10ea37671dSMatthew Dillon * Redistribution and use in source and binary forms, with or without
11ea37671dSMatthew Dillon * modification, are permitted provided that the following conditions
12ea37671dSMatthew Dillon * are met:
13ea37671dSMatthew Dillon *
14ea37671dSMatthew Dillon * 1. Redistributions of source code must retain the above copyright
15ea37671dSMatthew Dillon * notice, this list of conditions and the following disclaimer.
16ea37671dSMatthew Dillon * 2. Redistributions in binary form must reproduce the above copyright
17ea37671dSMatthew Dillon * notice, this list of conditions and the following disclaimer in
18ea37671dSMatthew Dillon * the documentation and/or other materials provided with the
19ea37671dSMatthew Dillon * distribution.
20ea37671dSMatthew Dillon * 3. Neither the name of The DragonFly Project nor the names of its
21ea37671dSMatthew Dillon * contributors may be used to endorse or promote products derived
22ea37671dSMatthew Dillon * from this software without specific, prior written permission.
23ea37671dSMatthew Dillon *
24ea37671dSMatthew Dillon * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25ea37671dSMatthew Dillon * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26ea37671dSMatthew Dillon * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
27ea37671dSMatthew Dillon * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
28ea37671dSMatthew Dillon * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
29ea37671dSMatthew Dillon * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
30ea37671dSMatthew Dillon * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31ea37671dSMatthew Dillon * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
32ea37671dSMatthew Dillon * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
33ea37671dSMatthew Dillon * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
34ea37671dSMatthew Dillon * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
35ea37671dSMatthew Dillon * SUCH DAMAGE.
36ea37671dSMatthew Dillon */
37ea37671dSMatthew Dillon #include "dsynth.h"
38ea37671dSMatthew Dillon
39ea37671dSMatthew Dillon #include <curses.h>
40ea37671dSMatthew Dillon
41ea37671dSMatthew Dillon /*
42ea37671dSMatthew Dillon * ncurses - LINES, COLS are the main things we care about
43ea37671dSMatthew Dillon */
44ea37671dSMatthew Dillon static WINDOW *CWin;
45ea37671dSMatthew Dillon static WINDOW *CMon;
46ea37671dSMatthew Dillon static const char *Line0 = " Total - Built - Ignored - "
47ea37671dSMatthew Dillon "Load - Pkg/hour - ";
48ea37671dSMatthew Dillon static const char *Line1 = " Left - Failed - Skipped - "
49ea37671dSMatthew Dillon "Swap - Impulse - --:--:-- ";
50ea37671dSMatthew Dillon static const char *LineB = "==========================================="
51ea37671dSMatthew Dillon "====================================";
52ea37671dSMatthew Dillon static const char *LineI = " ID Duration Build Phase Origin "
53ea37671dSMatthew Dillon " Lines";
54ea37671dSMatthew Dillon
55ea37671dSMatthew Dillon static int LastReduce;
56aac7a6d9SMatthew Dillon static monitorlog_t nclog;
57ea37671dSMatthew Dillon
58ea37671dSMatthew Dillon #define TOTAL_COL 7
59ea37671dSMatthew Dillon #define BUILT_COL 21
60ea37671dSMatthew Dillon #define IGNORED_COL 36
61ea37671dSMatthew Dillon #define LOAD_COL 48
62ea37671dSMatthew Dillon #define GPKGRATE_COL 64
63ea37671dSMatthew Dillon #define REDUCE_COL 71
64ea37671dSMatthew Dillon
65ea37671dSMatthew Dillon #define LEFT_COL 7
66ea37671dSMatthew Dillon #define FAILED_COL 21
67ea37671dSMatthew Dillon #define SKIPPED_COL 36
68ea37671dSMatthew Dillon #define SWAP_COL 48
69ea37671dSMatthew Dillon #define IMPULSE_COL 64
70ea37671dSMatthew Dillon #define TIME_COL 71
71ea37671dSMatthew Dillon
72ea37671dSMatthew Dillon #define ID_COL 1
73ea37671dSMatthew Dillon #define DURATION_COL 5
74ea37671dSMatthew Dillon #define BUILD_PHASE_COL 15
75ea37671dSMatthew Dillon #define ORIGIN_COL 32
76d7cc0214SMatthew Dillon #define LINES_COL 72
77ea37671dSMatthew Dillon
78ea37671dSMatthew Dillon /*
79ea37671dSMatthew Dillon * The row that the worker list starts on, and the row that the log starts
80ea37671dSMatthew Dillon * on.
81ea37671dSMatthew Dillon */
82ea37671dSMatthew Dillon #define WORKER_START 5
83ea37671dSMatthew Dillon #define LOG_START (WORKER_START + MaxWorkers + 1)
84ea37671dSMatthew Dillon
85ea37671dSMatthew Dillon static void NCursesReset(void);
86ea37671dSMatthew Dillon
87ea37671dSMatthew Dillon static void
NCursesInit(void)88ea37671dSMatthew Dillon NCursesInit(void)
89ea37671dSMatthew Dillon {
90ea37671dSMatthew Dillon if (UseNCurses == 0)
91ea37671dSMatthew Dillon return;
92ea37671dSMatthew Dillon
93ea37671dSMatthew Dillon CWin = initscr();
94ea37671dSMatthew Dillon NCursesReset();
95ea37671dSMatthew Dillon
96ea37671dSMatthew Dillon intrflush(stdscr, FALSE);
97ea37671dSMatthew Dillon nonl();
98ea37671dSMatthew Dillon noecho();
99ea37671dSMatthew Dillon cbreak();
100ea37671dSMatthew Dillon
101ea37671dSMatthew Dillon start_color();
102ea37671dSMatthew Dillon use_default_colors();
103ea37671dSMatthew Dillon init_pair(1, COLOR_RED, -1);
104ea37671dSMatthew Dillon init_pair(2, COLOR_GREEN, -1);
105ea37671dSMatthew Dillon init_pair(3, -1, -1);
106ea37671dSMatthew Dillon }
107ea37671dSMatthew Dillon
108ea37671dSMatthew Dillon static void
NCursesReset(void)109ea37671dSMatthew Dillon NCursesReset(void)
110ea37671dSMatthew Dillon {
111ea37671dSMatthew Dillon int i;
112ea37671dSMatthew Dillon
113ea37671dSMatthew Dillon if (UseNCurses == 0)
114ea37671dSMatthew Dillon return;
115ea37671dSMatthew Dillon
116ea37671dSMatthew Dillon if (CMon) {
117ea37671dSMatthew Dillon delwin(CMon);
118ea37671dSMatthew Dillon CMon = NULL;
119ea37671dSMatthew Dillon }
120ea37671dSMatthew Dillon
121ea37671dSMatthew Dillon werase(CWin);
122ea37671dSMatthew Dillon curs_set(0);
123ea37671dSMatthew Dillon redrawwin(CWin);
124ea37671dSMatthew Dillon wrefresh(CWin);
125ea37671dSMatthew Dillon mvwprintw(CWin, 0, 0, "%s", Line0);
126ea37671dSMatthew Dillon mvwprintw(CWin, 1, 0, "%s", Line1);
127ea37671dSMatthew Dillon mvwprintw(CWin, 2, 0, "%s", LineB);
128ea37671dSMatthew Dillon mvwprintw(CWin, 3, 0, "%s", LineI);
129ea37671dSMatthew Dillon mvwprintw(CWin, 4, 0, "%s", LineB);
130ea37671dSMatthew Dillon
131ea37671dSMatthew Dillon for (i = 0; i < MaxWorkers; ++i) {
132ea37671dSMatthew Dillon mvwprintw(CWin, WORKER_START + i, ID_COL, "%02d", i);
133ea37671dSMatthew Dillon mvwprintw(CWin, WORKER_START + i, DURATION_COL, "--:--:--");
134ea37671dSMatthew Dillon mvwprintw(CWin, WORKER_START + i, BUILD_PHASE_COL, "Idle");
135d7cc0214SMatthew Dillon mvwprintw(CWin, WORKER_START + i, ORIGIN_COL, "%38.38s", "");
136d7cc0214SMatthew Dillon mvwprintw(CWin, WORKER_START + i, LINES_COL, "%7.7s", "");
137ea37671dSMatthew Dillon }
138ea37671dSMatthew Dillon mvwprintw(CWin, WORKER_START + MaxWorkers, 0, "%s", LineB);
139ea37671dSMatthew Dillon wrefresh(CWin);
140ea37671dSMatthew Dillon
141ea37671dSMatthew Dillon CMon = subwin(CWin, 0, 0, LOG_START, 0);
142ea37671dSMatthew Dillon scrollok(CMon, 1);
143aac7a6d9SMatthew Dillon
144aac7a6d9SMatthew Dillon bzero(&nclog, sizeof(nclog));
145aac7a6d9SMatthew Dillon nclog.fd = dlog00_fd();
146ea37671dSMatthew Dillon nodelay(CMon, 1);
147ea37671dSMatthew Dillon
148ea37671dSMatthew Dillon LastReduce = -1;
149ea37671dSMatthew Dillon }
150ea37671dSMatthew Dillon
151ea37671dSMatthew Dillon static void
NCursesUpdateTop(topinfo_t * info)152ea37671dSMatthew Dillon NCursesUpdateTop(topinfo_t *info)
153ea37671dSMatthew Dillon {
154ea37671dSMatthew Dillon if (UseNCurses == 0)
155ea37671dSMatthew Dillon return;
156ea37671dSMatthew Dillon
157*2295606eSMatthew Dillon mvwprintw(CWin, 0, TOTAL_COL, "%-6d", info->total);
158*2295606eSMatthew Dillon mvwprintw(CWin, 0, BUILT_COL, "%-6d", info->successful);
159*2295606eSMatthew Dillon mvwprintw(CWin, 0, IGNORED_COL, "%-6d", info->ignored);
160ea37671dSMatthew Dillon if (info->dload[0] > 999.9)
161ea37671dSMatthew Dillon mvwprintw(CWin, 0, LOAD_COL, "%5.0f", info->dload[0]);
162ea37671dSMatthew Dillon else
163ea37671dSMatthew Dillon mvwprintw(CWin, 0, LOAD_COL, "%5.1f", info->dload[0]);
164*2295606eSMatthew Dillon mvwprintw(CWin, 0, GPKGRATE_COL, "%-6d", info->pkgrate);
165ea37671dSMatthew Dillon
166ea37671dSMatthew Dillon /*
167ea37671dSMatthew Dillon * If dynamic worker reduction is active include a field,
168ea37671dSMatthew Dillon * Otherwise blank the field.
169ea37671dSMatthew Dillon */
170aac7a6d9SMatthew Dillon if (LastReduce != info->dynmaxworkers) {
171aac7a6d9SMatthew Dillon LastReduce = info->dynmaxworkers;
172ea37671dSMatthew Dillon if (MaxWorkers == LastReduce)
173ea37671dSMatthew Dillon mvwprintw(CWin, 0, REDUCE_COL, " ");
174ea37671dSMatthew Dillon else
1754986398eSMatthew Dillon mvwprintw(CWin, 0, REDUCE_COL, "Lim %-3d",
176ea37671dSMatthew Dillon LastReduce);
177ea37671dSMatthew Dillon }
178ea37671dSMatthew Dillon
179*2295606eSMatthew Dillon mvwprintw(CWin, 1, LEFT_COL, "%-6d", info->remaining);
180*2295606eSMatthew Dillon mvwprintw(CWin, 1, FAILED_COL, "%-6d", info->failed);
181*2295606eSMatthew Dillon mvwprintw(CWin, 1, SKIPPED_COL, "%-6d", info->skipped);
182ea37671dSMatthew Dillon if (info->noswap)
183ea37671dSMatthew Dillon mvwprintw(CWin, 1, SWAP_COL, "- ");
184ea37671dSMatthew Dillon else
185ea37671dSMatthew Dillon mvwprintw(CWin, 1, SWAP_COL, "%5.1f", info->dswap);
186*2295606eSMatthew Dillon mvwprintw(CWin, 1, IMPULSE_COL, "%-6d", info->pkgimpulse);
187ea37671dSMatthew Dillon if (info->h > 99)
188ea37671dSMatthew Dillon mvwprintw(CWin, 1, TIME_COL-1, "%3d:%02d:%02d",
189ea37671dSMatthew Dillon info->h, info->m, info->s);
190ea37671dSMatthew Dillon else
191ea37671dSMatthew Dillon mvwprintw(CWin, 1, TIME_COL, "%02d:%02d:%02d",
192ea37671dSMatthew Dillon info->h, info->m, info->s);
193ea37671dSMatthew Dillon }
194ea37671dSMatthew Dillon
195ea37671dSMatthew Dillon static void
NCursesUpdateLogs(void)196ea37671dSMatthew Dillon NCursesUpdateLogs(void)
197ea37671dSMatthew Dillon {
198ea37671dSMatthew Dillon char *ptr;
199ea37671dSMatthew Dillon char c;
200ea37671dSMatthew Dillon ssize_t n;
201ea37671dSMatthew Dillon int w;
202ea37671dSMatthew Dillon
203ea37671dSMatthew Dillon if (UseNCurses == 0)
204ea37671dSMatthew Dillon return;
205ea37671dSMatthew Dillon
206ea37671dSMatthew Dillon for (;;) {
207aac7a6d9SMatthew Dillon n = readlogline(&nclog, &ptr);
208ea37671dSMatthew Dillon if (n < 0)
209ea37671dSMatthew Dillon break;
210ea37671dSMatthew Dillon if (n == 0)
211ea37671dSMatthew Dillon continue;
212ea37671dSMatthew Dillon
213ea37671dSMatthew Dillon /*
214ea37671dSMatthew Dillon * Scroll down
215ea37671dSMatthew Dillon */
216ea37671dSMatthew Dillon if (n > COLS)
217ea37671dSMatthew Dillon w = COLS;
218ea37671dSMatthew Dillon else
219ea37671dSMatthew Dillon w = n;
220ea37671dSMatthew Dillon c = ptr[w];
221ea37671dSMatthew Dillon ptr[w] = 0;
222ea37671dSMatthew Dillon
223ea37671dSMatthew Dillon /*
224ea37671dSMatthew Dillon * Filter out these logs from the display (they remain in
225ea37671dSMatthew Dillon * the 00*.log file) to reduce clutter.
226ea37671dSMatthew Dillon */
227aac7a6d9SMatthew Dillon if (strncmp(ptr, "[XXX] Load=", 11) != 0) {
228ea37671dSMatthew Dillon /*
229ea37671dSMatthew Dillon * Output possibly colored log line
230ea37671dSMatthew Dillon */
231ea37671dSMatthew Dillon wscrl(CMon, -1);
232ea37671dSMatthew Dillon if (strstr(ptr, "] SUCCESS ")) {
233ea37671dSMatthew Dillon wattrset(CMon, COLOR_PAIR(2));
234ea37671dSMatthew Dillon } else if (strstr(ptr, "] FAILURE ")) {
235ea37671dSMatthew Dillon wattrset(CMon, COLOR_PAIR(1));
236ea37671dSMatthew Dillon }
237ea37671dSMatthew Dillon mvwprintw(CMon, 0, 0, "%s", ptr);
238ea37671dSMatthew Dillon wattrset(CMon, COLOR_PAIR(3));
239ea37671dSMatthew Dillon }
240aac7a6d9SMatthew Dillon ptr[w] = c;
241aac7a6d9SMatthew Dillon }
242ea37671dSMatthew Dillon }
243ea37671dSMatthew Dillon
244ea37671dSMatthew Dillon static void
NCursesUpdate(worker_t * work,const char * portdir)245aac7a6d9SMatthew Dillon NCursesUpdate(worker_t *work, const char *portdir)
246ea37671dSMatthew Dillon {
247ea37671dSMatthew Dillon const char *phase;
248ea37671dSMatthew Dillon const char *origin;
249ea37671dSMatthew Dillon time_t t;
250ea37671dSMatthew Dillon int i = work->index;
251ea37671dSMatthew Dillon int h;
252ea37671dSMatthew Dillon int m;
253ea37671dSMatthew Dillon int s;
254ea37671dSMatthew Dillon
255ea37671dSMatthew Dillon if (UseNCurses == 0)
256ea37671dSMatthew Dillon return;
257ea37671dSMatthew Dillon
258ea37671dSMatthew Dillon phase = "Unknown";
259ea37671dSMatthew Dillon origin = "";
260ea37671dSMatthew Dillon
261ea37671dSMatthew Dillon switch(work->state) {
262ea37671dSMatthew Dillon case WORKER_NONE:
263ea37671dSMatthew Dillon phase = "None";
264ea37671dSMatthew Dillon /* fall through */
265ea37671dSMatthew Dillon case WORKER_IDLE:
266ea37671dSMatthew Dillon if (work->state == WORKER_IDLE)
267ea37671dSMatthew Dillon phase = "Idle";
268ea37671dSMatthew Dillon /* fall through */
269ea37671dSMatthew Dillon case WORKER_FAILED:
270ea37671dSMatthew Dillon if (work->state == WORKER_FAILED)
271ea37671dSMatthew Dillon phase = "Failed";
272ea37671dSMatthew Dillon /* fall through */
273ea37671dSMatthew Dillon case WORKER_EXITING:
274ea37671dSMatthew Dillon if (work->state == WORKER_EXITING)
275ea37671dSMatthew Dillon phase = "Exiting";
276ea37671dSMatthew Dillon mvwprintw(CWin, WORKER_START + i, DURATION_COL,
277ea37671dSMatthew Dillon "--:--:--");
278ea37671dSMatthew Dillon mvwprintw(CWin, WORKER_START + i, BUILD_PHASE_COL,
279ea37671dSMatthew Dillon "%-16.16s", phase);
280ea37671dSMatthew Dillon mvwprintw(CWin, WORKER_START + i, ORIGIN_COL,
281d7cc0214SMatthew Dillon "%-38.38s", "");
282ea37671dSMatthew Dillon mvwprintw(CWin, WORKER_START + i, LINES_COL,
283d7cc0214SMatthew Dillon "%-7.7s", "");
284ea37671dSMatthew Dillon return;
285ea37671dSMatthew Dillon case WORKER_PENDING:
286ea37671dSMatthew Dillon phase = "Pending";
287ea37671dSMatthew Dillon break;
288ea37671dSMatthew Dillon case WORKER_RUNNING:
289ea37671dSMatthew Dillon phase = "Running";
290ea37671dSMatthew Dillon break;
291ea37671dSMatthew Dillon case WORKER_DONE:
292ea37671dSMatthew Dillon phase = "Done";
293ea37671dSMatthew Dillon break;
294ea37671dSMatthew Dillon case WORKER_FROZEN:
295ea37671dSMatthew Dillon phase = "FROZEN";
296ea37671dSMatthew Dillon break;
297ea37671dSMatthew Dillon default:
298ea37671dSMatthew Dillon break;
299ea37671dSMatthew Dillon }
300ea37671dSMatthew Dillon
301ea37671dSMatthew Dillon t = time(NULL) - work->start_time;
302ea37671dSMatthew Dillon s = t % 60;
303ea37671dSMatthew Dillon m = t / 60 % 60;
304ea37671dSMatthew Dillon h = t / 60 / 60;
305ea37671dSMatthew Dillon
306ea37671dSMatthew Dillon if (work->state == WORKER_RUNNING)
307ea37671dSMatthew Dillon phase = getphasestr(work->phase);
308ea37671dSMatthew Dillon
309aac7a6d9SMatthew Dillon /*
310aac7a6d9SMatthew Dillon * When called from the monitor frontend portdir has to be passed
311aac7a6d9SMatthew Dillon * in directly because work->pkg is not mapped.
312aac7a6d9SMatthew Dillon */
313aac7a6d9SMatthew Dillon if (portdir)
314aac7a6d9SMatthew Dillon origin = portdir;
315aac7a6d9SMatthew Dillon else if (work->pkg)
316ea37671dSMatthew Dillon origin = work->pkg->portdir;
317ea37671dSMatthew Dillon else
318ea37671dSMatthew Dillon origin = "";
319ea37671dSMatthew Dillon
320ea37671dSMatthew Dillon mvwprintw(CWin, WORKER_START + i, DURATION_COL,
321ea37671dSMatthew Dillon "%02d:%02d:%02d", h, m, s);
322ea37671dSMatthew Dillon mvwprintw(CWin, WORKER_START + i, BUILD_PHASE_COL,
323ea37671dSMatthew Dillon "%-16.16s", phase);
324ea37671dSMatthew Dillon mvwprintw(CWin, WORKER_START + i, ORIGIN_COL,
325d7cc0214SMatthew Dillon "%-38.38s", origin);
326d7cc0214SMatthew Dillon if (work->lines > 9999999) {
327ea37671dSMatthew Dillon mvwprintw(CWin, WORKER_START + i, LINES_COL,
328d7cc0214SMatthew Dillon "%7s", "*MANY*%d", work->lines % 10);
329d7cc0214SMatthew Dillon } else {
330d7cc0214SMatthew Dillon mvwprintw(CWin, WORKER_START + i, LINES_COL,
331d7cc0214SMatthew Dillon "%7d", work->lines);
332d7cc0214SMatthew Dillon }
333ea37671dSMatthew Dillon }
334ea37671dSMatthew Dillon
335ea37671dSMatthew Dillon static void
NCursesSync(void)336ea37671dSMatthew Dillon NCursesSync(void)
337ea37671dSMatthew Dillon {
338ea37671dSMatthew Dillon int c;
339ea37671dSMatthew Dillon
340ea37671dSMatthew Dillon if (UseNCurses == 0)
341ea37671dSMatthew Dillon return;
342ea37671dSMatthew Dillon
343ea37671dSMatthew Dillon while ((c = wgetch(CMon)) != ERR) {
344ea37671dSMatthew Dillon if (c == KEY_RESIZE)
345ea37671dSMatthew Dillon NCursesReset();
346ea37671dSMatthew Dillon }
347ea37671dSMatthew Dillon wrefresh(CWin);
348ea37671dSMatthew Dillon wrefresh(CMon);
349ea37671dSMatthew Dillon }
350ea37671dSMatthew Dillon
351ea37671dSMatthew Dillon static void
NCursesDone(void)352ea37671dSMatthew Dillon NCursesDone(void)
353ea37671dSMatthew Dillon {
354ea37671dSMatthew Dillon if (UseNCurses == 0)
355ea37671dSMatthew Dillon return;
356ea37671dSMatthew Dillon
357ea37671dSMatthew Dillon endwin();
358ea37671dSMatthew Dillon }
359ea37671dSMatthew Dillon
360ea37671dSMatthew Dillon runstats_t NCursesRunStats = {
361ea37671dSMatthew Dillon .init = NCursesInit,
362ea37671dSMatthew Dillon .done = NCursesDone,
363ea37671dSMatthew Dillon .reset = NCursesReset,
364ea37671dSMatthew Dillon .update = NCursesUpdate,
365ea37671dSMatthew Dillon .updateTop = NCursesUpdateTop,
366ea37671dSMatthew Dillon .updateLogs = NCursesUpdateLogs,
367ea37671dSMatthew Dillon .sync = NCursesSync
368ea37671dSMatthew Dillon };
369