b9cf703c5ed351776a4042a3ea9476b81e7ef3e9
[dhcpcd-ui] / src / dhcpcd-qt / dhcpcd-qt.cpp
1 /*
2  * dhcpcd-qt
3  * Copyright 2014-2017 Roy Marples <roy@marples.name>
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
15  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
18  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24  * SUCH DAMAGE.
25  */
26
27 #include <QCursor>
28 #include <QDebug>
29 #include <QList>
30 #include <QSocketNotifier>
31 #include <QtGui>
32
33 #include <cerrno>
34
35 #include "config.h"
36 #include "dhcpcd-qt.h"
37 #include "dhcpcd-about.h"
38 #include "dhcpcd-preferences.h"
39 #include "dhcpcd-wi.h"
40 #include "dhcpcd-ifmenu.h"
41 #include "dhcpcd-ssidmenu.h"
42
43 #if defined(KNOTIFY5)
44 #include <KNotification>
45 #elif defined(KNOTIFY4)
46 #include <knotification.h>
47 #endif
48
49 DhcpcdQt::DhcpcdQt()
50 {
51
52         createActions();
53         createTrayIcon();
54
55         onLine = carrier = false;
56         lastStatus = DHC_UNKNOWN;
57         aniTimer = new QTimer(this);
58         connect(aniTimer, SIGNAL(timeout()), this, SLOT(animate()));
59         notifier = NULL;
60         retryOpenTimer = NULL;
61
62         about = NULL;
63         preferences = NULL;
64
65         wis = new QList<DhcpcdWi *>();
66         ssidMenu = NULL;
67
68         qDebug("%s", "Connecting ...");
69         con = dhcpcd_new();
70         if (con == NULL) {
71                 qCritical("libdhcpcd: %s", strerror(errno));
72                 exit(EXIT_FAILURE);
73                 return;
74         }
75         dhcpcd_set_progname(con, "dhcpcd-qt");
76         dhcpcd_set_status_callback(con, dhcpcd_status_cb, this);
77         dhcpcd_set_if_callback(con, dhcpcd_if_cb, this);
78         dhcpcd_wpa_set_scan_callback(con, dhcpcd_wpa_scan_cb, this);
79         dhcpcd_wpa_set_status_callback(con, dhcpcd_wpa_status_cb, this);
80         tryOpen();
81 }
82
83 DhcpcdQt::~DhcpcdQt()
84 {
85
86         if (ssidMenu) {
87                 ssidMenu->setVisible(false);
88                 ssidMenu->deleteLater();
89         }
90
91         if (con != NULL) {
92                 dhcpcd_close(con);
93                 dhcpcd_free(con);
94         }
95
96         for (auto &wi : *wis)
97                 wi->deleteLater();
98         delete wis;
99 }
100
101 DHCPCD_CONNECTION *DhcpcdQt::getConnection()
102 {
103
104         return con;
105 }
106
107 QList<DhcpcdWi *> *DhcpcdQt::getWis()
108 {
109
110         return wis;
111 }
112
113 const char * DhcpcdQt::signalStrengthIcon(DHCPCD_WI_SCAN *scan)
114 {
115
116         if (scan == NULL)
117                 return "network-wireless-connected-00";
118         if (scan->strength.value > 80)
119                 return "network-wireless-connected-100";
120         if (scan->strength.value > 55)
121                 return "network-wireless-connected-75";
122         if (scan->strength.value > 30)
123                 return "network-wireless-connected-50";
124         if (scan->strength.value > 5)
125                 return "network-wireless-connected-25";
126         return "network-wireless-connected-00";
127 }
128
129 DHCPCD_WI_SCAN * DhcpcdQt::getStrongestSignal()
130 {
131         DHCPCD_WI_SCAN *scan, *scans, *s;
132         DHCPCD_WPA *wpa;
133         DHCPCD_IF *i;
134
135         scan = NULL;
136         for (auto &wi : *wis) {
137                 wpa = wi->getWpa();
138                 i = dhcpcd_wpa_if(wpa);
139                 scans = wi->getScans();
140                 for (s = scans; s; s = s->next) {
141                         if (dhcpcd_wi_associated(i, s) &&
142                             (scan == NULL ||
143                             s->strength.value > scan->strength.value))
144                                 scan = s;
145                 }
146         }
147         return scan;
148 }
149
150 void DhcpcdQt::animate()
151 {
152         const char *icon;
153         DHCPCD_WI_SCAN *scan;
154
155         scan = getStrongestSignal();
156
157         if (onLine) {
158                 if (aniCounter++ > 6) {
159                         aniTimer->stop();
160                         aniCounter = 0;
161                         return;
162                 }
163
164                 if (aniCounter % 2 == 0)
165                         icon = scan ? "network-wireless-connected-00" :
166                             "network-idle";
167                 else
168                         icon = scan ? DhcpcdQt::signalStrengthIcon(scan) :
169                             "network-transmit-receive";
170         } else {
171                 if (scan) {
172                         switch(aniCounter++) {
173                         case 0:
174                                 icon = "network-wireless-connected-00";
175                                 break;
176                         case 1:
177                                 icon = "network-wireless-connected-25";
178                                 break;
179                         case 2:
180                                 icon = "network-wireless-connected-50";
181                                 break;
182                         case 3:
183                                 icon = "network-wireless-connected-75";
184                                 break;
185                         default:
186                                 icon = "network-wireless-connected-100";
187                                 aniCounter = 0;
188                         }
189                 } else {
190                         switch(aniCounter++) {
191                         case 0:
192                                 icon = "network-transmit";
193                                 break;
194                         case 1:
195                                 icon = "network-receive";
196                                 break;
197                         default:
198                                 icon = "network-idle";
199                                 aniCounter = 0;
200                         }
201                 }
202         }
203
204         setIcon("status", icon);
205 }
206
207 void DhcpcdQt::updateOnline(bool showIf)
208 {
209         bool isOn, isCarrier;
210         char *msg;
211         DHCPCD_IF *ifs, *i;
212         QString msgs;
213
214         isOn = isCarrier = false;
215         ifs = dhcpcd_interfaces(con);
216         for (i = ifs; i; i = i->next) {
217                 if (i->type == DHT_LINK) {
218                         if (i->up)
219                                 isCarrier = true;
220                 } else {
221                         if (i->up)
222                                 isOn = true;
223                 }
224                 msg = dhcpcd_if_message(i, NULL);
225                 if (msg) {
226                         if (showIf)
227                                 qDebug("%s", msg);
228                         if (msgs.isEmpty())
229                                 msgs = QString::fromLatin1(msg);
230                         else
231                                 msgs += '\n' + QString::fromLatin1(msg);
232                         free(msg);
233                 } else if (showIf)
234                         qDebug("%s: %s", i->ifname, i->reason);
235         }
236
237         if (onLine != isOn || carrier != isCarrier) {
238                 onLine = isOn;
239                 carrier = isCarrier;
240                 aniTimer->stop();
241                 aniCounter = 0;
242                 if (isOn) {
243                         animate();
244                         aniTimer->start(300);
245                 } else if (isCarrier) {
246                         animate();
247                         aniTimer->start(500);
248                 } else
249                         setIcon("status", "network-offline");
250         }
251
252         trayIcon->setToolTip(msgs);
253 }
254
255 void DhcpcdQt::statusCallback(unsigned int status, const char *status_msg)
256 {
257
258         qDebug("Status changed to %s", status_msg);
259         if (status == DHC_DOWN) {
260                 aniTimer->stop();
261                 aniCounter = 0;
262                 onLine = carrier = false;
263                 setIcon("status", "network-offline");
264                 trayIcon->setToolTip(tr("Not connected to dhcpcd"));
265                 /* Close down everything */
266                 if (notifier) {
267                         notifier->setEnabled(false);
268                         notifier->deleteLater();
269                         notifier = NULL;
270                 }
271                 if (ssidMenu) {
272                         ssidMenu->deleteLater();
273                         ssidMenu = NULL;
274                 }
275                 preferencesAction->setEnabled(false);
276                 if (preferences) {
277                         preferences->deleteLater();
278                         preferences = NULL;
279                 }
280         } else {
281                 bool refresh;
282
283                 if (lastStatus == DHC_UNKNOWN || lastStatus == DHC_DOWN) {
284                         qDebug("Connected to dhcpcd-%s", dhcpcd_version(con));
285                         refresh = true;
286                 } else
287                         refresh = lastStatus == DHC_OPENED ? true : false;
288                 updateOnline(refresh);
289         }
290
291         lastStatus = status;
292
293         if (status == DHC_DOWN) {
294                 if (retryOpenTimer == NULL) {
295                         retryOpenTimer = new QTimer(this);
296                         connect(retryOpenTimer, SIGNAL(timeout()),
297                             this, SLOT(tryOpen()));
298                         retryOpenTimer->start(DHCPCD_RETRYOPEN);
299                 }
300         }
301 }
302
303 void DhcpcdQt::dhcpcd_status_cb(_unused DHCPCD_CONNECTION *con,
304     unsigned int status, const char *status_msg, void *d)
305 {
306         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
307
308         dhcpcdQt->statusCallback(status, status_msg);
309 }
310
311 void DhcpcdQt::ifCallback(DHCPCD_IF *i)
312 {
313         char *msg;
314         bool new_msg;
315
316         if (i->state == DHS_RENEW ||
317             i->state == DHS_STOP || i->state == DHS_STOPPED)
318         {
319                 msg = dhcpcd_if_message(i, &new_msg);
320                 if (msg) {
321                         qDebug("%s", msg);
322                         if (new_msg) {
323                                 QString t = tr("Network Event");
324                                 QString m = msg;
325                                 QString icon;
326
327                                 if (i->up)
328                                         icon = "network-transmit-receive";
329                                 //else
330                                 //      icon = "network-transmit";
331                                 if (!i->up)
332                                         icon = "network-offline";
333                                 notify(t, m, icon);
334                         }
335                         free(msg);
336                 }
337         }
338
339         updateOnline(false);
340
341         if (i->wireless) {
342                 for (auto &wi : *wis) {
343                         DHCPCD_WPA *wpa = wi->getWpa();
344                         if (dhcpcd_wpa_if(wpa) == i) {
345                                 DHCPCD_WI_SCAN *scans;
346
347                                 scans = dhcpcd_wi_scans(i);
348                                 processScans(wi, scans);
349                         }
350                 }
351         }
352 }
353
354 void DhcpcdQt::dhcpcd_if_cb(DHCPCD_IF *i, void *d)
355 {
356         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
357
358         dhcpcdQt->ifCallback(i);
359 }
360
361 DhcpcdWi *DhcpcdQt::findWi(DHCPCD_WPA *wpa)
362 {
363
364         for (auto &wi : *wis) {
365                 if (wi->getWpa() == wpa)
366                         return wi;
367         }
368         return NULL;
369 }
370
371 void DhcpcdQt::processScans(DhcpcdWi *wi, DHCPCD_WI_SCAN *scans)
372 {
373
374         /* Don't spam the user if we're already connected. */
375         if (lastStatus != DHC_CONNECTED) {
376                 QString title = tr("New Access Point");
377                 QString txt;
378                 DHCPCD_WI_SCAN *s1, *s2;
379
380                 for (s1 = scans; s1; s1 = s1->next) {
381                         for (s2 = wi->getScans(); s2; s2 = s2->next) {
382                                 if (strcmp(s1->ssid, s2->ssid) == 0)
383                                         break;
384                         }
385                         if (s2 == NULL) {
386                                 if (!txt.isEmpty()) {
387                                         title = tr("New Access Points");
388                                         txt += '\n';
389                                 }
390                                 txt += s1->ssid;
391                         }
392                 }
393                 if (!txt.isEmpty() &&
394                     (ssidMenu == NULL || !ssidMenu->isVisible()))
395                         notify(title, txt, "network-wireless");
396         }
397
398         if (wi->setScans(scans) && ssidMenu && ssidMenu->isVisible())
399                 ssidMenu->popup(ssidMenuPos);
400 }
401
402 void DhcpcdQt::scanCallback(DHCPCD_WPA *wpa)
403 {
404         DHCPCD_WI_SCAN *scans;
405         int fd = dhcpcd_wpa_get_fd(wpa);
406         DhcpcdWi *wi;
407
408         wi = findWi(wpa);
409         if (fd == -1) {
410                 qCritical("No fd for WPA");
411                 if (wi) {
412                         wis->removeOne(wi);
413                         wi->close();
414                         wi->deleteLater();
415                 }
416                 return;
417         }
418
419         DHCPCD_IF *i = dhcpcd_wpa_if(wpa);
420         if (i == NULL) {
421                 qCritical("No interface for WPA");
422                 if (wi) {
423                         wis->removeOne(wi);
424                         wi->close();
425                         wi->deleteLater();
426                 }
427                 return;
428         }
429
430         qDebug("%s: Received scan results", i->ifname);
431         scans = dhcpcd_wi_scans(i);
432         if (wi == NULL) {
433                 wi = new DhcpcdWi(this, wpa);
434                 if (wi->open()) {
435                         wis->append(wi);
436                         wi->setScans(scans);
437                 } else {
438                         wi->close();
439                         wi->deleteLater();
440                 }
441         } else
442                 processScans(wi, scans);
443
444         if (!aniTimer->isActive()) {
445                 DHCPCD_WI_SCAN *scan;
446                 const char *icon;
447
448                 scan = getStrongestSignal();
449                 if (scan)
450                         icon = DhcpcdQt::signalStrengthIcon(scan);
451                 else if (onLine)
452                         icon = "network-transmit-receive";
453                 else
454                         icon = "network-offline";
455
456                 setIcon("status", icon);
457         }
458 }
459
460 void DhcpcdQt::dhcpcd_wpa_scan_cb(DHCPCD_WPA *wpa, void *d)
461 {
462         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
463
464         dhcpcdQt->scanCallback(wpa);
465 }
466
467 void DhcpcdQt::wpaStatusCallback(DHCPCD_WPA *wpa,
468     unsigned int status, const char *status_msg)
469 {
470         DHCPCD_IF *i;
471
472         i = dhcpcd_wpa_if(wpa);
473         qDebug("%s: WPA status %s", i->ifname, status_msg);
474         if (status == DHC_DOWN) {
475                 DhcpcdWi *wi = findWi(wpa);
476                 if (wi) {
477                         wis->removeOne(wi);
478                         wi->close();
479                         wi->deleteLater();
480                 }
481         }
482 }
483
484 void DhcpcdQt::dhcpcd_wpa_status_cb(DHCPCD_WPA *wpa,
485     unsigned int status, const char *status_msg, void *d)
486 {
487         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
488
489         dhcpcdQt->wpaStatusCallback(wpa, status, status_msg);
490 }
491
492 void DhcpcdQt::tryOpen() {
493         int fd = dhcpcd_open(con, true);
494         static int last_error;
495
496         if (fd == -1) {
497                 if (errno == EACCES || errno == EPERM) {
498                         if ((fd = dhcpcd_open(con, false)) != -1)
499                                 goto unprived;
500                 }
501                 if (errno != last_error) {
502                         last_error = errno;
503                         const char *errt = strerror(errno);
504                         qCritical("dhcpcd_open: %s", errt);
505                         trayIcon->setToolTip(
506                             tr("Error connecting to dhcpcd: %1").arg(errt));
507                 }
508                 if (retryOpenTimer == NULL) {
509                         retryOpenTimer = new QTimer(this);
510                         connect(retryOpenTimer, SIGNAL(timeout()),
511                             this, SLOT(tryOpen()));
512                         retryOpenTimer->start(DHCPCD_RETRYOPEN);
513                 }
514                 return;
515         }
516
517 unprived:
518         /* Start listening to WPA events */
519         dhcpcd_wpa_start(con);
520
521         if (retryOpenTimer) {
522                 retryOpenTimer->stop();
523                 retryOpenTimer->deleteLater();
524                 retryOpenTimer = NULL;
525         }
526
527         notifier = new QSocketNotifier(fd, QSocketNotifier::Read);
528         connect(notifier, SIGNAL(activated(int)), this, SLOT(dispatch()));
529
530         preferencesAction->setEnabled(dhcpcd_privileged(con));
531 }
532
533 void DhcpcdQt::dispatch()
534 {
535
536         dhcpcd_dispatch(con);
537 }
538
539 void DhcpcdQt::notify(const QString &title, const QString &msg,
540     const QString &icon)
541 {
542
543 #if defined(KNOTIFY4) || defined(KNOTIFY5)
544         KNotification *n = new KNotification("event", this);
545         n->setIconName(icon);
546         n->setTitle(title);
547         n->setText(msg);
548         n->sendEvent();
549 #else
550 #if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
551         const QIcon i = getIcon("status", icon);
552 #else
553         QSystemTrayIcon::MessageIcon i = QSystemTrayIcon::Information;
554
555         if (icon.compare("network-offline") == 0)
556                 i = QSystemTrayIcon::Warning;
557 #endif
558         trayIcon->showMessage(title, msg, i);
559 #endif
560 }
561
562 void DhcpcdQt::closeEvent(QCloseEvent *event)
563 {
564
565         if (trayIcon->isVisible()) {
566                 hide();
567                 event->ignore();
568         }
569 }
570
571 QIcon DhcpcdQt::getIcon(QString category, QString name)
572 {
573         QIcon icon;
574
575         if (QIcon::hasThemeIcon(name))
576                 icon = QIcon::fromTheme(name);
577         else {
578                 /* For some reason, SVG no longer displays ... */
579                 QString file = QString("%1/hicolor/22x22/%2/%3.png")
580                     .arg(ICONDIR, category, name);
581                 icon = QIcon(file);
582         }
583
584         return icon;
585 }
586
587 void DhcpcdQt::setIcon(QString category, QString name)
588 {
589         QIcon icon = getIcon(category, name);
590
591         trayIcon->setIcon(icon);
592 }
593
594 QIcon DhcpcdQt::icon()
595 {
596
597         return getIcon("status", "network-transmit-receive");
598 }
599
600 void DhcpcdQt::menuDeleted(QMenu *menu)
601 {
602
603         if (ssidMenu == menu)
604                 ssidMenu = NULL;
605 }
606
607 void DhcpcdQt::createSsidMenu()
608 {
609
610         if (ssidMenu) {
611                 ssidMenu->deleteLater();
612                 ssidMenu = NULL;
613         }
614         if (wis->size() == 0)
615                 return;
616
617         ssidMenu = new QMenu(this);
618         if (wis->size() == 1)
619                 wis->first()->createMenu(ssidMenu);
620         else {
621                 for (auto &wi : *wis)
622                         ssidMenu->addMenu(wi->createIfMenu(ssidMenu));
623         }
624         ssidMenuPos = QCursor::pos();
625         ssidMenu->popup(ssidMenuPos);
626 }
627
628 void DhcpcdQt::iconActivated(QSystemTrayIcon::ActivationReason reason)
629 {
630
631         if (reason == QSystemTrayIcon::Trigger)
632                 createSsidMenu();
633 }
634
635 void DhcpcdQt::dialogClosed(QDialog *dialog)
636 {
637
638         if (dialog == about)
639                 about = NULL;
640         else if (dialog == preferences)
641                 preferences = NULL;
642 }
643
644 void DhcpcdQt::showPreferences()
645 {
646
647         if (preferences == NULL) {
648                 preferences = new DhcpcdPreferences(this);
649                 preferences->show();
650         } else
651                 preferences->activateWindow();
652 }
653
654 void DhcpcdQt::showAbout()
655 {
656
657         if (about == NULL) {
658                 about = new DhcpcdAbout(this);
659                 about->show();
660         } else
661                 about->activateWindow();
662 }
663
664 void DhcpcdQt::createActions()
665 {
666
667         preferencesAction = new QAction(tr("&Preferences"), this);
668         preferencesAction->setIcon(QIcon::fromTheme("preferences-system-network"));
669         preferencesAction->setEnabled(false);
670         connect(preferencesAction, SIGNAL(triggered()),
671             this, SLOT(showPreferences()));
672
673         aboutAction = new QAction(tr("&About"), this);
674         aboutAction->setIcon(QIcon::fromTheme("help-about"));
675         connect(aboutAction, SIGNAL(triggered()), this, SLOT(showAbout()));
676
677         quitAction = new QAction(tr("&Quit"), this);
678         quitAction->setIcon(QIcon::fromTheme("application-exit"));
679         connect(quitAction, SIGNAL(triggered()), qApp, SLOT(quit()));
680
681 }
682
683 void DhcpcdQt::createTrayIcon()
684 {
685
686         trayIconMenu = new QMenu(this);
687         trayIconMenu->addAction(preferencesAction);
688         trayIconMenu->addSeparator();
689         trayIconMenu->addAction(aboutAction);
690         trayIconMenu->addAction(quitAction);
691
692         trayIcon = new QSystemTrayIcon(this);
693         setIcon("status", "network-offline");
694         trayIcon->setContextMenu(trayIconMenu);
695
696         connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
697             this, SLOT(iconActivated(QSystemTrayIcon::ActivationReason)));
698
699         trayIcon->show();
700 }