Use theme icons. Add comment about looking into animated icon.
[dhcpcd-ui] / main.c
1 /*
2  * dhcpcd-gtk
3  * Copyright 2009 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 /* TODO: Animate the icon from carrier -> address
28  * maybe use network-idle -> network-transmit ->
29  * network-receive -> network-transmit-receive */
30
31 #include <arpa/inet.h>
32
33 #include <stdlib.h>
34 #include <string.h>
35
36 #include <dbus/dbus-glib.h>
37 #include <gtk/gtk.h>
38 #include <libnotify/notify.h>
39
40 #include "config.h"
41 #include "menu.h"
42
43 /* Work out if we have a private address or not
44  * 10/8
45  * 172.16/12
46  * 192.168/16
47  */
48 #ifndef IN_PRIVATE
49 # define IN_PRIVATE(addr) (((addr & IN_CLASSA_NET) == 0x0a000000) || \
50                            ((addr & 0xfff00000)    == 0xac100000) || \
51                            ((addr & IN_CLASSB_NET) == 0xc0a80000))
52 #endif
53 #ifndef IN_LINKLOCAL
54 # define IN_LINKLOCAL(addr) ((addr & IN_CLASSB_NET) == 0xa9fe0000)
55 #endif
56
57 struct if_msg {
58         char *name;
59         char *reason;
60         struct in_addr ip;
61         unsigned char cidr;
62         gboolean wireless;
63         char *ssid;
64 };
65
66 static DBusGProxy *bus_proxy;
67 static GtkStatusIcon *status_icon;
68 static GList *interfaces;
69 static gboolean online;
70 static NotifyNotification *nn;
71
72 static char **interface_order;
73
74 const char *const up_reasons[] = {
75         "BOUND",
76         "RENEW",
77         "REBIND",
78         "REBOOT",
79         "IPV4LL",
80         "INFORM",
81         "TIMEOUT",
82         NULL
83 };
84
85 const char *const down_reasons[] = {
86         "EXPIRE",
87         "FAIL",
88         "NAK",
89         "NOCARRIER",
90         "STOP",
91         NULL
92 };
93
94 /* Should be in a header */
95 void notify_close(void);
96
97 static gboolean
98 ignore_if_msg(const struct if_msg *ifm)
99 {
100         if (g_strcmp0(ifm->reason, "STOP") == 0 ||
101             g_strcmp0(ifm->reason, "RELEASE") == 0)
102                 return TRUE;
103         return FALSE;
104 }
105
106 static void
107 free_if_msg(struct if_msg *ifm)
108 {
109         g_free(ifm->name);
110         g_free(ifm->reason);
111         g_free(ifm->ssid);
112         g_free(ifm);
113 }
114
115 static void
116 error_exit(const char *msg, GError *error)
117 {
118         GtkWidget *dialog;
119
120         if (error) {
121                 g_critical("%s: %s", msg, error->message);
122                 dialog = gtk_message_dialog_new(NULL,
123                                                 0,
124                                                 GTK_MESSAGE_ERROR,
125                                                 GTK_BUTTONS_CLOSE,
126                                                 "%s: %s",
127                                                 msg,
128                                                 error->message);
129         } else {
130                 g_critical("%s", msg);
131                 dialog = gtk_message_dialog_new(NULL,
132                                                 0,
133                                                 GTK_MESSAGE_ERROR,
134                                                 GTK_BUTTONS_CLOSE,
135                                                 "%s",
136                                                 msg);
137         }
138         gtk_dialog_run(GTK_DIALOG(dialog));
139         gtk_widget_destroy(dialog);
140         if (gtk_main_level())
141                 gtk_main_quit();
142         else
143                 exit(EXIT_FAILURE);
144 }
145
146 static struct if_msg *
147 make_if_msg(GHashTable *config)
148 {
149         GValue *val;
150         struct if_msg *ifm;
151
152         val = g_hash_table_lookup(config, "Interface");
153         if (val == NULL)
154                 return NULL;
155         ifm = g_malloc0(sizeof(*ifm));
156         ifm->name = g_strdup(g_value_get_string(val));
157         val = g_hash_table_lookup(config, "Reason");
158         if (val)
159                 ifm->reason = g_strdup(g_value_get_string(val));
160         val = g_hash_table_lookup(config, "Wireless");
161         if (val)
162                 ifm->wireless = g_value_get_boolean(val);
163         if (ifm->wireless) {
164                 val = g_hash_table_lookup(config, "SSID");
165                 if (val)
166                         ifm->ssid = g_strdup(g_value_get_string(val));
167         }
168         val = g_hash_table_lookup(config, "IPAddress");
169         if (val)
170                 ifm->ip.s_addr = g_value_get_uint(val);
171         val = g_hash_table_lookup(config, "SubnetCIDR");
172         if (val)
173                 ifm->cidr = g_value_get_uchar(val);
174         val = g_hash_table_lookup(config, "InterfaceOrder");
175         if (val) {
176                 g_strfreev(interface_order);
177                 interface_order = g_strsplit(g_value_get_string(val), " ", 0);
178         }
179         return ifm;
180 }
181
182 static gboolean
183 if_up(const struct if_msg *ifm)
184 {
185         const char *const *r;
186
187         for (r = up_reasons; *r; r++)
188                 if (g_strcmp0(*r, ifm->reason) == 0)
189                         return TRUE;
190         return FALSE;
191 }
192
193 static char *
194 print_if_msg(const struct if_msg *ifm)
195 {
196         char *msg, *p;
197         const char *reason = NULL;
198         size_t len;
199         gboolean showip, showssid;
200     
201         showip = TRUE;
202         showssid = FALSE;
203         if (if_up(ifm))
204                 reason = "Acquired address";
205         else {
206                 if (g_strcmp0(ifm->reason, "EXPIRE") == 0)
207                         reason = "Failed to renew";
208                 else if (g_strcmp0(ifm->reason, "CARRIER") == 0) {
209                         if (ifm->wireless) {
210                                 reason = "Asssociated with";
211                                 if (ifm->ssid != NULL)
212                                         showssid = TRUE;
213                         } else
214                                 reason = "Cable plugged in";
215                         showip = FALSE;
216                 } else if (g_strcmp0(ifm->reason, "NOCARRIER") == 0) {
217                         if (ifm->wireless) {
218                                 if (ifm->ssid != NULL || ifm->ip.s_addr != 0) {
219                                         reason = "Lost association with";
220                                         showssid = TRUE;
221                                 } else
222                                     reason = "Not associated";
223                         } else
224                                 reason = "Cable unplugged";
225                         showip = FALSE;
226                 }
227         }
228         if (reason == NULL)
229                 reason = ifm->reason;
230         
231         len = strlen(ifm->name) + 3;
232         len += strlen(reason) + 1;
233         if (ifm->ip.s_addr != 0) {
234                 len += 16; /* 000. * 4 */
235                 if (ifm->cidr != 0)
236                         len += 3; /* /32 */
237         }
238         if (showssid)
239                 len += strlen(ifm->ssid) + 1;
240         msg = p = g_malloc(len);
241         p += g_snprintf(msg, len, "%s: %s", ifm->name, reason);
242         if (showssid)
243                 p += g_snprintf(p, len - (p - msg), " %s", ifm->ssid);
244         if (ifm->ip.s_addr != 0 && showip) {
245                 p += g_snprintf(p, len - (p - msg), " %s", inet_ntoa(ifm->ip));
246                 if (ifm->cidr != 0)
247                         g_snprintf(p, len - (p - msg), "/%d", ifm->cidr);
248         }
249         return msg;
250 }
251
252 static gint
253 if_msg_comparer(gconstpointer a, gconstpointer b)
254 {
255         const struct if_msg *ifa, *ifb;
256         const char *const *order;
257
258         ifa = (const struct if_msg *)a;
259         ifb = (const struct if_msg *)b;
260         for (order = (const char *const *)interface_order; *order; order++) {
261                 if (g_strcmp0(*order, ifa->name) == 0)
262                         return -1;
263                 if (g_strcmp0(*order, ifb->name) == 0)
264                         return 1;
265         }
266         return 0;
267 }
268
269 static void
270 update_online(char **buffer)
271 {
272         gboolean ison;
273         char *msg, *msgs, *tmp;
274         const char *icon;
275         const GList *gl;
276         const struct if_msg *ifm;
277
278         ison = FALSE;
279         msgs = NULL;
280         for (gl = interfaces; gl; gl = gl->next) {
281                 ifm = (const struct if_msg *)gl->data;
282                 if (if_up(ifm))
283                         ison = TRUE;
284                 msg = print_if_msg(ifm);
285                 if (msgs) {
286                         tmp = g_strconcat(msgs, "\n", msg, NULL);
287                         g_free(msgs);
288                         g_free(msg);
289                         msgs = tmp;
290                 } else
291                         msgs = msg;
292         }
293
294         if (online != ison) {
295                 online = ison;
296                 icon = online ? "connect_established" : "connect_no";
297                 gtk_status_icon_set_from_icon_name(status_icon, icon);
298         }
299         gtk_status_icon_set_tooltip(status_icon, msgs);
300         if (buffer)
301                 *buffer = msgs;
302         else
303                 g_free(msgs);
304 }
305
306 void
307 notify_close(void)
308 {
309         if (nn != NULL)
310                 notify_notification_close(nn, NULL);
311 }
312
313 static void
314 notify_closed(void)
315 {
316         nn = NULL;
317 }
318
319 static void
320 notify(const char *title, const char *msg, const char *icon)
321 {
322         char **msgs, **m;
323
324         msgs = g_strsplit(msg, "\n", 0);
325         for (m = msgs; *m; m++)
326                 g_message("%s", *m);
327         g_strfreev(msgs);
328         if (nn != NULL)
329                 notify_notification_close(nn, NULL);
330         if (gtk_status_icon_get_visible(status_icon))
331                 nn = notify_notification_new_with_status_icon(title,
332                                                               msg,
333                                                               icon,
334                                                               status_icon);
335         else
336                 nn = notify_notification_new(title, msg, icon, NULL);
337         notify_notification_set_timeout(nn, 5000);
338         g_signal_connect(nn, "closed", G_CALLBACK(notify_closed), NULL);
339         notify_notification_show(nn, NULL);
340 }
341
342 static void
343 dhcpcd_event(_unused DBusGProxy *proxy, GHashTable *config, _unused void *data)
344 {
345         struct if_msg *ifm, *ifp;
346         gboolean rem;
347         GList *gl;
348         char *msg, *title;
349         const char *act, *net;
350         const char *const *r;
351         in_addr_t ipn;
352
353         ifm = make_if_msg(config);
354         if (ifm == NULL)
355                 return;
356
357         rem = ignore_if_msg(ifm);
358         ifp = NULL;
359         for (gl = interfaces; gl; gl = gl->next) {
360                 ifp = (struct if_msg *)gl->data;
361                 if (g_strcmp0(ifp->name, ifm->name) == 0) {
362                         free_if_msg(ifp);
363                         if (rem)
364                                 interfaces = g_list_delete_link(interfaces, gl);
365                         else
366                                 gl->data = ifm;
367                         break;
368                 }
369         }
370         if (ifp == NULL && !rem)
371                 interfaces = g_list_prepend(interfaces, ifm);
372         interfaces = g_list_sort(interfaces, if_msg_comparer);
373         update_online(NULL);
374
375         /* We should ignore renew and stop so we don't annoy the user */
376         if (g_strcmp0(ifm->reason, "RENEW") == 0 ||
377             g_strcmp0(ifm->reason, "STOP") == 0)
378                 return;
379
380         msg = print_if_msg(ifm);
381         title = NULL;
382         if (if_up(ifm))
383                 act = "Connected to ";
384         else
385                 act = NULL;
386         for (r = down_reasons; *r; r++) {
387                 if (g_strcmp0(*r, ifm->reason) == 0) {
388                         act = "Disconnected from ";
389                         break;
390                 }
391         }
392         if (act && ifm->ip.s_addr) {
393                 ipn = htonl(ifm->ip.s_addr);
394                 if (IN_LINKLOCAL(ipn))
395                         net = "private network";
396                 else if (IN_PRIVATE(ipn))
397                         net = "LAN";
398                 else
399                         net = "internet";
400                 title = g_strconcat(act, net, NULL);
401         }
402
403         if (title) {
404                 notify(title, msg, GTK_STOCK_NETWORK);
405                 g_free(title);
406         } else
407                 notify("Interface event", msg, GTK_STOCK_NETWORK);
408         g_free(msg);
409 }
410
411 static void
412 foreach_make_ifm(_unused gpointer key, gpointer value, _unused gpointer data)
413 {
414         struct if_msg *ifm;
415
416         ifm = make_if_msg((GHashTable *)value);
417         if (ignore_if_msg(ifm))
418                 g_free(ifm);
419         else if (ifm)
420                 interfaces = g_list_prepend(interfaces, ifm);
421 }
422
423 static void
424 dhcpcd_get_interfaces()
425 {
426         GHashTable *ifs;
427         GError *error = NULL;
428         GType otype;
429         char *msg;
430
431         otype = dbus_g_type_get_map("GHashTable", G_TYPE_STRING, G_TYPE_VALUE);
432         otype = dbus_g_type_get_map("GHashTable", G_TYPE_STRING, otype);
433         if (!dbus_g_proxy_call(bus_proxy, "GetInterfaces", &error,
434                                G_TYPE_INVALID,
435                                otype, &ifs, G_TYPE_INVALID))
436                 error_exit("GetInterfaces", error);
437         g_hash_table_foreach(ifs, foreach_make_ifm, NULL);
438         g_hash_table_unref(ifs);
439
440         /* Each interface config only remembers the last order when
441          * that interface was configured, so get the real order now. */
442         g_strfreev(interface_order);
443         interface_order = NULL;
444         if (!dbus_g_proxy_call(bus_proxy, "ListInterfaces", &error,
445                                G_TYPE_INVALID,
446                                G_TYPE_STRV, &interface_order, G_TYPE_INVALID))
447                 error_exit("ListInterfaces", error);
448         interfaces = g_list_sort(interfaces, if_msg_comparer);
449         msg = NULL;
450         update_online(&msg);
451         // GTK+ 2.16 msg = gtk_status_icon_get_tooltip_text(status_icon);
452         if (msg != NULL) {
453                 notify("Interface status", msg, GTK_STOCK_NETWORK);
454                 g_free(msg);
455         }
456 }
457
458 static void
459 check_status(const char *status)
460 {
461         static char *last = NULL;
462         GList *gl;
463         char *version;
464         const char *msg;
465         gboolean refresh;
466         GError *error = NULL;
467
468         g_message("status changed to %s", status);
469         if (g_strcmp0(status, "down") == 0) {
470                 for (gl = interfaces; gl; gl = gl->next)
471                         free_if_msg((struct if_msg *)gl->data);
472                 g_list_free(interfaces);
473                 interfaces = NULL;
474                 update_online(NULL);
475                 msg = last? "Connection to dhcpcd lost" : "dhcpcd not running";
476                 gtk_status_icon_set_tooltip(status_icon, msg);
477                 notify("No network", msg, GTK_STOCK_NETWORK);
478         }
479
480         refresh = FALSE;
481         if (last == NULL) {
482                 if (g_strcmp0(status, "down") != 0)
483                         refresh = TRUE;
484         } else {
485                 if (g_strcmp0(status, last) == 0)
486                         return;
487                 if (g_strcmp0(last, "down") == 0)
488                         refresh = TRUE;
489                 g_free(last);
490         }
491         last = g_strdup(status);
492
493         if (!refresh)
494                 return;
495         if (!dbus_g_proxy_call(bus_proxy, "GetDhcpcdVersion", &error,
496                                G_TYPE_INVALID,
497                                G_TYPE_STRING, &version, G_TYPE_INVALID))
498                 error_exit("GetDhcpcdVersion", error);
499         g_message("Connected to dhcpcd-%s", version);
500         g_free(version);
501         dhcpcd_get_interfaces();
502 }
503
504 static void
505 dhcpcd_status(_unused DBusGProxy *proxy, const char *status, _unused void *data)
506 {
507         check_status(status);
508 }
509
510 int
511 main(int argc, char *argv[])
512 {
513         DBusGConnection *bus;
514         GError *error = NULL;
515         char *version = NULL;
516         GType otype;
517         
518         gtk_init(&argc, &argv);
519         g_set_application_name("dhcpcd Monitor");
520         status_icon = gtk_status_icon_new_from_icon_name("connect_no");
521         if (status_icon == NULL)
522                 status_icon = gtk_status_icon_new_from_stock(GTK_STOCK_DISCONNECT);
523         
524         gtk_status_icon_set_tooltip(status_icon, "Connecting to dhcpcd ...");
525         gtk_status_icon_set_visible(status_icon, TRUE);
526
527         notify_init(PACKAGE);
528
529         bus = dbus_g_bus_get(DBUS_BUS_SYSTEM, &error);
530         if (bus == NULL || error != NULL)
531                 error_exit("could not connect to system bus", error);
532         bus_proxy = dbus_g_proxy_new_for_name(bus,
533                                               DHCPCD_SERVICE,
534                                               DHCPCD_PATH,
535                                               DHCPCD_SERVICE);
536         if (!dbus_g_proxy_call(bus_proxy, "GetVersion", &error,
537                                G_TYPE_INVALID,
538                                G_TYPE_STRING, &version, G_TYPE_INVALID))
539                 error_exit("GetVersion", error);
540         g_message("Connected to dhcpcd-dbus-%s", version);
541         g_free(version);
542
543         gtk_status_icon_set_tooltip(status_icon, "Triggering dhcpcd ...");
544         online = FALSE;
545         menu_init(status_icon);
546
547         if (!dbus_g_proxy_call(bus_proxy, "GetStatus", &error,
548                                G_TYPE_INVALID,
549                                G_TYPE_STRING, &version, G_TYPE_INVALID))
550                 error_exit("GetStatus", error);
551         check_status(version);
552         g_free(version);
553
554         otype = dbus_g_type_get_map("GHashTable", G_TYPE_STRING, G_TYPE_VALUE);
555         dbus_g_proxy_add_signal(bus_proxy, "Event",
556                                 otype, G_TYPE_INVALID);
557         dbus_g_proxy_connect_signal(bus_proxy, "Event",
558                                     G_CALLBACK(dhcpcd_event),
559                                     NULL, NULL);
560         dbus_g_proxy_add_signal(bus_proxy, "StatusChanged",
561                                 G_TYPE_STRING, G_TYPE_INVALID);
562         dbus_g_proxy_connect_signal(bus_proxy, "StatusChanged",
563                                     G_CALLBACK(dhcpcd_status),
564                                     NULL, NULL);
565
566         gtk_main();
567         return 0;
568 }