Fix a crash.
[dhcpcd-ui] / src / dhcpcd-qt / dhcpcd-qt.cpp
1 /*
2  * dhcpcd-qt
3  * Copyright 2014 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 DhcpcdQt::DhcpcdQt()
44 {
45
46         createActions();
47         createTrayIcon();
48
49         onLine = carrier = false;
50         lastStatus = NULL;
51         aniTimer = new QTimer(this);
52         connect(aniTimer, SIGNAL(timeout()), this, SLOT(animate()));
53         notifier = NULL;
54         retryOpenTimer = NULL;
55
56         about = NULL;
57         preferences = NULL;
58
59         wis = new QList<DhcpcdWi *>();
60         ssidMenu = NULL;
61
62         qDebug("%s", "Connecting ...");
63         con = dhcpcd_new();
64         if (con == NULL) {
65                 qCritical("libdhcpcd: %s", strerror(errno));
66                 exit(EXIT_FAILURE);
67                 return;
68         }
69         dhcpcd_set_progname(con, "dhcpcd-qt");
70         dhcpcd_set_status_callback(con, dhcpcd_status_cb, this);
71         dhcpcd_set_if_callback(con, dhcpcd_if_cb, this);
72         dhcpcd_wpa_set_scan_callback(con, dhcpcd_wpa_scan_cb, this);
73         dhcpcd_wpa_set_status_callback(con, dhcpcd_wpa_status_cb, this);
74         tryOpen();
75 }
76
77 DhcpcdQt::~DhcpcdQt()
78 {
79
80         /* This will have already been destroyed,
81          * but the reference may not be. */
82         ssidMenu = NULL;
83
84         if (con != NULL) {
85                 dhcpcd_close(con);
86                 dhcpcd_free(con);
87         }
88
89         free(lastStatus);
90
91         qDeleteAll(*wis);
92         delete wis;
93
94 }
95
96 DHCPCD_CONNECTION *DhcpcdQt::getConnection()
97 {
98
99         return con;
100 }
101
102 QList<DhcpcdWi *> *DhcpcdQt::getWis()
103 {
104
105         return wis;
106 }
107
108 void DhcpcdQt::animate()
109 {
110         const char *icon;
111
112         if (onLine) {
113                 if (aniCounter++ > 6) {
114                         aniTimer->stop();
115                         aniCounter = 0;
116                         return;
117                 }
118
119                 if (aniCounter % 2 == 0)
120                         icon = "network-idle";
121                 else
122                         icon = "network-transmit-receive";
123         } else {
124                 switch(aniCounter++) {
125                 case 0:
126                         icon = "network-transmit";
127                         break;
128                 case 1:
129                         icon = "network-receive";
130                         break;
131                 default:
132                         icon = "network-idle";
133                         aniCounter = 0;
134                 }
135         }
136
137         setIcon("status", icon);
138 }
139
140 void DhcpcdQt::updateOnline(bool showIf)
141 {
142         bool isOn, isCarrier;
143         char *msg;
144         DHCPCD_IF *ifs, *i;
145         QString msgs;
146
147         isOn = isCarrier = false;
148         ifs = dhcpcd_interfaces(con);
149         for (i = ifs; i; i = i->next) {
150                 if (strcmp(i->type, "link") == 0) {
151                         if (i->up)
152                                 isCarrier = true;
153                 } else {
154                         if (i->up)
155                                 isOn = true;
156                 }
157                 msg = dhcpcd_if_message(i, NULL);
158                 if (msg) {
159                         if (showIf)
160                                 qDebug() << msg;
161                         if (msgs.isEmpty())
162                                 msgs = QString::fromAscii(msg);
163                         else
164                                 msgs += '\n' + QString::fromAscii(msg);
165                         free(msg);
166                 } else if (showIf)
167                         qDebug() << i->ifname << i->reason;
168         }
169
170         if (onLine != isOn || carrier != isCarrier) {
171                 onLine = isOn;
172                 carrier = isCarrier;
173                 aniTimer->stop();
174                 aniCounter = 0;
175                 if (isOn) {
176                         animate();
177                         aniTimer->start(300);
178                 } else if (isCarrier) {
179                         animate();
180                         aniTimer->start(500);
181                 } else
182                         setIcon("status", "network-offline");
183         }
184
185         trayIcon->setToolTip(msgs);
186 }
187
188 void DhcpcdQt::statusCallback(const char *status)
189 {
190
191         qDebug("Status changed to %s", status);
192         if (strcmp(status, "down") == 0) {
193                 aniTimer->stop();
194                 aniCounter = 0;
195                 onLine = carrier = false;
196                 setIcon("status", "network-offline");
197                 trayIcon->setToolTip(tr("Not connected to dhcpcd"));
198                 /* Close down everything */
199                 if (notifier) {
200                         delete notifier;
201                         notifier = NULL;
202                 }
203                 if (ssidMenu) {
204                         delete ssidMenu;
205                         ssidMenu = NULL;
206                 }
207                 if (preferences) {
208                         delete preferences;
209                         preferences = NULL;
210                 }
211                 preferencesAction->setEnabled(false);
212         } else {
213                 bool refresh;
214
215                 if (lastStatus == NULL || strcmp(lastStatus, "down") == 0) {
216                         qDebug("Connected to dhcpcd-%s", dhcpcd_version(con));
217                         refresh = true;
218                 } else
219                         refresh = strcmp(lastStatus, "opened") ? false : true;
220                 updateOnline(refresh);
221         }
222
223         free(lastStatus);
224         lastStatus = strdup(status);
225
226         if (strcmp(status, "down") == 0) {
227                 if (retryOpenTimer == NULL) {
228                         retryOpenTimer = new QTimer(this);
229                         connect(retryOpenTimer, SIGNAL(timeout()),
230                             this, SLOT(tryOpen()));
231                         retryOpenTimer->start(DHCPCD_RETRYOPEN);
232                 }
233         }
234 }
235
236 void DhcpcdQt::dhcpcd_status_cb(_unused DHCPCD_CONNECTION *con,
237     const char *status, void *d)
238 {
239         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
240
241         dhcpcdQt->statusCallback(status);
242 }
243
244 void DhcpcdQt::ifCallback(DHCPCD_IF *i)
245 {
246         char *msg;
247         bool new_msg;
248
249         if (strcmp(i->reason, "RENEW") &&
250             strcmp(i->reason, "STOP") &&
251             strcmp(i->reason, "STOPPED"))
252         {
253                 msg = dhcpcd_if_message(i, &new_msg);
254                 if (msg) {
255                         qDebug("%s", msg);
256                         if (new_msg) {
257                                 QSystemTrayIcon::MessageIcon icon =
258                                     i->up ? QSystemTrayIcon::Information :
259                                     QSystemTrayIcon::Warning;
260                                 trayIcon->showMessage(tr("Network Event"),
261                                     msg, icon);
262                         }
263                         free(msg);
264                 }
265         }
266
267         updateOnline(false);
268 }
269
270 void DhcpcdQt::dhcpcd_if_cb(DHCPCD_IF *i, void *d)
271 {
272         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
273
274         dhcpcdQt->ifCallback(i);
275 }
276
277 DhcpcdWi *DhcpcdQt::findWi(DHCPCD_WPA *wpa)
278 {
279
280         for (auto &wi : *wis) {
281                 if (wi->getWpa() == wpa)
282                         return wi;
283         }
284         return NULL;
285 }
286
287 void DhcpcdQt::scanCallback(DHCPCD_WPA *wpa)
288 {
289         DHCPCD_WI_SCAN *scans, *s1, *s2;
290         int fd = dhcpcd_wpa_get_fd(wpa);
291         DhcpcdWi *wi;
292
293         wi = findWi(wpa);
294         if (fd == -1) {
295                 qCritical("No fd for WPA");
296                 if (wi) {
297                         wis->removeOne(wi);
298                         delete wi;
299                 }
300                 return;
301         }
302
303         DHCPCD_IF *i = dhcpcd_wpa_if(wpa);
304         if (i == NULL) {
305                 qCritical("No interface for WPA");
306                 if (wi) {
307                         wis->removeOne(wi);
308                         delete wi;
309                 }
310                 return;
311         }
312
313         qDebug("%s: Received scan results", i->ifname);
314         scans = dhcpcd_wi_scans(i);
315         if (wi == NULL) {
316                 wi = new DhcpcdWi(this, wpa);
317                 wis->append(wi);
318         } else {
319                 QString title = tr("New Access Point");
320                 QString txt;
321                 for (s1 = scans; s1; s1 = s1->next) {
322                         for (s2 = wi->getScans(); s2; s2 = s2->next) {
323                                 if (strcmp(s1->ssid, s2->ssid) == 0)
324                                         break;
325                         }
326                         if (s2 == NULL) {
327                                 if (!txt.isEmpty()) {
328                                         title = tr("New Access Points");
329                                         txt += '\n';
330                                 }
331                                 txt += s1->ssid;
332                         }
333                 }
334                 if (!txt.isEmpty() &&
335                     (ssidMenu == NULL || !ssidMenu->isVisible()))
336                         notify(title, txt);
337         }
338
339         if (wi->setScans(scans) && ssidMenu->isVisible())
340                 ssidMenu->popup(ssidMenuPos);
341 }
342
343 void DhcpcdQt::dhcpcd_wpa_scan_cb(DHCPCD_WPA *wpa, void *d)
344 {
345         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
346
347         dhcpcdQt->scanCallback(wpa);
348 }
349
350 void DhcpcdQt::wpaStatusCallback(DHCPCD_WPA *wpa, const char *status)
351 {
352         DHCPCD_IF *i;
353
354         i = dhcpcd_wpa_if(wpa);
355         qDebug("%s: WPA status %s", i->ifname, status);
356         if (strcmp(status, "down") == 0) {
357                 DhcpcdWi *wi = findWi(wpa);
358                 if (wi) {
359                         wis->removeOne(wi);
360                         delete wi;
361                 }
362         }
363 }
364
365 void DhcpcdQt::dhcpcd_wpa_status_cb(DHCPCD_WPA *wpa, const char *status,
366     void *d)
367 {
368         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
369
370         dhcpcdQt->wpaStatusCallback(wpa, status);
371 }
372
373 void DhcpcdQt::tryOpen() {
374         int fd = dhcpcd_open(con, true);
375         static int last_error;
376
377         if (fd == -1) {
378                 if (errno == EACCES || errno == EPERM) {
379                         if ((fd = dhcpcd_open(con, false)) != -1)
380                                 goto unprived;
381                 }
382                 if (errno != last_error) {
383                         last_error = errno;
384                         const char *errt = strerror(errno);
385                         qCritical("dhcpcd_open: %s", errt);
386                         trayIcon->setToolTip(
387                             tr("Error connecting to dhcpcd: %1").arg(errt));
388                 }
389                 if (retryOpenTimer == NULL) {
390                         retryOpenTimer = new QTimer(this);
391                         connect(retryOpenTimer, SIGNAL(timeout()),
392                             this, SLOT(tryOpen()));
393                         retryOpenTimer->start(DHCPCD_RETRYOPEN);
394                 }
395                 return;
396         }
397
398 unprived:
399         /* Start listening to WPA events */
400         dhcpcd_wpa_start(con);
401
402         if (retryOpenTimer) {
403                 delete retryOpenTimer;
404                 retryOpenTimer = NULL;
405         }
406
407         notifier = new QSocketNotifier(fd, QSocketNotifier::Read);
408         connect(notifier, SIGNAL(activated(int)), this, SLOT(dispatch()));
409
410         preferencesAction->setEnabled(dhcpcd_privileged(con));
411 }
412
413 void DhcpcdQt::dispatch() {
414
415         if (dhcpcd_get_fd(con) == -1) {
416                 qWarning("dhcpcd connection lost");
417                 return;
418         }
419
420         dhcpcd_dispatch(con);
421 }
422
423 void DhcpcdQt::notify(QString &title, QString &msg,
424     QSystemTrayIcon::MessageIcon icon)
425 {
426
427         qDebug("%s", qPrintable(msg));
428         trayIcon->showMessage(title, msg, icon);
429 }
430
431
432 void DhcpcdQt::closeEvent(QCloseEvent *event)
433 {
434
435         if (trayIcon->isVisible()) {
436                 hide();
437                 event->ignore();
438         }
439 }
440
441 QIcon DhcpcdQt::getIcon(QString category, QString name)
442 {
443         QIcon icon;
444
445         if (QIcon::hasThemeIcon(name))
446                 icon = QIcon::fromTheme(name);
447         else
448                 icon = QIcon(ICONDIR "/hicolor/scalable/" + category + "/" + name + ".svg");
449         return icon;
450 }
451
452 void DhcpcdQt::setIcon(QString category, QString name)
453 {
454         QIcon icon = getIcon(category, name);
455
456         trayIcon->setIcon(icon);
457 }
458
459 QIcon DhcpcdQt::icon()
460 {
461
462         return getIcon("status", "network-transmit-receive");
463 }
464
465 void DhcpcdQt::createSsidMenu()
466 {
467
468         if (ssidMenu) {
469                 delete ssidMenu;
470                 ssidMenu = NULL;
471         }
472         if (wis->size() == 0)
473                 return;
474
475         ssidMenu = new QMenu(this);
476         if (wis->size() == 1)
477                 wis->first()->createMenu(ssidMenu);
478         else {
479                 for (auto &wi : *wis)
480                         ssidMenu->addMenu(wi->createIfMenu(ssidMenu));
481         }
482         ssidMenuPos = QCursor::pos();
483         ssidMenu->popup(ssidMenuPos);
484 }
485
486 void DhcpcdQt::iconActivated(QSystemTrayIcon::ActivationReason reason)
487 {
488
489         if (reason == QSystemTrayIcon::Trigger)
490                 createSsidMenu();
491 }
492
493 void DhcpcdQt::dialogClosed(QDialog *dialog)
494 {
495
496         if (dialog == about)
497                 about = NULL;
498         else if (dialog == preferences)
499                 preferences = NULL;
500 }
501
502 void DhcpcdQt::showPreferences()
503 {
504
505         if (preferences == NULL) {
506                 preferences = new DhcpcdPreferences(this);
507                 preferences->show();
508         } else
509                 preferences->activateWindow();
510 }
511
512 void DhcpcdQt::showAbout()
513 {
514
515         if (about == NULL) {
516                 about = new DhcpcdAbout(this);
517                 about->show();
518         } else
519                 about->activateWindow();
520 }
521
522 void DhcpcdQt::createActions()
523 {
524
525         preferencesAction = new QAction(tr("&Preferences"), this);
526         preferencesAction->setIcon(QIcon::fromTheme("preferences-system-network"));
527         preferencesAction->setEnabled(false);
528         connect(preferencesAction, SIGNAL(triggered()),
529             this, SLOT(showPreferences()));
530
531         aboutAction = new QAction(tr("&About"), this);
532         aboutAction->setIcon(QIcon::fromTheme("help-about"));
533         connect(aboutAction, SIGNAL(triggered()), this, SLOT(showAbout()));
534
535         quitAction = new QAction(tr("&Quit"), this);
536         quitAction->setIcon(QIcon::fromTheme("application-exit"));
537         connect(quitAction, SIGNAL(triggered()), qApp, SLOT(quit()));
538
539 }
540
541 void DhcpcdQt::createTrayIcon()
542 {
543
544         trayIconMenu = new QMenu(this);
545         trayIconMenu->addAction(preferencesAction);
546         trayIconMenu->addSeparator();
547         trayIconMenu->addAction(aboutAction);
548         trayIconMenu->addAction(quitAction);
549
550         trayIcon = new QSystemTrayIcon(this);
551         setIcon("status", "network-offline");
552         trayIcon->setContextMenu(trayIconMenu);
553
554         connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
555             this, SLOT(iconActivated(QSystemTrayIcon::ActivationReason)));
556
557         trayIcon->show();
558 }