1 /*
2  * Copyright 2015-2021 The Regents of the University of California
3  * All rights reserved.
4  *
5  * This file is part of Spoofer.
6  *
7  * Spoofer is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * Spoofer is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with Spoofer.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 // Disable some Qt features we don't need to speed up compilation and avoid
22 // some compiler warnings.
23 #define QT_NO_MATRIX4X4
24 #define QT_NO_VECTOR3D
25 #define QT_NO_VECTOR4D
26 #define QT_NO_QUATERNION
27 
28 #include <time.h>
29 
30 #include "spoof_qt.h"
31 #include <QtGlobal>
32 #include <QAction>
33 #include <QtWidgets>
34 #include <QFile>
35 #include <QTextStream>
36 #include <QFileSystemWatcher>
37 #include <QSystemTrayIcon>
38 #include <QTableWidget>
39 #include <QUrl>
40 #include <QHostAddress>
41 
42 #include "../../config.h"
43 #include "port.h"
44 #include "mainwindow.h"
45 #include "FileTailThread.h"
46 #include "ActionButton.h"
47 #include "PreferencesDialog.h"
48 #include "ColoredLabel.h"
49 #include "SubnetAddr.h"
50 static const char cvsid[] ATR_USED = "$Id: mainwindow.cpp,v 1.224 2021/04/28 17:39:09 kkeys Exp $";
51 
52 #define HISTORYTABLE_BGCOLOR "#EEEEEE"
53 
54 MainWindow::WindowOutput *MainWindow::winout = nullptr;
55 MainWindow *MainWindow::theInstance = nullptr;
56 QPlainTextEdit *MainWindow::consoleWidget = nullptr;
57 ActionButton *MainWindow::consoleButton = nullptr;
58 QString MainWindow::mainTitle(QSL("Spoofer Manager GUI"));
59 
60 class SessionResults {
61 public:
62     int row;
63     int ipv;
64     uint32_t asn;
65     QString ipaddr;
66     QString routable;
67     QString privaddr;
68     QString ininternal;
69     QString inprivaddr;
SessionResults(int _ipv)70     SessionResults(int _ipv) : row(-1), ipv(_ipv), asn(0), ipaddr(),
71 	routable(), privaddr(), ininternal(), inprivaddr()
72 	{ }
73 };
74 
75 class RunResults {
76     RunResults(const RunResults&) NO_METHOD; // no copy-ctor
77     RunResults operator=(const RunResults&) NO_METHOD; // no copy-assign
78 public:
79     bool live;
80     time_t started;
81     QString logname;
82     QString report;
83     bool proberVersionMatch;
84     SessionResults *results4;
85     SessionResults *results6;
RunResults(bool _live,const QString & _logname)86     RunResults(bool _live, const QString &_logname) :
87 	live(_live), started(_live ? time(nullptr) : 0), logname(_logname),
88 	report(), proberVersionMatch(false), results4(), results6()
89 	{
90 	    static QRegularExpression re(SpooferBase::proberLogRegex);
91 	    QRegularExpressionMatch match = re.match(logname);
92 	    if (match.hasMatch()) {
93 		int year = match.captured(1).toInt();
94 		int mon = match.captured(2).toInt();
95 		int day = match.captured(3).toInt();
96 		int hour = match.captured(4).toInt();
97 		int min = match.captured(5).toInt();
98 		int sec = match.captured(6).toInt();
99 		QDateTime datetime(QDate(year, mon, day), QTime(hour, min, sec), Qt::UTC);
100 		started = static_cast<time_t>(datetime.toTime_t());
101 	    }
102 	}
~RunResults()103     ~RunResults() {
104 	if (results4) delete results4;
105 	if (results6) delete results6;
106     }
107     void parseProberText(const QString &text, bool showMessages = true);
108     void findResults(const QString &ipv, ProgressRow **progRow, SessionResults **results);
109 };
110 
111 class LinkWidget : public ColoredLabel {
112 public:
LinkWidget(const QUrl & _url,const QString & _text,QWidget * _parent=nullptr)113     LinkWidget(const QUrl &_url, const QString &_text,
114 	QWidget *_parent = nullptr) :
115 	ColoredLabel(QSL("<a href='") % _url.toString(QUrl::FullyEncoded) %
116 	    QSL("'>") % _text.toHtmlEscaped() % QSL("</a>"), _parent)
117     {
118 	this->setTextInteractionFlags(Qt::LinksAccessibleByMouse |
119 	    Qt::LinksAccessibleByKeyboard);
120 	this->setOpenExternalLinks(true);
121 	this->setStatusTip(_url.toString());
122     }
123 };
124 
125 template<class T>
126 class TableRow {
127 public:
128     QLabel *title;
129     T *content;
TableRow(QGridLayout * grid,int rownum,const QString & _title,T * _content)130     TableRow(QGridLayout *grid, int rownum, const QString &_title, T *_content) :
131 	title(new QLabel(_title)), content(_content)
132     {
133 	grid->addWidget(title, rownum, 0, Qt::AlignTop);
134 	grid->addWidget(content, rownum, 1, Qt::AlignTop);
135     }
show()136     void show() {
137 	title->show();
138 	content->show();
139     }
140 };
141 
retainSizeWhenHidden(QWidget * w)142 static void retainSizeWhenHidden(QWidget *w)
143 {
144     QSizePolicy sp;
145     sp = w->sizePolicy();
146     sp.setRetainSizeWhenHidden(true);
147     w->setSizePolicy(sp);
148 }
149 
150 class ProgressRow : public TableRow<QWidget> {
151     // +---------+-------------------+
152     // | title   | content---------+ |
153     // |         | | text          | |
154     // |         | | progbar       | |
155     // |         | +---------------+ |
156     // +---------+-------------------+
157     ProgressRow(const ProgressRow&) NO_METHOD; // no copy-ctor
158     ProgressRow operator=(const ProgressRow&) NO_METHOD; // no copy-assign
159 public:
160     QString name;
161     QLabel *text;
162     QProgressBar *bar;
163     SessionResults *results;
ProgressRow(QGridLayout * grid,int rownum,QString _name)164     ProgressRow(QGridLayout *grid, int rownum, QString _name) :
165 	TableRow(grid, rownum, _name, new QWidget()),
166 	name(_name), text(), bar(), results()
167     {
168 	QVBoxLayout *vbox = new QVBoxLayout();
169 	content->setLayout(vbox);
170 	vbox->setContentsMargins(0,0,0,0);
171 	vbox->addWidget(text = new QLabel(QSL("no results yet")));
172 	vbox->addWidget(bar = new QProgressBar());
173 	bar->setFormat(QSL("%v/%m"));
174 	retainSizeWhenHidden(bar);
175 	retainSizeWhenHidden(text);
176 	retainSizeWhenHidden(title);
177 	bar->hide();
178 	text->hide();
179 	title->hide();
180     }
start()181     void start() {
182 	text->setText(QSL("untested"));
183 	bar->reset();
184 	bar->setMaximum(0);
185 	bar->setValue(0);
186 	bar->hide();
187 	text->show();
188 	title->show();
189 	show();
190     }
stage(const QString & _text)191     void stage(const QString &_text) {
192 	text->setText(_text);
193 	bar->show();
194 	text->show();
195 	title->show();
196     }
tick(int progress,int goal)197     void tick(int progress, int goal) {
198 	bar->setMaximum(goal);
199 	bar->setValue(progress);
200     }
endSession()201     void endSession() {
202 	text->setText(!results ? QSL("incomplete") : QSL("done"));
203     }
endProber()204     void endProber() {
205 	bar->hide();
206 	text->hide();
207 	title->hide();
208 	results = nullptr;
209     }
210 };
211 
writeData(const char * data,qint64 maxSize)212 qint64 MainWindow::WindowOutput::writeData(const char *data, qint64 maxSize)
213 {
214     // Note: on Windows, this is called twice for each line of text: first with
215     // the visible text, and then with "\r\n".
216     QScrollBar *sb = _textedit->verticalScrollBar();
217     bool wasNearBottom = (sb->value() >= sb->maximum() - 5); // exact equality doesn't work
218 
219     // If sequential lines start with PROGRESS_PREFIX, 2nd will overwrite 1st.
220     int chunklen = 0;
221     int len = safe_int<int>(maxSize);
222     const char *chunk = data;
223     while (chunklen < len) {
224 	if (chunk[chunklen++] != '\n' && chunklen < len)
225 	    continue;
226 	if (_cursor->atBlockStart()) { // beginning of line?
227 	    if (strncmp(chunk, PROGRESS_PREFIX, sizeof(PROGRESS_PREFIX)-1) == 0) {
228 		if (overwritableStart >= 0)
229 		    _cursor->setPosition(overwritableStart, QTextCursor::KeepAnchor);
230 		overwritableStart = _cursor->position();
231 	    } else {
232 		overwritableStart = -1;
233 	    }
234 	    _cursor->insertText(QSL(" ")); // indent
235 	}
236 	_cursor->insertText(QString(QString::fromLocal8Bit(chunk, chunklen)));
237 	chunk += chunklen;
238 	len -= chunklen;
239 	chunklen = 0;
240     }
241 
242     if (wasNearBottom)
243 	sb->setValue(sb->maximum());
244     return maxSize;
245 }
246 
logHandler(QtMsgType type,const QMessageLogContext & ctx,const QString & msg)247 void MainWindow::logHandler(QtMsgType type, const QMessageLogContext &ctx,
248     const QString &msg)
249 {
250     if (type == QtCriticalMsg || type == QtFatalMsg)
251 	if (!consoleWidget->isVisible())
252 	    consoleButton->click();
253     QTextCharFormat oldfmt = winout->setLogCharFmt(type);
254     SpooferBase::logHandler(type, ctx, msg);
255     winout->setCharFmt(oldfmt);
256 }
257 
setActionTip(QAction * action,QString tip)258 static void setActionTip(QAction *action, QString tip)
259 {
260     action->setStatusTip(tip);
261     action->setToolTip(tip);
262 }
263 
MainWindow(QWidget * _parent)264 MainWindow::MainWindow(QWidget *_parent) :
265     QMainWindow(_parent), SpooferUI(),
266     schedWatcher(), countdownTimer(this), centralLayout(),
267     proberInfoLabel(), proberCountdownLabel(), schedulerStatusLabel(),
268     proberBoxLayout(), firstProberMsgIdx(), nextProberMsgIdx(),
269     proberTimeRow(), progRow4(), progRow6(), currentRun(),
270     aboutAct(), exitAct(), runAct(), abortAct(),
271     pauseAct(), resumeAct(), shutdownAct(), hideGuiAct(), showGuiAct(),
272     runButton(), pauseButton(), historyWidget(), prefDialog(),
273     upgradeBox(), upgradeNotice(new QLabel()), upgradeDetails(new QLabel()),
274     upgradeAutoNotice(new QLabel()),
275 #ifdef AUTOUPGRADE_ENABLED
276     upgradeResultBox(), upgradeResultLabel(new QLabel()), upgradeResultTime(0),
277     upgradeAct(), upgradeCancelAct(), upgradeButton(), upgradeCancelButton(),
278     autoupgradeTimer(), autoupgradeTime(),
279 #endif
280     debugFlags()
281 {
282     theInstance = this;
283 }
284 
285 #define CHAR_ARROW_RT "\u25B6"
286 
287 #define HISTORY_COLUMNS(XX)  /* "X-Macro" */ \
288     XX(date,         "date",              "Date and time of prober run") \
289     XX(ipv,          "IPv",               "Internet Protocol version") \
290     XX(ipaddr_short, "client address",    "IP address of spoofer client\n(click for full IPv6 addresses)") \
291     XX(ipaddr_long,  "client address",    "IP address of spoofer client\n(click to abbreviate IPv6 addresses)") \
292     XX(asn,          "ASN",               "Autonomous System number of client") \
293     XX(privaddr,     "outbound\nprivate", "Result of client sending packets to server with spoofed private addresses") \
294     XX(routable,     "outbound\nroutable","Result of client sending packets to server with spoofed routable addresses") \
295     XX(inprivaddr,   "inbound\nprivate",  "Result of server sending packets to client with spoofed private addresses") \
296     XX(ininternal,   "inbound\ninternal", "Result of server sending packets to client with spoofed internal (same subnet as client) addresses") \
297     XX(report,       "report",            "Summary report at website") \
298     XX(log,          "log",               "Prober technical log (click to hide)") \
299     XX(showlog,      CHAR_ARROW_RT,       "Click to show logs")
300 
301 enum historyColumnId {
302 #define DECLARE_HIST_ENUM(id, name, tip)  HIST_##id,
303 HISTORY_COLUMNS(DECLARE_HIST_ENUM)
304 HIST_N
305 };
306 
307 static const struct {
308     QString name;
309     QString tip;
310 } historyHeader[HIST_N] = {
311 #define DEFINE_HIST_HEADER(id, name, tip)  { QSL(name), QSL(tip) },
312 HISTORY_COLUMNS(DEFINE_HIST_HEADER)
313 };
314 
resultWidget(const QString & raw)315 static QLabel *resultWidget(const QString &raw)
316 {
317     static QChar checkMark(0x2714);
318     static QChar xMark(0x2718);
319     QString text;
320     QLabel *widget = new ColoredLabel();
321     QPalette palette = widget->palette();
322     if (raw.compare(QSL("BLOCKED"), Qt::CaseInsensitive) == 0) {
323 	text.append(checkMark).append(QSL(" "));
324 	palette.setColor(QPalette::Text, Qt::darkGreen);
325     } else if (raw.compare(QSL("RECEIVED"), Qt::CaseInsensitive) == 0) {
326 	text.append(xMark).append(QSL(" "));
327 	palette.setColor(QPalette::Text, Qt::darkRed);
328     } else if (raw.compare(QSL("REWRITTEN"), Qt::CaseInsensitive) == 0) {
329 	text.append(xMark).append(QSL(" "));
330 	palette.setColor(QPalette::Text, Qt::darkYellow);
331     } else {
332 	text.append(QSL("? "));
333     }
334     widget->setText(text.append(raw));
335     widget->setPalette(palette);
336     return widget;
337 }
338 
scaleVerticalMargins(QLayout * layout,qreal factor)339 static void scaleVerticalMargins(QLayout *layout, qreal factor)
340 {
341     QMargins m = layout->contentsMargins();
342     m.setTop(int(m.top() * factor));
343     m.setBottom(int(m.bottom() * factor));
344     layout->setContentsMargins(m);
345 }
346 
newFrame(QLayout * parentLayout,QLayout * selfLayout)347 static QFrame *newFrame(QLayout *parentLayout, QLayout *selfLayout)
348 {
349     QFrame *frame = new QFrame();
350     frame->setFrameStyle(QFrame::Panel | QFrame::Sunken);
351     parentLayout->addWidget(frame);
352     frame->setLayout(selfLayout);
353     scaleVerticalMargins(selfLayout, 0.5);
354     return frame;
355 }
356 
newBanner(QLayout * parentLayout,QLayout * selfLayout)357 static QFrame *newBanner(QLayout *parentLayout, QLayout *selfLayout)
358 {
359     QFrame *banner = newFrame(parentLayout, selfLayout);
360     banner->setAutoFillBackground(true);
361     banner->setBackgroundRole(QPalette::ToolTipBase);
362     banner->setForegroundRole(QPalette::ToolTipText);
363     banner->hide();
364     return banner;
365 }
366 
367 // Do extra initialization before QMainWindow::show().
init()368 void MainWindow::init()
369 {
370     QIcon spooferIcon;
371     spooferIcon.addFile(QSL(":/icons/spoofer16.png"), QSize(16,16));
372     spooferIcon.addFile(QSL(":/icons/spoofer32.png"), QSize(32,32));
373     spooferIcon.addFile(QSL(":/icons/spoofer48.png"), QSize(48,48));
374     spooferIcon.addFile(QSL(":/icons/spoofer64.png"), QSize(64,64));
375     spooferIcon.addFile(QSL(":/icons/spoofer128.png"), QSize(128,128));
376     spooferIcon.addFile(QSL(":/icons/spoofer256.png"), QSize(256,256));
377     setWindowIcon(spooferIcon);
378 
379     scheduler = new QLocalSocket(this);
380 
381     //========= result history table
382     historyWidget = new QTableWidget(this);
383 
384     //========= text output console
385     consoleWidget = new QPlainTextEdit(this);
386 
387     //========= Actions
388     QAction *prefAct = new QAction(QSL("&Preferences"), this);
389     setActionTip(prefAct, QSL("Open preferences dialog"));
390     connect(prefAct, &QAction::triggered, this, &MainWindow::openPreferences);
391 
392 #if 0 // not used
393     helpAct = new QAction(QSL("&Help"), this);
394     setActionTip(helpAct, QSL("View documentation"));
395     helpAct->setShortcut(QKeySequence::HelpContents);
396     connect(helpAct, &QAction::triggered, this, &MainWindow::help);
397 #endif
398 
399     aboutAct = new QAction(QSL("&About Spoofer"), this);
400     setActionTip(aboutAct, QSL("About Spoofer"));
401     connect(aboutAct, &QAction::triggered, this, &MainWindow::about);
402 
403     exitAct = new QAction(QSL("&Exit GUI"), this);
404     setActionTip(exitAct, QSL("Exit this GUI (does not affect Scheduler or Prober)"));
405     exitAct->setShortcut(QKeySequence::Quit);
406     connect(exitAct, &QAction::triggered, this, &MainWindow::close);
407 
408     hideGuiAct = new QAction(QSL("&Hide GUI"), this);
409     setActionTip(hideGuiAct, QSL("Hide GUI"));
410     hideGuiAct->setEnabled(true);
411     connect(hideGuiAct, &QAction::triggered, this, &MainWindow::hide);
412 
413     showGuiAct = new QAction(QSL("&Show GUI"), this);
414     setActionTip(showGuiAct, QSL("Show GUI"));
415     showGuiAct->setEnabled(true);
416     connect(showGuiAct, &QAction::triggered, this, &MainWindow::show);
417 
418     QAction *hideConsoleAct = new QAction(QSL("Hide &Console"), this);
419     setActionTip(hideConsoleAct, QSL("Hide console"));
420     connect(hideConsoleAct, &QAction::triggered, consoleWidget, &QPlainTextEdit::hide);
421 
422     QAction *showConsoleAct = new QAction(QSL("Show &Console"), this);
423     setActionTip(showConsoleAct, QSL("Show console"));
424     connect(showConsoleAct, &QAction::triggered, consoleWidget, &QPlainTextEdit::show);
425 
426     QAction *clearConsoleAct = new QAction(QSL("Clear Console"), this);
427     setActionTip(clearConsoleAct, QSL("Erase contents of console window"));
428     connect(clearConsoleAct, &QAction::triggered, consoleWidget, &QPlainTextEdit::clear);
429 
430 #ifdef HIDABLE_HISTORY
431     QAction *hideHistoryAct = new QAction(QSL("Hide Result &History"), this);
432     setActionTip(hideHistoryAct, QSL("Hide results of previous tests"));
433     connect(hideHistoryAct, &QAction::triggered, historyWidget, &QTableWidget::hide);
434 
435     QAction *showHistoryAct = new QAction(QSL("Show Result &History"), this);
436     setActionTip(showHistoryAct, QSL("Show results of previous tests"));
437     connect(showHistoryAct, &QAction::triggered, historyWidget, &QTableWidget::show);
438 #endif
439 
440     runAct = new QAction(QSL("Start Tests"), this);
441     setActionTip(runAct, QSL("Start a Prober test run now"));
442     runAct->setShortcut(QKeySequence::New);
443     runAct->setEnabled(false);
444     connect(runAct, &QAction::triggered, this, &MainWindow::runProber);
445 
446     abortAct = new QAction(QSL("Stop Tests"), this);
447     setActionTip(abortAct, QSL("Stop the current Prober test run"));
448     abortAct->setEnabled(false);
449     connect(abortAct, &QAction::triggered, this, &MainWindow::abortProber);
450 
451     pauseAct = new QAction(QSL("&Pause Scheduler"), this);
452     setActionTip(pauseAct, QSL("Disable automatic scheduled Prober runs"));
453     pauseAct->setEnabled(false);
454     connect(pauseAct, &QAction::triggered,
455 	this, &MainWindow::pauseScheduler);
456 
457     resumeAct = new QAction(QSL("&Resume Scheduler"), this);
458     setActionTip(resumeAct, QSL("Enable automatic scheduled Prober runs"));
459     resumeAct->setEnabled(false);
460     connect(resumeAct, &QAction::triggered,
461 	this, &MainWindow::resumeScheduler);
462 
463     shutdownAct = new QAction(QSL("&Shutdown Scheduler"), this);
464     setActionTip(shutdownAct, QSL("Shut down Scheduler (not recommended)"));
465     shutdownAct->setEnabled(false);
466     connect(shutdownAct, &QAction::triggered,
467 	this, &MainWindow::shutdownScheduler);
468 
469 #ifdef AUTOUPGRADE_ENABLED
470     upgradeAct = new QAction(QSL("Upgrade"), this);
471     setActionTip(upgradeAct, QSL("Upgrade Spoofer now"));
472     upgradeAct->setEnabled(true);
473     connect(upgradeAct, &QAction::triggered, this, &MainWindow::doUpgrade);
474 
475     upgradeCancelAct = new QAction(QSL("Cancel"), this);
476     setActionTip(upgradeCancelAct, QSL("Cancel automatic upgrade"));
477     upgradeCancelAct->setEnabled(true);
478     connect(upgradeCancelAct, &QAction::triggered, this, &MainWindow::cancelUpgrade);
479 #endif
480 
481     //========= menubar
482     // Note: menubar->addAction() may work on some platforms, but does not
483     // work at all on OSX; only QMenus can be added to menubar.
484     // (Toolbars, OTOH, can contain actions and widgets but not menus.)
485     QMenuBar *menubar = this->menuBar();
486 
487     // Note: on OSX, Qt will move the special entries "about", "exit", and
488     // "preferences" out to the OS menu unless their role is changed.
489 #ifdef Q_OS_MACOS
490     QMenu *spooferMenu = new QMenu(QSL("Preferences"));
491     spooferMenu->menuAction()->setMenuRole(QAction::NoRole);
492 #else
493     QMenu *spooferMenu = new QMenu(QSL("Spoo&fer"));
494 #endif
495     spooferMenu->addAction(prefAct);
496     spooferMenu->addAction(aboutAct);
497     spooferMenu->addAction(exitAct);
498     menubar->addMenu(spooferMenu);
499 
500     QMenu *schedulerMenu = new QMenu(QSL("&Scheduler"));
501     schedulerMenu->addAction(pauseAct);
502     schedulerMenu->addAction(resumeAct);
503     schedulerMenu->addAction(shutdownAct);
504     menubar->addMenu(schedulerMenu);
505 
506     QMenu *proberMenu = new QMenu(QSL("&Prober"));
507     proberMenu->addAction(runAct);
508     proberMenu->addAction(abortAct);
509     menubar->addMenu(proberMenu);
510 
511     //========= status bar (for action tips)
512     statusBar()->setSizeGripEnabled(false);
513 
514     //========= central widget
515     QWidget *widget = new QWidget();
516     setCentralWidget(widget);
517     centralLayout = new QVBoxLayout;
518     // centralLayout->addStrut(600); // minimum width
519     widget->setLayout(centralLayout);
520     centralLayout->setSpacing(4);
521     scaleVerticalMargins(centralLayout, 0.5);
522 
523 #ifdef AUTOUPGRADE_ENABLED
524     //========= upgrade result information
525     QHBoxLayout *upgradeResultLayout = new QHBoxLayout();
526     upgradeResultBox = newBanner(centralLayout, upgradeResultLayout);
527 
528     upgradeResultLayout->addWidget(upgradeResultLabel, 1);
529     upgradeResultLabel->setWordWrap(true);
530 
531     auto *upgradeResultClose = new QPushButton();
532     upgradeResultLayout->addWidget(upgradeResultClose, 0, Qt::AlignRight);
533     upgradeResultClose->setIcon(style()->standardIcon(QStyle::SP_DockWidgetCloseButton));
534     upgradeResultClose->setFlat(true);
535     upgradeResultClose->setToolTip(QSL("Close this notice"));
536     connect(upgradeResultClose, &QToolButton::clicked, upgradeResultBox, &QWidget::hide);
537 #endif
538 
539     //========= pending upgrade information
540     QGridLayout *upgradeLayout = new QGridLayout();
541     upgradeBox = newBanner(centralLayout, upgradeLayout);
542 
543     // row 0: upgrade available
544     QVBoxLayout *upgradeNoticeLayout = new QVBoxLayout();
545     upgradeLayout->addLayout(upgradeNoticeLayout, 0, 0);
546     upgradeNoticeLayout->addWidget(upgradeNotice);
547     upgradeNotice->setWordWrap(true);
548     upgradeNoticeLayout->addWidget(upgradeDetails);
549     upgradeDetails->setWordWrap(true);
550     connect(upgradeNotice, &QLabel::linkActivated, this, &MainWindow::toggleUpgradeDetails);
551     upgradeDetails->setTextInteractionFlags(Qt::TextSelectableByMouse);
552     upgradeDetails->hide();
553 #ifdef AUTOUPGRADE_ENABLED
554     upgradeButton = new ActionButton(upgradeAct, nullptr, this);
555     upgradeLayout->addWidget(upgradeButton, 0, 1, Qt::AlignRight | Qt::AlignVCenter);
556     // row 1: automatic upgrade
557     upgradeLayout->addWidget(upgradeAutoNotice, 1, 0);
558     upgradeAutoNotice->setWordWrap(true);
559     upgradeCancelButton = new ActionButton(upgradeCancelAct, nullptr, this);
560     upgradeLayout->addWidget(upgradeCancelButton, 1, 1, Qt::AlignRight | Qt::AlignVCenter);
561     upgradeLayout->setColumnStretch(0, 1);
562 
563     connect(&autoupgradeTimer, &QTimer::timeout, this, &MainWindow::showUpgradeTimer);
564 #endif
565 
566     //========= scheduler information
567     QHBoxLayout *siLayout = new QHBoxLayout();
568     /* QFrame *siBox = */ newFrame(centralLayout, siLayout);
569     siLayout->addWidget(new QLabel(QSL("Scheduler:"), this));
570     siLayout->addWidget(schedulerStatusLabel = new QLabel(QSL("unknown"), this), 1);
571     siLayout->addWidget(pauseButton = new ActionButton(pauseAct, resumeAct, this));
572 
573     //========= prober information
574     proberBoxLayout = new QVBoxLayout();
575     /* QFrame *proberBox = */ newFrame(centralLayout, proberBoxLayout);
576 
577     // prober grid
578     QGridLayout *proberGrid = new QGridLayout;
579     proberGrid->setColumnStretch(1, 1);
580     proberBoxLayout->addLayout(proberGrid);
581     scaleVerticalMargins(proberGrid, 0.5);
582     int rownum = -1;
583 
584     // prober grid first line
585     proberGrid->addWidget(new QLabel(QSL("Prober:"), this), ++rownum, 0, Qt::AlignVCenter);
586     QHBoxLayout *p0 = new QHBoxLayout();
587     proberGrid->addLayout(p0, rownum, 1, Qt::AlignVCenter);
588     p0->addWidget(proberInfoLabel = new QLabel(QSL("schedule unknown"), this), 0);
589     p0->addWidget(proberCountdownLabel = new QLabel(QSL(""), this), 1);
590     p0->addWidget(runButton = new ActionButton(runAct, nullptr, this));
591     connect(&countdownTimer, &QTimer::timeout, this, &MainWindow::setCountdownLabel);
592 
593     // prober time and progress bars
594     proberTimeRow = new TableRow<QLabel>(proberGrid, ++rownum, QSL(""), new QLabel());
595     progRow4 = new ProgressRow(proberGrid, ++rownum, QSL("IPv4 progress:"));
596     progRow6 = new ProgressRow(proberGrid, ++rownum, QSL("IPv6 progress:"));
597 
598     // index for future prober messages
599     nextProberMsgIdx = firstProberMsgIdx = proberBoxLayout->count();
600 
601     //========= history buttons
602     QHBoxLayout *historyButtons = new QHBoxLayout();
603     centralLayout->addLayout(historyButtons);
604 
605 #ifdef HIDABLE_HISTORY
606     //========= show/hide history button
607     historyWidget->hide();
608     ActionButton *historyButton = historyWidget->isVisible() ?
609 	new ActionButton(hideHistoryAct, showHistoryAct, this) :
610 	new ActionButton(showHistoryAct, hideHistoryAct, this);
611     historyButtons->addWidget(historyButton, 0, Qt::AlignLeft);
612 #else
613     historyButtons->addWidget(new QLabel(QSL("Result history:")));
614 #endif
615 
616     //========= show/hide blank history checkbox
617     QCheckBox *hideBlankTestsButton = new QCheckBox(QSL("Hide old blank tests"), this);
618     hideBlankTestsButton->setChecked(true);
619     connect(hideBlankTestsButton, &QCheckBox::toggled, this, &MainWindow::hideBlankTests);
620     historyButtons->addWidget(hideBlankTestsButton, 0, Qt::AlignRight);
621 
622     //========= result history table
623     centralLayout->addWidget(historyWidget, 2);
624     historyWidget->setSelectionMode(QAbstractItemView::NoSelection);
625     historyWidget->setColumnCount(HIST_N);
626     for (int i = 0; i < HIST_N; i++) {
627 	QTableWidgetItem *item = new QTableWidgetItem(historyHeader[i].name);
628 	if (!historyHeader[i].tip.isEmpty())
629 	    item->setToolTip(historyHeader[i].tip);
630 	historyWidget->setHorizontalHeaderItem(i, item);
631     }
632 
633     int hsize = QFontInfo(historyWidget->font()).pixelSize();
634     historyWidget->setMinimumHeight(12*hsize);
635     historyWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
636     historyWidget->verticalHeader()->setVisible(false);
637     historyWidget->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
638     QHeaderView *hh = historyWidget->horizontalHeader();
639     hh->setSectionResizeMode(QHeaderView::ResizeToContents);
640     hh->setSectionsClickable(true);
641     connect(hh, &QHeaderView::sectionClicked,
642 	this, &MainWindow::handleHistoryColumnClick);
643     hh->setSectionHidden(HIST_log, true);
644     hh->setSectionHidden(HIST_ipaddr_long, true);
645     hh->setMinimumSectionSize(0);
646 
647     {
648 	// There ought to be a better way to do this...
649 	hh->setSectionHidden(HIST_showlog, true); // don't count showlog
650 	historyWidget->insertRow(0); // add dummy row to calculate width
651 	addHistoryCell(0, HIST_date, Qt::AlignLeft, new QLabel(QSL("0000-00-00 00:00:00")));
652 	addHistoryCell(0, HIST_ipv, Qt::AlignHCenter, new QLabel(QSL("0")));
653 	addHistoryCell(0, HIST_ipaddr_short, Qt::AlignLeft, new QLabel(QSL("000:000:000:000::/64")));
654 	addHistoryCell(0, HIST_asn, Qt::AlignRight, new QLabel(QSL("0000")));
655 	addHistoryCell(0, HIST_privaddr, Qt::AlignHCenter, resultWidget(QSL("rewritten")));
656 	addHistoryCell(0, HIST_routable, Qt::AlignHCenter, resultWidget(QSL("rewritten")));
657 	addHistoryCell(0, HIST_inprivaddr, Qt::AlignHCenter, resultWidget(QSL("rewritten")));
658 	addHistoryCell(0, HIST_ininternal, Qt::AlignHCenter, resultWidget(QSL("rewritten")));
659 	addHistoryCell(0, HIST_report, Qt::AlignHCenter, new QLabel(QSL("report")));
660 	historyWidget->resizeColumnsToContents();
661 	int histWidth = 0;
662 	for (int i = 0; i < HIST_N; i++)
663 	    if (!hh->isSectionHidden(i))
664 		histWidth += historyWidget->columnWidth(i);
665 	histWidth += historyWidget->verticalScrollBar()->sizeHint().width();
666 	historyWidget->setMinimumWidth(histWidth);
667 	historyWidget->removeRow(0); // remove dummy row
668 	hh->setSectionHidden(HIST_showlog, false);
669     }
670 
671     //========= console buttons
672     QHBoxLayout *consoleButtons = new QHBoxLayout();
673     centralLayout->addLayout(consoleButtons);
674 
675     //========= show/hide console button
676     consoleWidget->hide();
677     consoleButton = new ActionButton(showConsoleAct, hideConsoleAct, this);
678     // If console visibility could be changed by something other than the
679     // button, we'd have to detect that using a custom class Foo with an
680     // eventFilter() that catches Show and Hide events, and then
681     // consoleWidget->installEventFilter(Foo).
682     consoleButtons->addWidget(consoleButton, 0, Qt::AlignLeft);
683 
684     //========= clear console button
685     ActionButton *clearConsoleButton = new ActionButton(clearConsoleAct, nullptr, this);
686     clearConsoleButton->hide();
687     consoleButtons->addWidget(clearConsoleButton, 1, Qt::AlignLeft);
688     connect(hideConsoleAct, &QAction::triggered, clearConsoleButton, &QPushButton::hide);
689     connect(showConsoleAct, &QAction::triggered, clearConsoleButton, &QPushButton::show);
690 
691     //========= text output console
692     centralLayout->addWidget(consoleWidget, 5);
693     consoleWidget->setReadOnly(true);
694     int csize = QFontInfo(consoleWidget->font()).pixelSize();
695     consoleWidget->setMinimumHeight(15*csize);
696     consoleWidget->setMaximumBlockCount(10000);
697     // Create a cursor for appending, independent of the default cursor which
698     // can be moved by the user (e.g., to copy a selection).
699     winout = new WindowOutput(consoleWidget);
700     outdev.setDevice(winout, QSL(""));
701     errdev.setDevice(winout, QSL(""));
702     qInstallMessageHandler(MainWindow::logHandler);
703 
704     //========= stretch
705     // keep other stuff from stretching when history and console are hidden
706     centralLayout->addStretch(0);
707 
708 
709 
710 
711     qDebug() << "### cwd: " << qPrintable(QDir::toNativeSeparators(QDir::currentPath()));
712 
713     //========= other
714     this->setWindowTitle(mainTitle);
715     this->setEnabled(false); // until event loop is started
716 
717     showStatus();
718 
719 #if 0
720     if (QSystemTrayIcon::isSystemTrayAvailable()) {
721 	qDebug() << "### System tray is available";
722 	QIcon *icon = new QIcon(QSL("/home/kkeys/WIP/spoofer/cropped/spoofer32.png")); // XXX
723 	QSystemTrayIcon *trayicon = new QSystemTrayIcon(*icon, 0);
724 	QMenu *traymenu = new QMenu(this);
725 	traymenu->addAction(aboutAct);
726 	traymenu->addAction(pauseAct);
727 	traymenu->addAction(resumeAct);
728 	traymenu->addAction(showGuiAct);
729 	traymenu->addAction(hideGuiAct);
730 	traymenu->addAction(exitAct);
731 	trayicon->setContextMenu(traymenu);
732 	trayicon->show();
733     } else {
734 	qDebug() << "### System tray is NOT available";
735     }
736 #endif
737 
738     // Turn off the table grid, but give each cell a margin.  The bgcolor
739     // will appear in the cell margins as well as the space outside the table.
740     historyWidget->setShowGrid(false);
741     qApp->setStyleSheet(QSL(
742 	"QTableWidget { background: " HISTORYTABLE_BGCOLOR "; }"
743 	"QTableWidget:item { background: white; margin: 1px; }"
744 	));
745 
746     this->setEnabled(true);
747 }
748 
handleHistoryColumnClick(int logicalIndex)749 void MainWindow::handleHistoryColumnClick(int logicalIndex)
750 {
751     QHeaderView *hh = historyWidget->horizontalHeader();
752     if (logicalIndex == HIST_log) {
753 	hh->setSectionHidden(HIST_log, true);
754 	hh->setSectionHidden(HIST_showlog, false);
755     } else if (logicalIndex == HIST_showlog) {
756 	hh->setSectionHidden(HIST_log, false);
757 	hh->setSectionHidden(HIST_showlog, true);
758     } else if (logicalIndex == HIST_ipaddr_short) {
759 	hh->setSectionHidden(HIST_ipaddr_long, false);
760 	hh->setSectionHidden(HIST_ipaddr_short, true);
761     } else if (logicalIndex == HIST_ipaddr_long) {
762 	hh->setSectionHidden(HIST_ipaddr_long, true);
763 	hh->setSectionHidden(HIST_ipaddr_short, false);
764     }
765 }
766 
767 // Before doing anything that might alter the main window (e.g., by calling
768 // logHandler()) or trigger QApplication::exit(), wait until the MainWindow is
769 // visible (the Show event) and the event loop has started, plus a delay
770 // for mysterious other stuff that can cause display glitches if we didn't
771 // wait for it.
showEvent(QShowEvent * ev)772 void MainWindow::showEvent(QShowEvent *ev)
773 {
774     Q_UNUSED(ev);
775     static bool started = false;
776     if (!started)
777 	QTimer::singleShot(1, this, SLOT(initEvents()));
778     started = true;
779 }
780 
initEvents()781 void MainWindow::initEvents()
782 {
783     QLabel *label = new QLabel(QSL("Loading result history..."), historyWidget);
784     label->raise();
785     label->show();
786     label->setContentsMargins(10,10,10,10);
787     label->setAutoFillBackground(true);
788     label->adjustSize();
789     int pw = historyWidget->size().width();
790     int ph = historyWidget->size().height();
791     int lw = label->size().width();
792     int lh = label->size().height();
793     label->setGeometry((pw-lw)/2, (ph-lh)/2, lw, lh); // center
794     repaint();
795     qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
796 
797     loadHistoricLogs();
798 
799     delete label;
800     repaint();
801     qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
802 
803     if (!connectScheduler(true))
804 	watchForSchedulerRestart();
805 
806     this->setEnabled(true);
807 }
808 
connectScheduler(bool privileged)809 bool MainWindow::connectScheduler(bool privileged)
810 {
811     connect(scheduler, &QLocalSocket::connected,
812 	this, &MainWindow::schedConnected);
813     connect(scheduler, &QLocalSocket::disconnected,
814 	this, &MainWindow::schedDisconnected);
815     connect(scheduler, QLOCALSOCKET_ERROR_OCCURRED,
816 	this, &MainWindow::schedError);
817     connect(scheduler, &QLocalSocket::readyRead,
818 	this, &MainWindow::readScheduler);
819     return connectToScheduler(privileged);
820 }
821 
opAllowed(const Config::MemberBase & cfgItem)822 bool MainWindow::opAllowed(const Config::MemberBase &cfgItem)
823 {
824     return (scheduler && scheduler->state() == QLocalSocket::ConnectedState) &&
825 	(connectionIsPrivileged || cfgItem.variant().toBool());
826 }
827 
additionalArgs()828 QStringList MainWindow::additionalArgs() {
829     QStringList list;
830     list << QSL("-qwindowgeometry") <<
831 	QSL("+") % QString::number(x()) % QSL("+") % QString::number(y());
832     return list;
833 }
834 
openPreferences()835 void MainWindow::openPreferences()
836 {
837     if (!prefDialog) {
838 	prefDialog = new PreferencesDialog(this, this->scheduler,
839 	    opAllowed(config->unprivPref));
840 	connect(prefDialog, &QDialog::finished, this, &MainWindow::closePreferences);
841     }
842     prefDialog->show();
843     prefDialog->raise();
844     prefDialog->activateWindow();
845 }
846 
closePreferences()847 void MainWindow::closePreferences()
848 {
849     if (prefDialog) {
850 	disconnect(prefDialog, nullptr, nullptr, nullptr);
851 	prefDialog->deleteLater();
852 	prefDialog = nullptr;
853     }
854 }
855 
toggleUpgradeDetails(const QString & link)856 void MainWindow::toggleUpgradeDetails(const QString &link)
857 {
858     Q_UNUSED(link);
859     upgradeDetails->setVisible(!upgradeDetails->isVisible());
860 }
861 
promptForUpgrade()862 void MainWindow::promptForUpgrade()
863 {
864     upgradeBox->hide();
865 
866     QString notice(QSL("<div>Spoofer upgrade %1: %2 "
867 	"<a href='#'>(details)</a>.</div>")
868 	.arg(upgradeInfo->mandatory ? QSL("required") : QSL("available"))
869 	.arg(upgradeInfo->vstr.toHtmlEscaped()));
870     if (!upgradeInfo->warning.isNull())
871 	notice += QSL("<div style='color:red'>") %
872 	    upgradeInfo->warning.toHtmlEscaped() % QSL("</div>");
873     upgradeNotice->setText(notice);
874 
875     QString details;
876 #ifndef UPGRADE_WITHOUT_DOWNLOAD
877     details.append(QSL("<div>URL: ") % upgradeInfo->file.toHtmlEscaped() % QSL("</div>"));
878 #endif
879 #ifdef UPGRADE_CMD
880     details.append(QSL("<div>command: <code>") % QSL(UPGRADE_CMD).toHtmlEscaped() % QSL("</code></div>"));
881 #endif
882     upgradeDetails->setText(details);
883     upgradeDetails->hide();
884 
885 #ifdef AUTOUPGRADE_ENABLED
886     time_t now;
887     time(&now);
888     if (now - upgradeResultTime > 60)
889 	upgradeResultBox->hide();
890     if (upgradeInfo->autoTime >= 0) {
891 	autoupgradeTime = now + upgradeInfo->autoTime;
892 	autoupgradeTimer.start(1000); // every second
893 	showUpgradeTimer();
894     }
895     upgradeAutoNotice->setVisible(upgradeInfo->autoTime >= 0);
896     upgradeCancelButton->setVisible(upgradeInfo->autoTime >= 0);
897     upgradeButton->show();
898 #endif
899     upgradeNotice->show();
900     upgradeBox->show();
901     if (upgradeInfo->autoTime >= 0)
902 	runAct->setEnabled(false);
903 }
904 
905 #ifdef AUTOUPGRADE_ENABLED
showUpgradeTimer()906 void MainWindow::showUpgradeTimer()
907 {
908     time_t now;
909     time(&now);
910     upgradeAutoNotice->setText(QSL("Spoofer will upgrade automatically in ") %
911 	QString::number(autoupgradeTime - now) % QSL(" seconds."));
912 }
913 
doUpgrade()914 void MainWindow::doUpgrade()
915 {
916     scheduler->write("upgrade\n");
917     cancelUpgradePrompt();
918 }
919 
cancelUpgrade()920 void MainWindow::cancelUpgrade()
921 {
922     scheduler->write("cancel\n");
923     cancelUpgradePrompt();
924 }
925 
showUpgradeProgress(const QString & text)926 void MainWindow::showUpgradeProgress(const QString &text)
927 {
928     qDebug() << "showUpgradeProgress" << text;
929     upgradeAutoNotice->setText(text);
930     upgradeAutoNotice->show();
931     upgradeCancelButton->show();
932     upgradeBox->show();
933 
934     upgradeResultBox->hide();
935     upgradeNotice->hide();
936     upgradeDetails->hide();
937     upgradeButton->hide();
938     runAct->setEnabled(false);
939 }
940 
showUpgradeResult(const QString & text)941 void MainWindow::showUpgradeResult(const QString &text)
942 {
943     qDebug() << "showUpgradeResult" << text;
944     upgradeResultLabel->setText(text);
945     upgradeResultBox->show();
946     time(&upgradeResultTime);
947 
948     upgradeAutoNotice->hide();
949     upgradeCancelButton->hide();
950     upgradeBox->hide();
951     showStatus();
952 }
953 #endif // AUTOUPGRADE_ENABLED
954 
cancelUpgradePrompt()955 void MainWindow::cancelUpgradePrompt()
956 {
957 #ifdef AUTOUPGRADE_ENABLED
958     autoupgradeTimer.stop();
959 #endif // AUTOUPGRADE_ENABLED
960     upgradeBox->hide();
961 }
962 
help()963 void MainWindow::help() {
964     qDebug() << "### signal: help";
965 }
966 
about()967 void MainWindow::about() {
968     static QString text;
969     if (text.isNull()) {
970 	text +=
971 	    QSL("<h1>" PACKAGE_NAME "</h1>") %
972 	    QSL("<span style='white-space: nowrap;'>") %
973 	    QSL(PACKAGE_DESC ", version " PACKAGE_VERSION "<br />") %
974 	    QSL("</span>") %
975 	    QSL(PACKAGE_LONGDESC "<br /><br />") %
976 	    QSL("<span style='white-space: nowrap;'>") %
977 	    QSL(COPYRIGHT).replace(QSL("; "), QSL("<br />")) %
978 	    QSL("</span>") %
979 	    QSL("<br /><br />") %
980 	    QSL("<a href='" PACKAGE_URL "'>" PACKAGE_URL "</a>" "<br />");
981 	    // QSL("contact: " PACKAGE_BUGREPORT "<br />");
982     }
983     QMessageBox::about(this, QSL("About Spoofer"), text);
984 }
985 
configChanged()986 void MainWindow::configChanged()
987 {
988     if (prefDialog)
989 	prefDialog->warn(QSL("Warning: settings have changed since this window opened."));
990 }
991 
needConfig()992 void MainWindow::needConfig()
993 {
994     openPreferences();
995 }
996 
schedConnected()997 void MainWindow::schedConnected()
998 {
999     spout << "Connected to scheduler." << Qt_endl;
1000     connect(qApp, &QApplication::aboutToQuit, scheduler, &QLocalSocket::close);
1001     schedulerPaused = false; // until told otherwise
1002     schedulerNeedsConfig = false; // until told otherwise
1003     if (fileTail) // stale
1004 	fileTail->requestInterruption();
1005     showStatus();
1006     if (schedWatcher) delete schedWatcher;
1007     schedWatcher = nullptr;
1008 }
1009 
schedCleanup(bool isError)1010 void MainWindow::schedCleanup(bool isError)
1011 {
1012     if (prefDialog) {
1013 	prefDialog->warn(QSL("Disconnected from Scheduler.  Settings can not be changed."));
1014 	prefDialog->disable();
1015     }
1016     disconnect(qApp, nullptr, scheduler, nullptr);
1017     disconnect(scheduler, nullptr, nullptr, nullptr);
1018     if (isError) scheduler->abort();
1019     scheduler->close();
1020     scheduler->deleteLater();
1021     scheduler = new QLocalSocket(this);
1022     if (fileTail) {
1023 	// Without the scheduler to tell us SC_PROBER_FINISHED, we'll assume
1024 	// prober is done after a period of inactivity in the log.
1025 	fileTail->setTimeout(10000);
1026     }
1027     cancelUpgradePrompt();
1028     showStatus();
1029 #ifndef EVERYONE_IS_PRIVILEGED
1030     if (isError && connectionIsPrivileged)
1031 	if (connectScheduler(false)) return;
1032 #endif
1033     watchForSchedulerRestart();
1034 }
1035 
schedError()1036 void MainWindow::schedError()
1037 {
1038     qCritical() << "Scheduler error:" << qPrintable(scheduler->errorString());
1039     schedCleanup(true);
1040 }
1041 
showStatus()1042 void MainWindow::showStatus()
1043 {
1044     statusBar()->clearMessage();
1045     if (fileTail) {
1046 	runAct->setEnabled(false);
1047 	abortAct->setEnabled(opAllowed(config->unprivTest));
1048     } else {
1049 	runAct->setEnabled(opAllowed(config->unprivTest) && !upgradeAutoNotice->isVisible());
1050 	abortAct->setEnabled(false);
1051     }
1052     runButton->setAction(fileTail ? abortAct : runAct);
1053     bool connected = scheduler && (scheduler->state() == QLocalSocket::ConnectedState);
1054     schedulerStatusLabel->setText(
1055 	!connected ? QSL("not connected") :
1056 	schedulerNeedsConfig ? QSL("pending configuration") :
1057 	schedulerPaused ? QSL("paused") :
1058 	QSL("ready"));
1059     proberInfoLabel->setEnabled(scheduler && scheduler->isOpen());
1060     proberCountdownLabel->setEnabled(scheduler && scheduler->isOpen());
1061     shutdownAct->setEnabled(scheduler && scheduler->isOpen());
1062     pauseAct->setEnabled(opAllowed(config->unprivPref) && !schedulerPaused);
1063     resumeAct->setEnabled(opAllowed(config->unprivPref) && schedulerPaused);
1064     if (schedulerPaused)
1065 	pauseButton->setAction(resumeAct, pauseAct);
1066     else
1067 	pauseButton->setAction(pauseAct, resumeAct);
1068 }
1069 
finishProber()1070 bool MainWindow::finishProber()
1071 {
1072     if (!SpooferUI::finishProber())
1073 	return false;
1074     showStatus();
1075     // show results even if prober didn't exit properly
1076     proberTimeRow->title->setText(QSL("Last run:"));
1077     progRow4->endProber();
1078     progRow6->endProber();
1079 
1080     bool complete = false;
1081     if (currentRun) {
1082 	complete = addHistoryReport(currentRun);
1083 	delete currentRun;
1084 	currentRun = nullptr;
1085     }
1086     if (!complete)
1087 	proberTimeRow->content->setText(
1088 	    proberTimeRow->content->text() % QSL(" (incomplete)"));
1089     return true;
1090 }
1091 
setCountdownUnit(time_t when,time_t now,int count,int secs,QString unit,QString units)1092 bool MainWindow::setCountdownUnit(time_t when, time_t now, int count, int secs,
1093     QString unit, QString units)
1094 {
1095     if ((when - now) > count * secs) {
1096 	time_t n = (when - now + secs/2 - 1) / secs;
1097 	proberCountdownLabel->setText(
1098 	    QSL("(in about %1 %2)").arg(n).arg(n == 1 ? unit : units));
1099 	time_t timeout = std::min(
1100 	    (when - now + secs/2) % secs + 1, // the nearest X.5 units before when
1101 	    (when - now) - count * secs);     // count units before when
1102 	// qDebug() << "countdown: " << ftime(nullptr, &now) << ftime(nullptr, &when);
1103 	// qDebug() << "countdown: label" << proberCountdownLabel->text() << ", diff" << when-now << ", new timeout" << timeout;
1104 	countdownTimer.start(1000 * static_cast<int>(timeout));
1105 	return true;
1106     } else {
1107 	return false;
1108     }
1109 }
1110 
setCountdownLabel()1111 void MainWindow::setCountdownLabel()
1112 {
1113     time_t when = nextProberStart.when;
1114     if (!when) {
1115 	countdownTimer.stop();
1116 	proberCountdownLabel->setText(QString());
1117 	return;
1118     }
1119     time_t now;
1120     time(&now);
1121     if (!setCountdownUnit(when, now, 2, 86400, QSL("day"), QSL("days")) &&
1122 	!setCountdownUnit(when, now, 2, 3600, QSL("hour"), QSL("hours")) &&
1123 	!setCountdownUnit(when, now, 1, 60, QSL("minute"), QSL("minutes")))
1124     {
1125 	proberCountdownLabel->setText(QSL("(in less than 1 minute)"));
1126 	countdownTimer.stop();
1127     }
1128 }
1129 
printNextProberStart()1130 void MainWindow::printNextProberStart()
1131 {
1132     time_t when = nextProberStart.when;
1133     proberInfoLabel->setText(!when ? QSL("none scheduled") :
1134 	QSL("next scheduled for ") % ftime(QString(), &when));
1135     setCountdownLabel();
1136     SpooferUI::printNextProberStart();
1137 }
1138 
startFileTail(QString logname)1139 void MainWindow::startFileTail(QString logname)
1140 {
1141     clearProberMessages(); // clear old prober messages
1142     proberInfoLabel->setText(QSL("in progress"));
1143     proberCountdownLabel->setText(QSL(""));
1144 
1145     fileTail = new FileTailThread(logname);
1146     connect(fileTail, &FileTailThread::dataReady,
1147 	this, &MainWindow::handleProberText,
1148 	Qt::BlockingQueuedConnection);
1149 	// Blocking avoids races between dataReady and finished
1150     connect(fileTail, &FileTailThread::finished,
1151 	this, &MainWindow::finishProber);
1152     fileTail->start();
1153     currentRun = new RunResults(true, logname);
1154     showStatus();
1155     // hide any old results
1156     progRow4->start();
1157     progRow6->start();
1158     initRunDisplay(currentRun, QSL("Started:"));
1159     historyWidget->setRowHidden(0, false); // "live" row is visible by default
1160 }
1161 
hideBlankTests(bool checked)1162 void MainWindow::hideBlankTests(bool checked)
1163 {
1164     if (!historyWidget->rowCount()) return;
1165     bool liveSpan = fileTail ? historyWidget->rowSpan(0, HIST_date) : 0;
1166     for (int i = 0; i < historyWidget->rowCount(); i++) {
1167 	if (!historyWidget->cellWidget(i, HIST_asn))
1168 	    historyWidget->setRowHidden(i, checked && i >= liveSpan);
1169     }
1170     historyWidget->resizeColumnsToContents();
1171 }
1172 
loadHistoricLogs()1173 void MainWindow::loadHistoricLogs()
1174 {
1175     QDir dir(config->dataDir());
1176 
1177     QStringList lognames = dir.entryList(QStringList() << proberLogGlob,
1178 	QDir::Files, QDir::Name);
1179 
1180     for (int i = 0; i < lognames.size(); i++) {
1181 	bool showMessages = (i == lognames.size() - 1);
1182 	QString logname = dir.filePath(lognames.at(i));
1183 	QFile file(logname);
1184 	if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
1185 	    qWarning() << "Failed to load log" <<
1186 		qPrintable(QDir::toNativeSeparators(logname)) << ":" <<
1187 		qPrintable(getLastErrmsg());
1188 	    continue;
1189 	}
1190 	RunResults historicRun(false, logname);
1191 	initRunDisplay(&historicRun, QSL("Last run:"));
1192 	historyWidget->setRowHidden(0, true); // hide until endHistorySession()
1193 	QTextStream in(&file);
1194 	while (!in.atEnd()) {
1195 	    historicRun.parseProberText(in.read(4096), showMessages);
1196 	}
1197 	addHistoryReport(&historicRun);
1198     }
1199 }
1200 
findResults(const QString & ipv,ProgressRow ** progRow,SessionResults ** results)1201 void RunResults::findResults(const QString &ipv, ProgressRow **progRow, SessionResults **results)
1202 {
1203     *progRow = nullptr;
1204     if (results) *results = nullptr;
1205     if (ipv.at(0) == QChar::fromLatin1('6')) {
1206 	if (!this->results6)
1207 	    this->results6 = new SessionResults(6);
1208 	if (results)
1209 	    *results = this->results6;
1210 	if (this->live) {
1211 	    *progRow = MainWindow::instance()->progRow6;
1212 	    (*progRow)->results = this->results6;
1213 	}
1214     } else {
1215 	if (!this->results4)
1216 	    this->results4 = new SessionResults(4);
1217 	if (results)
1218 	    *results = this->results4;
1219 	if (this->live) {
1220 	    *progRow = MainWindow::instance()->progRow4;
1221 	    (*progRow)->results = this->results4;
1222 	}
1223     }
1224 }
1225 
addHistoryCell(int row,int column,Qt::Alignment halign,QLabel * label)1226 void MainWindow::addHistoryCell(int row, int column, Qt::Alignment halign, QLabel *label)
1227 {
1228     ColoredLabel *cl = dynamic_cast<ColoredLabel*>(label);
1229     if (cl && currentRun && currentRun->live) {
1230 	// Fade from yellow to transparent in 10s
1231 	QPropertyAnimation *anim = new QPropertyAnimation(cl, "bgcolor");
1232 	anim->setDuration(10000);
1233 	anim->setStartValue(QColor(255,255,0,255)); // opaque yellow
1234 	anim->setEndValue(QColor(255,255,0,0)); // transparent yellow
1235 	anim->start(QAbstractAnimation::DeleteWhenStopped);
1236     }
1237     label->setAlignment(halign | Qt::AlignVCenter);
1238     label->setContentsMargins(2,2,2,2);
1239     historyWidget->setCellWidget(row, column, label);
1240 }
1241 
addHistoryReport(const RunResults * runResults)1242 bool MainWindow::addHistoryReport(const RunResults *runResults)
1243 {
1244     if (runResults->report.isEmpty()) return false;
1245     addHistoryCell(0, HIST_report, Qt::AlignHCenter, new LinkWidget(QUrl(runResults->report), QSL("report")));
1246     return true;
1247 }
1248 
initRunDisplay(const RunResults * runResults,const QString & timeTitle)1249 void MainWindow::initRunDisplay(const RunResults *runResults, const QString &timeTitle)
1250 {
1251     debugFlags.clear();
1252 
1253     proberTimeRow->title->setText(timeTitle);
1254     proberTimeRow->content->setText(ftime(QString(), &runResults->started));
1255     proberTimeRow->show();
1256 
1257     historyWidget->insertRow(0);
1258 
1259     QLabel *timeLabel =
1260 	new ColoredLabel(ftime(QSL("yyyy-MM-dd HH:mm:ss"), &runResults->started));
1261     // Time zone would be a waste of column space (especially on Windows where
1262     // it's not abbreviated), so we put it in a tool tip instead.
1263     timeLabel->setToolTip(ftime(QSL("t"), &runResults->started));
1264     addHistoryCell(0, HIST_date, Qt::AlignLeft, timeLabel);
1265 
1266     LinkWidget *loglink = new LinkWidget(QUrl::fromLocalFile(runResults->logname), QSL("log"));
1267     addHistoryCell(0, HIST_log, Qt::AlignHCenter, loglink);
1268 
1269     // The showlog column's bgcolor matches the table's bg and borders, so the
1270     // column is effectively invisible.  (And, just in case that doesn't work,
1271     // we can still eliminate the column's internal borders with setSpan.)
1272     auto *showlog = new QLabel();
1273     showlog->setStyleSheet(QSL("background: " HISTORYTABLE_BGCOLOR ";"));
1274     historyWidget->setCellWidget(0, HIST_showlog, showlog);
1275     if (historyWidget->rowCount() > 1)
1276 	historyWidget->setSpan(0, HIST_showlog, historyWidget->rowCount(), 1);
1277 
1278     historyWidget->resizeColumnsToContents();
1279 }
1280 
addHistorySession(SessionResults * results)1281 void MainWindow::addHistorySession(SessionResults *results)
1282 {
1283     if (!results) return;
1284     if (results->row >= 0) return; // already added
1285 
1286     if (!historyWidget->cellWidget(0, HIST_ipv)) {
1287 	// first session of the run; we will fill in the base row
1288 	results->row = 0;
1289     } else {
1290 	// Nth session of the run; we will insert a new row
1291 	results->row = historyWidget->rowSpan(0, HIST_date);
1292 	historyWidget->insertRow(results->row);
1293 
1294 	// make some columns of row 0 span down into the new row
1295 	historyWidget->setSpan(0, HIST_date, results->row + 1, 1);
1296 	historyWidget->setSpan(0, HIST_report, results->row + 1, 1);
1297 	historyWidget->setSpan(0, HIST_log, results->row + 1, 1);
1298     }
1299 
1300     QString ipv = QString::number(results->ipv);
1301     ipv.append(debugFlags);
1302     addHistoryCell(results->row, HIST_ipv, Qt::AlignHCenter, new ColoredLabel(ipv));
1303 
1304     historyWidget->setRowHidden(results->row, !fileTail);
1305     if (fileTail)
1306 	historyWidget->resizeColumnsToContents();
1307 }
1308 
endHistorySession(SessionResults * results)1309 void MainWindow::endHistorySession(SessionResults *results)
1310 {
1311     if (!results) return;
1312 
1313     addHistoryCell(results->row, HIST_asn, Qt::AlignRight,
1314 	new ColoredLabel(results->asn ? QString::number(results->asn) : QString()));
1315     addHistoryCell(results->row, HIST_privaddr, Qt::AlignHCenter,
1316 	resultWidget(results->privaddr));
1317     addHistoryCell(results->row, HIST_routable, Qt::AlignHCenter,
1318 	resultWidget(results->routable));
1319     if (!results->inprivaddr.isNull())
1320 	addHistoryCell(results->row, HIST_inprivaddr, Qt::AlignHCenter,
1321 	    resultWidget(results->inprivaddr));
1322     if (!results->ininternal.isNull())
1323 	addHistoryCell(results->row, HIST_ininternal, Qt::AlignHCenter,
1324 	    resultWidget(results->ininternal));
1325 
1326     historyWidget->setRowHidden(results->row, false); // row with results should always be visible
1327     historyWidget->resizeColumnsToContents();
1328     results->row = -1;
1329 }
1330 
1331 // Delete all but the n most recent prober messages
clearProberMessages(int n)1332 void MainWindow::clearProberMessages(int n)
1333 {
1334 #ifdef AUTOUPGRADE_ENABLED
1335     upgradeResultBox->hide();
1336 #endif
1337     while (this->nextProberMsgIdx > this->firstProberMsgIdx + n) {
1338 	QLayoutItem *item =
1339 	    this->proberBoxLayout->takeAt(this->firstProberMsgIdx);
1340 	if (item && item->widget()) { // should never be false
1341 	    item->widget()->hide();
1342 	    delete item;
1343 	}
1344 	this->nextProberMsgIdx--;
1345     }
1346 }
1347 
displayProberMessage(const QString & text,bool enabled,const QColor & color)1348 void MainWindow::displayProberMessage(const QString &text, bool enabled,
1349     const QColor &color)
1350 {
1351     clearProberMessages(4); // Keep only the last 4 messages
1352 
1353     QLabel *label = new QLabel(text);
1354     label->setTextFormat(Qt::PlainText);
1355     label->setWordWrap(true);
1356     label->setTextInteractionFlags(Qt::TextSelectableByMouse);
1357     if (color.isValid()) {
1358 	QPalette pal = label->palette();
1359 	pal.setColor(QPalette::Active, QPalette::WindowText, color);
1360 	pal.setColor(QPalette::Inactive, QPalette::WindowText, color);
1361 	label->setPalette(pal);
1362     }
1363     label->setEnabled(enabled);
1364     label->setContentsMargins(20,0,0,0);
1365     this->proberBoxLayout->insertWidget(this->nextProberMsgIdx++, label);
1366 }
1367 
proberError(const QString & text)1368 void MainWindow::proberError(const QString &text)
1369 {
1370     if (!currentRun) // not part of an already-in-progress run
1371 	clearProberMessages();
1372     displayProberMessage(QSL("Prober error: ") + text, true, Qt::darkRed);
1373 }
1374 
parseProberText(const QString & text,bool showMessages)1375 void RunResults::parseProberText(const QString &text, bool showMessages)
1376 {
1377     static QString oldtext;
1378     static int offset = 0;
1379     // NB: If format changes, don't forget to update openwrt-files/spoofer
1380     static QString basePattern = QSL(
1381 	// prober version, if it exactly matches ours
1382 	">> \\Q" PACKAGE_DESC "\\E version (?<proberversion>\\Q" PACKAGE_VERSION "\\E)\n|"
1383 	// debugging flags
1384 	">>   (?<debug>standaloneMode|pretendMode|useDevServer)\n|"
1385 	// start session
1386 	"# ServerMessage \\(IPv(?<startv>[46])\\):\n"
1387 	"(#  (?!hello:)\\S.*\n)*" // e.g. "textmsg"
1388 	"#  hello:\n"
1389 	"(#   (?!clientip:)\\S.*\n)*"
1390 	"(?:#   clientip: (?<addr>[0-9a-fA-F.:]+)\n)?|"
1391 	// result summary (note: ", egress" substrings and optional ingress
1392 	// lines were added in prober 1.2.0.  "Ingress"/"egress" changed to
1393 	// "inbound"/"outbound" in 1.4.3.)
1394 	">> IPv(?<endv>[46]) Result Summary:\n"
1395 	">> +ASN: (?<asn>\\d+)\n"
1396 	">> +Spoofed private addresses(, (egress|outbound))?: (?<privaddr>\\w+)\n"
1397 	">> +Spoofed routable addresses(, (egress|outbound))?: (?<routable>\\w+)\n"
1398 	"(?:>> +Spoofed private addresses, (ingress|inbound): (?<inprivaddr>\\w+)\n)?"
1399 	"(?:>> +Spoofed internal addresses, (ingress|inbound): (?<ininternal>\\w+)\n)?|"
1400 	// report url
1401 	"Your test results:\\s+(?<report>\\S+)\n|"
1402 	// server or scheduler message
1403 	"(?<msg>[*][*][*] (?<msgpfx>(?:IPv[46] server )?"
1404 	    "(?<msgtype>[Ee]rror|[Ww]arning|[Nn]otice)): (?<msgtxt>.*))\n"
1405 	);
1406 
1407     static QRegularExpression baseRe(basePattern);
1408 
1409     // omitting this from baseRe speeds up loadHistoricLogs()
1410     static QRegularExpression liveRe(basePattern + QSL("|"
1411 	// start stage
1412 	">> Running IPv(?<stagev>[46]) (?<stagetext>test \\d+:.*)\n|"
1413 	// stage progress
1414 	"\\Q" PROGRESS_PREFIX "\\E  (?<progresstext>.*)\n|"
1415 	));
1416 
1417     const QString *subject = &text;
1418     if (!oldtext.isNull()) {
1419 	// append new text to old partially-matched text
1420 	oldtext += text;
1421 	subject = &oldtext;
1422     }
1423 
1424     QRegularExpression *re = this->live ? &liveRe : &baseRe;
1425     QRegularExpressionMatchIterator it = re->globalMatch(*subject, offset,
1426 	QRegularExpression::PartialPreferFirstMatch);
1427     while (it.hasNext()) {
1428 	QRegularExpressionMatch match = it.next();
1429 	if (match.hasPartialMatch()) {
1430 	    // save partially matched text and offset to try again later
1431 	    offset = match.capturedStart(0);
1432 	    if (oldtext.isNull())
1433 		oldtext = text;
1434 	    return;
1435 	}
1436 	ProgressRow *progRow = nullptr;
1437 	SessionResults *results = nullptr;
1438 	if (!match.captured(QSL("proberversion")).isNull()) {
1439 	    this->proberVersionMatch = true;
1440 	} else if (!match.captured(QSL("debug")).isNull()) {
1441 	    if (match.captured(QSL("debug")).compare(QSL("standaloneMode")) == 0)
1442 		MainWindow::instance()->debugFlags.append(QSL("S"));
1443 	    if (match.captured(QSL("debug")).compare(QSL("pretendMode")) == 0)
1444 		MainWindow::instance()->debugFlags.append(QSL("P"));
1445 	    if (match.captured(QSL("debug")).compare(QSL("useDevServer")) == 0)
1446 		MainWindow::instance()->debugFlags.append(QSL("T"));
1447 	} else if (!match.captured(QSL("startv")).isNull()) {
1448 	    this->findResults(match.captured(QSL("startv")), &progRow, &results);
1449 	    MainWindow::instance()->addHistorySession(results);
1450 	    if (!match.captured(QSL("addr")).isNull()) {
1451 		results->ipaddr = match.captured(QSL("addr"));
1452 		QLabel *longlabel = new ColoredLabel(results->ipaddr);
1453 		QLabel *shortlabel;
1454 		if (results->ipv == 6) {
1455 		    QString shortaddr(SubnetAddr(QHostAddress(results->ipaddr), 64).prefix().toString());
1456 		    shortlabel = new ColoredLabel(shortaddr);
1457 		} else {
1458 		    shortlabel = new ColoredLabel(results->ipaddr);
1459 		}
1460 		MainWindow::instance()->addHistoryCell(results->row, HIST_ipaddr_short,
1461 		    Qt::AlignLeft, shortlabel);
1462 		MainWindow::instance()->addHistoryCell(results->row, HIST_ipaddr_long,
1463 		    Qt::AlignLeft, longlabel);
1464 	    }
1465 	} else if (!match.captured(QSL("endv")).isNull()) {
1466 	    this->findResults(match.captured(QSL("endv")), &progRow, &results);
1467 	    results->asn = match.captured(QSL("asn")).toUInt();
1468 	    results->privaddr = match.captured(QSL("privaddr"));
1469 	    results->routable = match.captured(QSL("routable"));
1470 	    results->inprivaddr = match.captured(QSL("inprivaddr"));
1471 	    results->ininternal = match.captured(QSL("ininternal"));
1472 	    if (this->live) progRow->endSession();
1473 	    MainWindow::instance()->endHistorySession(results);
1474 	} else if (!match.captured(QSL("report")).isNull()) {
1475 	    this->report = match.captured(QSL("report"));
1476 	} else if ((this->live || (showMessages && this->proberVersionMatch)) &&
1477 	    !match.captured(QSL("msgtype")).isNull())
1478 	{
1479 	    // The proberVersionMatch requirement prevents us from displaying
1480 	    // a misleading out-of-date message like "new version available"
1481 	    // when the client has been upgraded since the logged prober run.
1482 	    QString msgtype = match.captured(QSL("msgtype")).toLower();
1483 	    QString msgtxt = match.captured(QSL("msgtxt"));
1484 	    if (msgtxt.startsWith(QSL("Upgrade available:"))) {
1485 		// message will be handled by SC_UPGRADE_AVAILABLE
1486 	    } else {
1487 		QColor color;
1488 		if (msgtype.compare(QSL("error"), Qt::CaseInsensitive) == 0)
1489 		    color = Qt::darkRed;
1490 		else if (msgtype.compare(QSL("warning"), Qt::CaseInsensitive) == 0)
1491 		    color = Qt::darkMagenta;
1492 		MainWindow::instance()->displayProberMessage(match.captured(QSL("msg")), this->live, color);
1493 	    }
1494 	} else if (!this->live) {
1495 	    // Do nothing.  Remaining matches are only for live runs.
1496 	} else if (!match.captured(QSL("stagev")).isNull()) {
1497 	    this->findResults(match.captured(QSL("stagev")), &progRow, nullptr);
1498 	    progRow->stage(match.captured(QSL("stagetext")));
1499 	} else if (!match.captured(QSL("progresstext")).isNull()) {
1500 	    QString progText = match.captured(QSL("progresstext"));
1501 	    QRegularExpression progRe(QSL("IPv(?<v>[46]): (\\d+\\+\\d+/)?(?<tries>\\d+)/(?<goal>\\d+)"));
1502 	    QRegularExpressionMatchIterator progIt = progRe.globalMatch(progText);
1503 	    while (progIt.hasNext()) {
1504 		QRegularExpressionMatch progMatch = progIt.next();
1505 		int tries = progMatch.captured(QSL("tries")).toInt();
1506 		int goal = progMatch.captured(QSL("goal")).toInt();
1507 		this->findResults(progMatch.captured(QSL("v")), &progRow, nullptr);
1508 		progRow->tick(tries, goal);
1509 	    }
1510 	}
1511     }
1512     oldtext.clear();
1513     offset = 0;
1514 }
1515 
handleProberText(QString * text)1516 void MainWindow::handleProberText(QString *text)
1517 {
1518     currentRun->parseProberText(*text);
1519     QTextCharFormat oldfmt = winout->setProberCharFmt();
1520     SpooferUI::handleProberText(text);
1521     winout->setCharFmt(oldfmt);
1522 }
1523 
schedDisconnected()1524 void MainWindow::schedDisconnected()
1525 {
1526     qWarning() << "Scheduler disconnected";
1527     schedCleanup(false);
1528 }
1529 
watchForSchedulerRestart()1530 void MainWindow::watchForSchedulerRestart()
1531 {
1532     // When scheduler starts, it will update the settings and create a lock
1533     // file in dataDir.  So we watch for changes in one of those.
1534     QString file = config->isFile() ? config->fileName() : QString();
1535     QString dir = config->dataDir();
1536     if (!schedWatcher) {
1537 	schedWatcher = new QFileSystemWatcher();
1538 	connect(schedWatcher, &QFileSystemWatcher::fileChanged,
1539 	    this, &MainWindow::reconnectScheduler);
1540 	connect(schedWatcher, &QFileSystemWatcher::directoryChanged,
1541 	    this, &MainWindow::reconnectScheduler);
1542     }
1543 
1544     // Note: after a change is signaled for a watched path, that path may or
1545     // may not still be in the watch list.
1546     if (!file.isNull())
1547 	schedWatcher->addPath(file);
1548 
1549     if (schedWatcher->files().isEmpty() && !dir.isNull())
1550 	schedWatcher->addPath(dir);
1551 
1552     if (schedWatcher->files().isEmpty() && schedWatcher->directories().isEmpty()) {
1553 	delete schedWatcher;
1554 	schedWatcher = nullptr;
1555 	qDebug() << "watching failed!";
1556 	return;
1557     }
1558     spout << "Waiting for scheduler to start..." << Qt_endl;
1559     qDebug() << "watching: " << schedWatcher->files() << " " << schedWatcher->directories();
1560 }
1561 
reconnectScheduler()1562 void MainWindow::reconnectScheduler()
1563 {
1564     qDebug() << "reconnectScheduler";
1565     QThread::msleep(1000); // give scheduler a chance to get going
1566     if (!connectScheduler(true))
1567 	watchForSchedulerRestart();
1568 }
1569 
runProber()1570 void MainWindow::runProber() {
1571     qDebug() << "### signal: run";
1572     if (!fileTail) {
1573 	scheduler->write("run\n");
1574     } else {
1575 	qDebug() << "### prober is already running";
1576     }
1577 }
1578 
abortProber()1579 void MainWindow::abortProber() {
1580     qDebug() << "### signal: abort";
1581     if (fileTail) {
1582 	scheduler->write("abort\n");
1583     } else {
1584 	qDebug() << "### prober is not running";
1585     }
1586 }
1587 
pauseScheduler()1588 void MainWindow::pauseScheduler() {
1589     qDebug() << "### signal: pause";
1590     scheduler->write("pause\n");
1591 }
1592 
resumeScheduler()1593 void MainWindow::resumeScheduler() {
1594     qDebug() << "### signal: resume";
1595     scheduler->write("resume\n");
1596 }
1597 
shutdownScheduler()1598 void MainWindow::shutdownScheduler() {
1599     QMessageBox mb;
1600     mb.setIcon(QMessageBox::Warning);
1601     mb.setText(QSL("<b>Shut down Spoofer scheduler?</b>"));
1602     mb.setInformativeText(QSL("Once the scheduler is shut down, this GUI will "
1603 	"not be able to restart it or do anything else; and it may restart "
1604 	"automatically at next reboot depending on its configuration.  Use "
1605 	"\"Pause Scheduler\" instead to disable scheduled tests in a way that "
1606 	"can be easily re-enabled and will persist across reboots."));
1607     mb.setStandardButtons(QMessageBox::No | QMessageBox::Yes);
1608     if (mb.exec() == QMessageBox::Yes)
1609 	scheduler->write("shutdown\n");
1610 }
1611 
1612