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 }