tmisu

Notification to stdout daemon
git clone https://git.inz.fi/tmisu/
Log | Files | Refs | README | LICENSE

tmisu.c (14621B)


      1 #define _POSIX_C_SOURCE 200809L
      2 #include <stdio.h>
      3 #include <string.h>
      4 #include <signal.h>
      5 #include <unistd.h>
      6 #include <stdlib.h>
      7 #include <getopt.h>
      8 #include <time.h>
      9 #include <sys/poll.h>
     10 
     11 #include <dbus/dbus.h>
     12 
     13 #include "tmisu.h"
     14 #include "output.h"
     15 
     16 struct conf {
     17 	enum output_format fmt;
     18 	const char *delimiter;
     19 };
     20 
     21 struct notification {
     22 	struct notification *next;
     23 	DBusMessage *msg;
     24 	dbus_uint32_t id;
     25 	time_t expiry;
     26 	char *title;
     27 };
     28 
     29 #define MAX_WATCH 32
     30 #define N(x) (sizeof(x) / sizeof(*(x)))
     31 static struct pollfd pfdb[MAX_WATCH + 1] = { 0 };
     32 static struct pollfd *pfds = pfdb + 1;
     33 static DBusWatch *watches[MAX_WATCH] = { 0 };
     34 static size_t nw = 1;
     35 static const char *delimiter = ", ";
     36 static int default_exp_timeout = 60;
     37 int use_json = 0;
     38 int changed = 1;
     39 
     40 static time_t expirys[MAX_WATCH] = { 0 };
     41 static DBusTimeout *timeouts[MAX_WATCH] = { 0 };
     42 static size_t nt = 0;
     43 
     44 static short poll_flags(unsigned int dbf)
     45 {
     46 	return (dbf & DBUS_WATCH_READABLE ? POLLIN : 0) |
     47 		(dbf & DBUS_WATCH_WRITABLE ? POLLOUT : 0) |
     48 		(dbf & DBUS_WATCH_ERROR ? POLLERR : 0) |
     49 		(dbf & DBUS_WATCH_HANGUP ? POLLHUP : 0);
     50 }
     51 
     52 static unsigned int dw_flags(short pf)
     53 {
     54 	return (pf & POLLIN ? DBUS_WATCH_READABLE : 0) |
     55 		(pf & POLLOUT ? DBUS_WATCH_WRITABLE : 0) |
     56 		(pf & POLLERR ? DBUS_WATCH_ERROR : 0) |
     57 		(pf & POLLHUP ? DBUS_WATCH_HANGUP : 0);
     58 }
     59 
     60 static void toggle_watch(DBusWatch *watch, void *data)
     61 {
     62 	size_t i = (size_t)dbus_watch_get_data(watch);
     63 
     64 	(void)data;
     65 
     66 	if (dbus_watch_get_enabled(watch))
     67 		pfds[i].events = poll_flags(dbus_watch_get_flags(watch));
     68 	else
     69 		pfds[i].events = 0;
     70 }
     71 
     72 static void remove_watch(DBusWatch *watch, void *data)
     73 {
     74 	size_t i = (size_t)dbus_watch_get_data(watch);
     75 
     76 	(void)data;
     77 
     78 	memmove(&pfds[i], &pfds[i + 1], (nw - i - 1) * sizeof(*pfds));
     79 	memmove(&watches[i], &watches[i + 1], (nw - i - 1) * sizeof(*watches));
     80 
     81 	for (; i < nw - 1; i++)
     82 		dbus_watch_set_data(watches[i], (void *)i, NULL);
     83 	nw--;
     84 }
     85 
     86 static dbus_bool_t add_watch(DBusWatch *watch, void *data)
     87 {
     88 	(void)data;
     89 
     90 	if (nw == N(watches))
     91 		return FALSE;
     92 
     93 	watches[nw] = watch;
     94 	pfds[nw].fd = dbus_watch_get_unix_fd(watch);
     95 	dbus_watch_set_data(watch, (void *)nw++, NULL);
     96 
     97 	toggle_watch(watch, NULL);
     98 
     99 	return TRUE;
    100 }
    101 
    102 void toggle_timeout(DBusTimeout *timeout,
    103 		    void *data)
    104 {
    105 	size_t i = (size_t)dbus_timeout_get_data(timeout);
    106 
    107 	(void)data;
    108 
    109 	if (dbus_timeout_get_enabled(timeout))
    110 		expirys[i] = time(NULL) +
    111 			(dbus_timeout_get_interval(timeout) + 999) / 1000;
    112 	else
    113 		expirys[i] = 0;
    114 }
    115 
    116 static dbus_bool_t add_timeout(DBusTimeout *timeout,
    117 			       void *data)
    118 {
    119 	(void)data;
    120 
    121 	if (nt == N(timeouts))
    122 		return FALSE;
    123 
    124 	timeouts[nt] = timeout;
    125 	expirys[nt] = 0;
    126 	dbus_timeout_set_data(timeout, (void *)nt++, NULL);
    127 
    128 	toggle_timeout(timeout, NULL);
    129 
    130 	return TRUE;
    131 }
    132 
    133 void remove_timeout(DBusTimeout *timeout,
    134 		    void *data)
    135 {
    136 	size_t i = (size_t)dbus_timeout_get_data(timeout);
    137 
    138 	(void)data;
    139 
    140 	memmove(&timeouts[i], &timeouts[i + 1], (nt - i - 1) * sizeof(*timeouts));
    141 	memmove(&expirys[i], &expirys[i + 1], (nt - i - 1) * sizeof(*expirys));
    142 
    143 	for (; i < nt - 1; i++)
    144 		dbus_timeout_set_data(timeouts[i], (void *)i, NULL);
    145 	nt--;
    146 }
    147 
    148 void trigger_timeouts(void)
    149 {
    150 	size_t i;
    151 	time_t now = time(NULL);
    152 
    153 	for (i = 0; i < nt; i++) {
    154 		DBusTimeout *to;
    155 		if (expirys[i] > now)
    156 			continue;
    157 		to = timeouts[i];
    158 		dbus_timeout_handle(to);
    159 		if (i < nt && timeouts[i] != to)
    160 			i--;
    161 	}
    162 }
    163 
    164 time_t timeout_next_expiry(void)
    165 {
    166 	time_t rv = 0;
    167 	size_t i;
    168 
    169 	for (i = 0; i < nt; i++)
    170 		if (expirys[i] && (!rv || expirys[i] < rv))
    171 			rv = expirys[i];
    172 
    173 	return rv;
    174 }
    175 
    176 static struct notification *notifications = NULL;
    177 static DBusConnection *connection = NULL;
    178 
    179 const struct notification *notif_update(DBusMessage *msg, dbus_uint32_t id, const char *title, time_t expiry)
    180 {
    181 	struct notification *i;
    182 
    183 	for (i = notifications; i && i->id != id; i = i->next);
    184 
    185 	if (!i)
    186 		return NULL;
    187 
    188 	if (!use_json)
    189 		changed |= !!strcmp(title, i->title);
    190 	free(i->title);
    191 	if (use_json)
    192 		dbus_message_unref(i->msg);
    193 	i->title = strdup(title);
    194 	i->expiry = expiry;
    195 	if (use_json)
    196 		i->msg = dbus_message_ref(msg);
    197 
    198 	return i;
    199 }
    200 
    201 const struct notification *notif_add(DBusMessage *msg, const char *title, time_t expiry)
    202 {
    203 	static dbus_uint32_t notification_id = 0;
    204 	struct notification *nn = malloc(sizeof(*nn));
    205 	struct notification *ne;
    206 
    207 	nn->id = ++notification_id;
    208 	if (use_json)
    209 		nn->msg = dbus_message_ref(msg);
    210 	nn->title = strdup(title);
    211 	nn->expiry = expiry;
    212 	nn->next = NULL;
    213 
    214 	if (notifications) {
    215 		for (ne = notifications; ne->next; ne = ne->next);
    216 		ne->next = nn;
    217 	} else {
    218 		notifications = nn;
    219 	}
    220 
    221 	changed = 1;
    222 
    223 	return nn;
    224 }
    225 
    226 enum reason {
    227 	EXPIRED = 1,
    228 	DISMISSED = 2,
    229 	REQUEST = 3,
    230 	UNKNOWN = 4
    231 };
    232 
    233 int notif_close(dbus_uint32_t id, dbus_uint32_t reason)
    234 {
    235 	struct notification *n;
    236 
    237 	if (!notifications)
    238 		return 0;
    239 	if (notifications->id == id) {
    240 		n = notifications;
    241 		notifications = n->next;
    242 		if (use_json)
    243 			dbus_message_unref(n->msg);
    244 		free(n->title);
    245 		free(n);
    246 	} else {
    247 		struct notification *i;
    248 		for (n = notifications;
    249 		     n->next && n->next->id != id; n = n->next);
    250 		if (!n->next)
    251 			return 0;
    252 		i = n->next;
    253 		n->next = n->next->next;
    254 		if (use_json)
    255 			dbus_message_unref(i->msg);
    256 		free(i->title);
    257 		free(i);
    258 	}
    259 
    260 	DBusMessage *sig = dbus_message_new_signal("/",
    261 						   "org.freedesktop.Notifications",
    262 						   "NotificationClosed");
    263 	dbus_message_append_args(sig,
    264 				 DBUS_TYPE_UINT32, &id,
    265 				 DBUS_TYPE_UINT32, &reason,
    266 				 DBUS_TYPE_INVALID);
    267 
    268 	dbus_connection_send(connection, sig, NULL);
    269 	dbus_message_unref(sig);
    270 
    271 	changed = 1;
    272 
    273 	return 1;
    274 }
    275 
    276 time_t notif_next_expiry(void)
    277 {
    278 	struct notification *i;
    279 	time_t rv = 0;
    280 
    281 	for (i = notifications; i; i = i->next) {
    282 		if (i->expiry &&
    283 		    (!rv || i->expiry < rv))
    284 			rv = i->expiry;
    285 	}
    286 
    287 	return rv;
    288 }
    289 
    290 void notif_check_expiry(void)
    291 {
    292 	time_t now = time(NULL);
    293 	struct notification *i;
    294 	struct notification *n;
    295 
    296 	for (i = notifications; i; i = n) {
    297 		n = i->next;
    298 
    299 		if (i->expiry && i->expiry <= now)
    300 			notif_close(i->id, EXPIRED);
    301 	}
    302 }
    303 
    304 static void print_sanitized(const char *string, const char *escape) {
    305 	while (*string) {
    306 		size_t len = strcspn(string, escape);
    307 		printf("%.*s", (int)len, string);
    308 		string += len;
    309 		while (*string && strchr(escape, *string)) {
    310 			if (*string == '\n')
    311 				printf("\\n");
    312 			else
    313 				printf("\\%c", *string);
    314 			string++;
    315 		}
    316 	}
    317 }
    318 
    319 void notif_dump(void)
    320 {
    321 	const char *sep = "";
    322 	struct notification *i;
    323 
    324 	if (use_json)
    325 		printf("[");
    326 	for (i = notifications; i; i = i->next) {
    327 		printf("%s", sep);
    328 		if (use_json)
    329 			output_notification(i->msg, i->id, FORMAT_JSON, "");
    330 		else
    331 			print_sanitized(i->title, "\\\n");
    332 		sep = delimiter;
    333 	}
    334 	if (use_json)
    335 		printf("]");
    336 	puts("");
    337 	fflush(stdout);
    338 }
    339 
    340 DBusHandlerResult handle_message(DBusConnection *connection, DBusMessage *message, void *user_data)
    341 {
    342 	DBusMessage *reply;
    343 
    344 	(void)user_data;
    345 
    346 	if (dbus_message_is_method_call(message, "org.freedesktop.DBus.Introspectable", "Introspect")) {
    347 		static const char *notificationpath = "/org/freedesktop/Notifications";
    348 		const char *path = dbus_message_get_path(message);
    349 		size_t pl = strlen(path);
    350 
    351 		if (strncmp(notificationpath, path, pl))
    352 			return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
    353 
    354 		if (pl == 1)
    355 			pl = 0;
    356 
    357 		if (notificationpath[pl] == '/') {
    358 			size_t npl = strcspn(notificationpath + pl + 1, "/");
    359 			char buffer[sizeof(INTROSPECTION_NODE_XML) + npl - 4];
    360 			sprintf(buffer, INTROSPECTION_NODE_XML, (int)npl, notificationpath + pl + 1);
    361 			reply = dbus_message_new_method_return(message);
    362 			dbus_message_append_args(reply,
    363 						 DBUS_TYPE_STRING, &(const char *){ buffer },
    364 						 DBUS_TYPE_INVALID);
    365 		} else if (notificationpath[pl]) {
    366 			return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
    367 		} else {
    368 			reply = dbus_message_new_method_return(message);
    369 			dbus_message_append_args(reply,
    370 						 DBUS_TYPE_STRING, &(const char *){ INTROSPECTION_XML },
    371 						 DBUS_TYPE_INVALID);
    372 		}
    373 	} else if (dbus_message_is_method_call(message, "org.freedesktop.Notifications", "GetServerInformation")) {
    374 		reply = dbus_message_new_method_return(message);
    375 		dbus_message_append_args(reply,
    376 					 DBUS_TYPE_STRING, &(const char *){ "tmisu" },
    377 					 DBUS_TYPE_STRING, &(const char *){ "inz" },
    378 					 DBUS_TYPE_STRING, &(const char *){ "1.0" },
    379 					 DBUS_TYPE_STRING, &(const char *){ "1.2" },
    380 					 DBUS_TYPE_INVALID);
    381 	} else if (dbus_message_is_method_call(message, "org.freedesktop.Notifications", "GetCapabilities")) {
    382 		DBusMessageIter i;
    383 		DBusMessageIter j;
    384 
    385 		reply = dbus_message_new_method_return(message);
    386 		dbus_message_iter_init_append(reply, &i);
    387 		dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING_AS_STRING, &j);
    388 		dbus_message_iter_append_basic(&j, DBUS_TYPE_STRING, &(const char *){ "body" });
    389 		dbus_message_iter_append_basic(&j, DBUS_TYPE_STRING, &(const char *){ "actions" });
    390 		dbus_message_iter_close_container(&i, &j);
    391 	} else if (dbus_message_is_method_call(message, "org.freedesktop.Notifications", "Notify")) {
    392 		const struct notification *n = NULL;
    393 		struct notification_data d;
    394 		DBusMessageIter iter;
    395 		time_t expiry;
    396 
    397 		if (!dbus_message_has_signature(message, "susssasa{sv}i"))
    398 			return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
    399 
    400 		dbus_message_iter_init(message, &iter);
    401 		dbus_message_iter_get_basic(&iter, &d.app_name);
    402 		dbus_message_iter_next(&iter);
    403 		dbus_message_iter_get_basic(&iter, &d.replaces);
    404 		dbus_message_iter_next(&iter);
    405 		dbus_message_iter_get_basic(&iter, &d.icon);
    406 		dbus_message_iter_next(&iter);
    407 		dbus_message_iter_get_basic(&iter, &d.summary);
    408 		dbus_message_iter_next(&iter);
    409 		dbus_message_iter_get_basic(&iter, &d.body);
    410 		dbus_message_iter_next(&iter);
    411 		dbus_message_iter_recurse(&iter, &d.actions);
    412 		dbus_message_iter_next(&iter);
    413 		dbus_message_iter_recurse(&iter, &d.hints);
    414 		dbus_message_iter_next(&iter);
    415 		dbus_message_iter_get_basic(&iter, &d.expiry_ms);
    416 
    417 		if (d.expiry_ms < 0)
    418 			d.expiry_ms = default_exp_timeout * 1000;
    419 		if (d.expiry_ms)
    420 			expiry = time(NULL) + (d.expiry_ms + 999) / 1000;
    421 		else
    422 			expiry = 0;
    423 
    424 		if (d.replaces)
    425 			n = notif_update(message, d.replaces, d.summary, expiry);
    426 		if (!n)
    427 			n = notif_add(message, d.summary, expiry);
    428 
    429 		reply = dbus_message_new_method_return(message);
    430 		// output_notification(message, n->id, cnf->fmt, cnf->delimiter);
    431 		dbus_message_append_args(reply,
    432 					 DBUS_TYPE_UINT32, &n->id,
    433 					 DBUS_TYPE_INVALID);
    434 	} else if (dbus_message_is_method_call(message, "org.freedesktop.Notifications", "CloseNotification")) {
    435 		if (!dbus_message_has_signature(message, "u"))
    436 			return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
    437 		dbus_uint32_t id;
    438 		dbus_message_get_args(message, NULL,
    439 				      DBUS_TYPE_UINT32, &id,
    440 				      DBUS_TYPE_INVALID);
    441 		if (notif_close(id, REQUEST))
    442 			reply = dbus_message_new_method_return(message);
    443 		else
    444 			reply = dbus_message_new_error(message, DBUS_ERROR_INVALID_ARGS, "Notification not found");
    445 	} else {
    446 		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
    447 	}
    448 
    449 	dbus_connection_send(connection, reply, NULL);
    450 	dbus_message_unref(reply);
    451 
    452 	return DBUS_HANDLER_RESULT_HANDLED;
    453 }
    454 
    455 int sfd[2];
    456 
    457 void sig_handler(int signal)
    458 {
    459 	if (write(sfd[1], &(char){ signal }, 1) != 1)
    460 		exit(1);
    461 }
    462 
    463 int main(int argc, char **argv) {
    464 
    465 	char argument;
    466 	while ((argument = getopt(argc, argv, "hjd:")) >= 0) {
    467 		switch (argument) {
    468 		case 'd':
    469 			delimiter = optarg;
    470 			break;
    471 		case 'h':
    472 			printf("%s\n",
    473 			       "tiramisu -[h|d|j]\n"
    474 			       "-h\tHelp dialog\n"
    475 			       "-d\tDelimeter for default output style.\n"
    476 			       "-j\tUse JSON output style\n");
    477 			return EXIT_SUCCESS;
    478 			break;
    479 		case 'j':
    480 			use_json = 1;
    481 			break;
    482 		default:
    483 			break;
    484 		}
    485 	}
    486 
    487 	if (pipe(sfd))
    488 		return 1;
    489 
    490 	connection = dbus_bus_get_private(DBUS_BUS_SESSION, NULL);
    491 
    492 	dbus_connection_set_watch_functions(connection,
    493 					    add_watch,
    494 					    remove_watch,
    495 					    toggle_watch,
    496 					    NULL, NULL);
    497 	dbus_connection_set_timeout_functions(connection,
    498 					      add_timeout,
    499 					      remove_timeout,
    500 					      toggle_timeout,
    501 					      NULL, NULL);
    502 
    503 
    504 	if (!connection) {
    505 		fprintf(stderr, "Could not connect to D-Bus\n");
    506 		return 1;
    507 	}
    508 
    509 	int result = dbus_bus_request_name(connection, "org.freedesktop.Notifications", DBUS_NAME_FLAG_REPLACE_EXISTING | DBUS_NAME_FLAG_DO_NOT_QUEUE, NULL);
    510 
    511 	if (result != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
    512 		dbus_connection_close(connection);
    513 		dbus_connection_unref(connection);
    514 
    515 		fprintf(stderr, "Could not acquire service name, is another notification daemon running?\n");
    516 		return 1;
    517 	}
    518 
    519 	dbus_bus_add_match(connection, "interface=org.freedesktop.Notifications,path=/org/freedesktop/Notifications,type=method_call", NULL);
    520 	dbus_bus_add_match(connection, "interface=org.freedesktop.DBus.Introspectable,method=Introspect,type=method_call", NULL);
    521 	dbus_connection_add_filter(connection, handle_message, NULL, NULL);
    522 
    523 	signal(SIGINT, sig_handler);
    524 	signal(SIGTERM, sig_handler);
    525 	signal(SIGUSR1, sig_handler);
    526 	signal(SIGUSR2, sig_handler);
    527 
    528 	pfdb[0].fd = sfd[0];
    529 	pfdb[0].events = POLLIN;
    530 
    531 	for (;;) {
    532 		size_t i;
    533 		time_t nexp;
    534 		time_t texp;
    535 		int interval;
    536 
    537 		notif_check_expiry();
    538 		trigger_timeouts();
    539 
    540 		nexp = notif_next_expiry();
    541 		texp = timeout_next_expiry();
    542 
    543 		if (changed)
    544 			notif_dump();
    545 		changed = 0;
    546 
    547 		if (nexp && (!texp || nexp <= texp))
    548 			interval = (nexp - time(NULL)) * 1000;
    549 		else if (texp && (!nexp || texp < nexp))
    550 			interval = (texp - time(NULL)) * 1000;
    551 		else
    552 			interval = -1;
    553 
    554 		int r = poll(pfdb, nw + 1, interval);
    555 
    556 		if (pfdb[0].revents) {
    557 			char s;
    558 			if (read(pfdb[0].fd, &s, 1) != 1)
    559 				break;
    560 
    561 			if (s == SIGINT || s == SIGTERM)
    562 				break;
    563 			if (s == SIGUSR1) {
    564 				if (notifications)
    565 					notif_close(notifications->id, DISMISSED);
    566 			} else if (s == SIGUSR2) {
    567 				while (notifications)
    568 					notif_close(notifications->id, DISMISSED);
    569 			}
    570 			signal(s, sig_handler);
    571 		}
    572 		for (i = 0; i < nw && r; i++) {
    573 			if (!pfds[i].revents)
    574 				continue;
    575 			dbus_watch_handle(watches[i],
    576 					  dw_flags(pfds[i].revents));
    577 			toggle_watch(watches[i], NULL);
    578 			r--;
    579 		}
    580 
    581 		while (dbus_connection_get_dispatch_status(connection) == DBUS_DISPATCH_DATA_REMAINS)
    582 			dbus_connection_dispatch(connection);
    583 	}
    584 
    585 	dbus_bus_release_name(connection, "org.freedesktop.Notifications", NULL);
    586 	dbus_connection_remove_filter(connection, handle_message, NULL);
    587 	dbus_bus_remove_match(connection, "interface=org.freedesktop.Notifications,path=/org/freedesktop/Notifications,type=method_call", NULL);
    588 	dbus_bus_remove_match(connection, "interface=org.freedesktop.DBus.Introspectable,method=Introspect,type=method_call", NULL);
    589 	dbus_connection_close(connection);
    590 	dbus_connection_unref(connection);
    591 
    592 	return 0;
    593 }