tmisu.c (17407B)
1 #define _POSIX_C_SOURCE 200809L 2 #include <stdio.h> 3 #include <string.h> 4 #include <signal.h> 5 #include <stdarg.h> 6 #include <unistd.h> 7 #include <stdlib.h> 8 #include <time.h> 9 #include <sys/poll.h> 10 #include <sys/wait.h> 11 #include <fcntl.h> 12 #include <errno.h> 13 14 #include <dbus/dbus.h> 15 16 #include "tmisu.h" 17 #include "output.h" 18 #include "arg.h" 19 20 #define N_IF "org.freedesktop.Notifications" 21 #define N_SRV N_IF 22 #define N_PATH "/org/freedesktop/Notifications" 23 24 char *argv0; 25 26 struct conf { 27 enum output_format fmt; 28 const char *delimiter; 29 }; 30 31 struct notification { 32 struct notification *next; 33 struct notification_data d; 34 DBusMessage *msg; 35 dbus_uint32_t id; 36 time_t expiry; 37 pid_t child; 38 }; 39 40 #define MAX_WATCH 32 41 #define N(x) (sizeof(x) / sizeof(*(x))) 42 static struct pollfd pfdb[MAX_WATCH + 1] = { 0 }; 43 static struct pollfd *pfds = pfdb + 1; 44 static DBusWatch *watches[MAX_WATCH] = { 0 }; 45 static size_t nw = 1; 46 static const char *delimiter = ", "; 47 static int default_exp_timeout = 60; 48 static int need_dispatch = 0; 49 static struct notification *notifications = NULL; 50 static DBusConnection *connection = NULL; 51 52 int use_json = 0; 53 static int changed = 1; 54 static const char *cmd = NULL; 55 56 static time_t expirys[MAX_WATCH] = { 0 }; 57 static DBusTimeout *timeouts[MAX_WATCH] = { 0 }; 58 static size_t nt = 0; 59 60 static void groan(const char *fmt, ...); 61 static void die(const char *fmt, ...); 62 63 static short poll_flags(unsigned int dbf) 64 { 65 return (dbf & DBUS_WATCH_READABLE ? POLLIN : 0) | 66 (dbf & DBUS_WATCH_WRITABLE ? POLLOUT : 0) | 67 (dbf & DBUS_WATCH_ERROR ? POLLERR : 0) | 68 (dbf & DBUS_WATCH_HANGUP ? POLLHUP : 0); 69 } 70 71 static unsigned int dw_flags(short pf) 72 { 73 return (pf & POLLIN ? DBUS_WATCH_READABLE : 0) | 74 (pf & POLLOUT ? DBUS_WATCH_WRITABLE : 0) | 75 (pf & POLLERR ? DBUS_WATCH_ERROR : 0) | 76 (pf & POLLHUP ? DBUS_WATCH_HANGUP : 0); 77 } 78 79 static void toggle_watch(DBusWatch *watch, void *data) 80 { 81 size_t i = (size_t)dbus_watch_get_data(watch); 82 83 (void)data; 84 85 if (dbus_watch_get_enabled(watch)) 86 pfds[i].events = poll_flags(dbus_watch_get_flags(watch)); 87 else 88 pfds[i].events = 0; 89 } 90 91 static void remove_watch(DBusWatch *watch, void *data) 92 { 93 size_t i = (size_t)dbus_watch_get_data(watch); 94 95 (void)data; 96 97 memmove(&pfds[i], &pfds[i + 1], (nw - i - 1) * sizeof(*pfds)); 98 memmove(&watches[i], &watches[i + 1], (nw - i - 1) * sizeof(*watches)); 99 100 for (; i < nw - 1; i++) 101 dbus_watch_set_data(watches[i], (void *)i, NULL); 102 nw--; 103 } 104 105 static dbus_bool_t add_watch(DBusWatch *watch, void *data) 106 { 107 (void)data; 108 109 if (nw == N(watches)) 110 return FALSE; 111 112 watches[nw] = watch; 113 pfds[nw].fd = dbus_watch_get_unix_fd(watch); 114 dbus_watch_set_data(watch, (void *)nw++, NULL); 115 116 toggle_watch(watch, NULL); 117 118 return TRUE; 119 } 120 121 void toggle_timeout(DBusTimeout *timeout, 122 void *data) 123 { 124 size_t i = (size_t)dbus_timeout_get_data(timeout); 125 126 (void)data; 127 128 if (dbus_timeout_get_enabled(timeout)) 129 expirys[i] = time(NULL) + 130 (dbus_timeout_get_interval(timeout) + 999) / 1000; 131 else 132 expirys[i] = 0; 133 } 134 135 static dbus_bool_t add_timeout(DBusTimeout *timeout, 136 void *data) 137 { 138 (void)data; 139 140 if (nt == N(timeouts)) 141 return FALSE; 142 143 timeouts[nt] = timeout; 144 expirys[nt] = 0; 145 dbus_timeout_set_data(timeout, (void *)nt++, NULL); 146 147 toggle_timeout(timeout, NULL); 148 149 return TRUE; 150 } 151 152 void remove_timeout(DBusTimeout *timeout, 153 void *data) 154 { 155 size_t i = (size_t)dbus_timeout_get_data(timeout); 156 157 (void)data; 158 159 memmove(&timeouts[i], &timeouts[i + 1], 160 (nt - i - 1) * sizeof(*timeouts)); 161 memmove(&expirys[i], &expirys[i + 1], 162 (nt - i - 1) * sizeof(*expirys)); 163 164 for (; i < nt - 1; i++) 165 dbus_timeout_set_data(timeouts[i], (void *)i, NULL); 166 nt--; 167 } 168 169 static void dispatch_status(DBusConnection *connection, 170 DBusDispatchStatus new_status, 171 void *data) 172 { 173 (void)connection; 174 (void)data; 175 need_dispatch = (new_status == DBUS_DISPATCH_DATA_REMAINS); 176 } 177 178 void trigger_timeouts(void) 179 { 180 size_t i; 181 time_t now = time(NULL); 182 183 for (i = 0; i < nt; i++) { 184 DBusTimeout *to; 185 if (expirys[i] > now) 186 continue; 187 to = timeouts[i]; 188 dbus_timeout_handle(to); 189 if (i < nt && timeouts[i] != to) 190 i--; 191 } 192 } 193 194 time_t timeout_next_expiry(void) 195 { 196 time_t rv = 0; 197 size_t i; 198 199 for (i = 0; i < nt; i++) 200 if (expirys[i] && (!rv || expirys[i] < rv)) 201 rv = expirys[i]; 202 203 return rv; 204 } 205 206 static void notif_spawn(struct notification *n) 207 { 208 if (!cmd) { 209 n->child = 0; 210 return; 211 } 212 213 n->child = fork(); 214 switch (n->child) { 215 case -1: 216 groan("fork() failed:"); 217 n->child = 0; 218 break; 219 case 0: 220 close(STDIN_FILENO); 221 open("/dev/null", O_RDONLY); 222 close(STDOUT_FILENO); 223 open("/dev/null", O_WRONLY); 224 execlp(cmd, cmd, n->d.summary, n->d.body, NULL); 225 exit(1); 226 break; 227 default: 228 break; 229 } 230 } 231 232 const struct notification *notif_update(DBusMessage *msg, 233 struct notification_data *d, 234 time_t expiry) 235 { 236 struct notification *i; 237 238 for (i = notifications; i && i->id != d->replaces; i = i->next); 239 240 if (!i) 241 return NULL; 242 243 changed |= use_json || strcmp(d->summary, i->d.summary); 244 dbus_message_unref(i->msg); 245 if (cmd && i->child) 246 kill(i->child, SIGTERM); 247 248 memcpy(&i->d, d, sizeof(i->d)); 249 i->expiry = expiry; 250 i->msg = dbus_message_ref(msg); 251 252 notif_spawn(i); 253 254 return i; 255 } 256 257 const struct notification *notif_add(DBusMessage *msg, 258 struct notification_data *d, 259 time_t expiry) 260 { 261 static dbus_uint32_t notification_id = 0; 262 struct notification *nn = malloc(sizeof(*nn)); 263 struct notification *ne; 264 265 if (!(nn->id = ++notification_id)) 266 nn->id = ++notification_id; 267 nn->msg = dbus_message_ref(msg); 268 memcpy(&nn->d, d, sizeof(nn->d)); 269 nn->expiry = expiry; 270 nn->next = NULL; 271 272 if (notifications) { 273 for (ne = notifications; ne->next; ne = ne->next); 274 ne->next = nn; 275 } else { 276 notifications = nn; 277 } 278 279 notif_spawn(nn); 280 281 changed = 1; 282 283 return nn; 284 } 285 286 enum reason { 287 ACTION, 288 EXPIRED, 289 DISMISSED, 290 REQUEST, 291 UNKNOWN, 292 }; 293 294 int notif_close(dbus_uint32_t id, pid_t pid, dbus_uint32_t reason) 295 { 296 DBusMessage *sig; 297 struct notification *n; 298 299 if (!notifications) 300 return 0; 301 if (notifications->id == id || notifications->child == pid) { 302 n = notifications; 303 notifications = n->next; 304 } else { 305 struct notification *i; 306 for (i = notifications; 307 i->next && i->next->id != id && i->next->child != pid; 308 i = i->next); 309 if (!i->next) 310 return 0; 311 n = i->next; 312 i->next = i->next->next; 313 } 314 315 if (reason == ACTION) { 316 sig = dbus_message_new_signal(N_PATH, N_IF, "ActionInvoked"); 317 dbus_message_append_args(sig, 318 DBUS_TYPE_UINT32, &n->id, 319 DBUS_TYPE_STRING, 320 &(const char *){ "default" }, 321 DBUS_TYPE_INVALID); 322 dbus_connection_send(connection, sig, NULL); 323 dbus_message_unref(sig); 324 reason = DISMISSED; 325 } 326 327 sig = dbus_message_new_signal(N_PATH, N_IF, "NotificationClosed"); 328 dbus_message_append_args(sig, 329 DBUS_TYPE_UINT32, &n->id, 330 DBUS_TYPE_UINT32, &reason, 331 DBUS_TYPE_INVALID); 332 333 dbus_connection_send(connection, sig, NULL); 334 dbus_message_unref(sig); 335 336 dbus_message_unref(n->msg); 337 if (cmd && n->child && n->child != pid) 338 kill(n->child, SIGTERM); 339 free(n); 340 341 changed = 1; 342 343 return 1; 344 } 345 346 time_t notif_next_expiry(void) 347 { 348 struct notification *i; 349 time_t rv = 0; 350 351 for (i = notifications; i; i = i->next) { 352 if (i->expiry && 353 (!rv || i->expiry < rv)) 354 rv = i->expiry; 355 } 356 357 return rv; 358 } 359 360 void notif_check_expiry(void) 361 { 362 time_t now = time(NULL); 363 struct notification *i; 364 struct notification *n; 365 366 for (i = notifications; i; i = n) { 367 n = i->next; 368 369 if (i->expiry && i->expiry <= now) 370 notif_close(i->id, -1, EXPIRED); 371 } 372 } 373 374 static void writ(const char *part, size_t plen, void *data) 375 { 376 fwrite(part, 1, plen, data); 377 } 378 379 static void esc(const char *string, const char *escape, 380 void (*cb)(const char *part, size_t plen, void *data), 381 void *data) { 382 static char buffer[1024]; 383 static const char *special = "\b\f\n\r\t"; 384 static const char *sp_rep = "bfnrt"; 385 size_t p = 0; 386 while (*string) { 387 while (*string && p < sizeof(buffer)) { 388 size_t len = strcspn(string, escape); 389 if (len > sizeof(buffer) - p) 390 len = sizeof(buffer) - p; 391 memcpy(buffer + p, string, len); 392 string += len; 393 p += len; 394 while (*string && strchr(escape, *string)) { 395 const char *sp; 396 buffer[p++] = '\\'; 397 if ((sp = strchr(special, *string))) 398 buffer[p++] = sp_rep[sp - special]; 399 else 400 buffer[p++] = *string; 401 string++; 402 } 403 } 404 cb(buffer, p, data); 405 p = 0; 406 } 407 } 408 409 void notif_dump(void) 410 { 411 const char *sep = ""; 412 struct notification *i; 413 414 if (use_json) 415 printf("["); 416 for (i = notifications; i; i = i->next) { 417 printf("%s", sep); 418 if (use_json) 419 output_notification(i->id, &i->d); 420 else 421 esc(i->d.summary, "\\\r\n\t\b\f", writ, stdout); 422 sep = delimiter; 423 } 424 if (use_json) 425 printf("]"); 426 puts(""); 427 fflush(stdout); 428 } 429 430 DBusHandlerResult handle_message(DBusConnection *connection, DBusMessage *message, void *user_data) 431 { 432 DBusMessage *reply; 433 434 (void)user_data; 435 436 if (dbus_message_is_method_call(message, "org.freedesktop.DBus.Introspectable", "Introspect")) { 437 static const char *notificationpath = N_PATH; 438 const char *path = dbus_message_get_path(message); 439 size_t pl = strlen(path); 440 441 if (strncmp(notificationpath, path, pl)) 442 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; 443 444 if (pl == 1) 445 pl = 0; 446 447 if (notificationpath[pl] == '/') { 448 size_t npl = strcspn(notificationpath + pl + 1, "/"); 449 char buffer[sizeof(INTROSPECTION_NODE_XML) + npl - 4]; 450 sprintf(buffer, INTROSPECTION_NODE_XML, (int)npl, notificationpath + pl + 1); 451 reply = dbus_message_new_method_return(message); 452 dbus_message_append_args(reply, 453 DBUS_TYPE_STRING, &(const char *){ buffer }, 454 DBUS_TYPE_INVALID); 455 } else if (notificationpath[pl]) { 456 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; 457 } else { 458 reply = dbus_message_new_method_return(message); 459 dbus_message_append_args(reply, 460 DBUS_TYPE_STRING, &(const char *){ INTROSPECTION_XML }, 461 DBUS_TYPE_INVALID); 462 } 463 } else if (dbus_message_is_method_call(message, N_IF, "GetServerInformation")) { 464 reply = dbus_message_new_method_return(message); 465 dbus_message_append_args(reply, 466 DBUS_TYPE_STRING, &(const char *){ "tmisu" }, 467 DBUS_TYPE_STRING, &(const char *){ "inz" }, 468 DBUS_TYPE_STRING, &(const char *){ "1.0" }, 469 DBUS_TYPE_STRING, &(const char *){ "1.2" }, 470 DBUS_TYPE_INVALID); 471 } else if (dbus_message_is_method_call(message, N_IF, "GetCapabilities")) { 472 DBusMessageIter i; 473 DBusMessageIter j; 474 475 reply = dbus_message_new_method_return(message); 476 dbus_message_iter_init_append(reply, &i); 477 dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING_AS_STRING, &j); 478 dbus_message_iter_append_basic(&j, DBUS_TYPE_STRING, &(const char *){ "body" }); 479 dbus_message_iter_append_basic(&j, DBUS_TYPE_STRING, &(const char *){ "actions" }); 480 dbus_message_iter_close_container(&i, &j); 481 } else if (dbus_message_is_method_call(message, N_IF, "Notify")) { 482 const struct notification *n = NULL; 483 struct notification_data d; 484 DBusMessageIter iter; 485 time_t expiry; 486 487 if (!dbus_message_has_signature(message, "susssasa{sv}i")) 488 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; 489 490 dbus_message_iter_init(message, &iter); 491 dbus_message_iter_get_basic(&iter, &d.app_name); 492 dbus_message_iter_next(&iter); 493 dbus_message_iter_get_basic(&iter, &d.replaces); 494 dbus_message_iter_next(&iter); 495 dbus_message_iter_get_basic(&iter, &d.icon); 496 dbus_message_iter_next(&iter); 497 dbus_message_iter_get_basic(&iter, &d.summary); 498 dbus_message_iter_next(&iter); 499 dbus_message_iter_get_basic(&iter, &d.body); 500 dbus_message_iter_next(&iter); 501 dbus_message_iter_recurse(&iter, &d.actions); 502 dbus_message_iter_next(&iter); 503 dbus_message_iter_recurse(&iter, &d.hints); 504 dbus_message_iter_next(&iter); 505 dbus_message_iter_get_basic(&iter, &d.expiry_ms); 506 507 if (d.expiry_ms < 0) 508 d.expiry_ms = default_exp_timeout * 1000; 509 if (d.expiry_ms) 510 expiry = time(NULL) + (d.expiry_ms + 999) / 1000; 511 else 512 expiry = 0; 513 514 if (d.replaces) 515 n = notif_update(message, 516 &d, 517 expiry); 518 if (!n) 519 n = notif_add(message, 520 &d, 521 expiry); 522 523 reply = dbus_message_new_method_return(message); 524 // output_notification(message, n->id, cnf->fmt, cnf->delimiter); 525 dbus_message_append_args(reply, 526 DBUS_TYPE_UINT32, &n->id, 527 DBUS_TYPE_INVALID); 528 } else if (dbus_message_is_method_call(message, N_IF, "CloseNotification")) { 529 if (!dbus_message_has_signature(message, "u")) 530 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; 531 dbus_uint32_t id; 532 dbus_message_get_args(message, NULL, 533 DBUS_TYPE_UINT32, &id, 534 DBUS_TYPE_INVALID); 535 if (notif_close(id, -1, REQUEST)) 536 reply = dbus_message_new_method_return(message); 537 else 538 reply = dbus_message_new_error(message, 539 DBUS_ERROR_INVALID_ARGS, 540 "No such id"); 541 } else { 542 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; 543 } 544 545 dbus_connection_send(connection, reply, NULL); 546 dbus_message_unref(reply); 547 548 return DBUS_HANDLER_RESULT_HANDLED; 549 } 550 551 int sfd[2]; 552 553 void sig_handler(int signal) 554 { 555 if (write(sfd[1], &(char){ signal }, 1) != 1) 556 exit(1); 557 } 558 559 static void die(const char *fmt, ...) 560 { 561 va_list args; 562 va_start(args, fmt); 563 vfprintf(stderr, fmt, args); 564 va_end(args); 565 if (*fmt && fmt[strlen(fmt) - 1] == ':') 566 fprintf(stderr, " %s", strerror(errno)); 567 fputs("\n", stderr); 568 exit(1); 569 } 570 571 static void groan(const char *fmt, ...) 572 { 573 va_list args; 574 va_start(args, fmt); 575 vfprintf(stderr, fmt, args); 576 va_end(args); 577 if (*fmt && fmt[strlen(fmt) - 1] == ':') 578 fprintf(stderr, " %s", strerror(errno)); 579 fputs("\n", stderr); 580 } 581 582 static void usage(void) 583 { 584 die("usage: %s [-d delim] [-j] [-c command]\n" 585 "-h\tThis help\n" 586 "-d\tDelimeter for default output style.\n" 587 "-j\tUse JSON output style\n" 588 "-c\tExecute command on notifications\n", 589 argv0); 590 } 591 592 int main(int argc, char **argv) 593 { 594 int r; 595 596 ARGBEGIN { 597 case 'd': 598 delimiter = EARGF(usage()); 599 break; 600 case 'j': 601 use_json = 1; 602 break; 603 case 'c': 604 cmd = EARGF(usage()); 605 break; 606 default: 607 usage(); 608 break; 609 } ARGEND; 610 611 612 if (pipe(sfd)) 613 die("Unable to create pipe:"); 614 615 connection = dbus_bus_get_private(DBUS_BUS_SESSION, NULL); 616 if (!connection) 617 die("Failed to connect to session bus"); 618 619 dbus_connection_set_watch_functions(connection, 620 add_watch, 621 remove_watch, 622 toggle_watch, 623 NULL, NULL); 624 dbus_connection_set_timeout_functions(connection, 625 add_timeout, 626 remove_timeout, 627 toggle_timeout, 628 NULL, NULL); 629 dbus_connection_set_dispatch_status_function(connection, 630 dispatch_status, 631 NULL, NULL); 632 dispatch_status(connection, 633 dbus_connection_get_dispatch_status(connection), 634 NULL); 635 636 r = dbus_bus_request_name(connection, 637 N_SRV, 638 DBUS_NAME_FLAG_REPLACE_EXISTING | 639 DBUS_NAME_FLAG_DO_NOT_QUEUE, NULL); 640 641 if (r != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) 642 die("Could not acquire service name, " 643 "is another notification daemon running?"); 644 645 dbus_bus_add_match(connection, "interface=" N_IF " ,path=" N_PATH "," 646 "type=method_call", NULL); 647 dbus_bus_add_match(connection, 648 "interface=org.freedesktop.DBus.Introspectable," 649 "method=Introspect,type=method_call", NULL); 650 dbus_connection_add_filter(connection, handle_message, NULL, NULL); 651 652 signal(SIGINT, sig_handler); 653 signal(SIGTERM, sig_handler); 654 signal(SIGUSR1, sig_handler); 655 signal(SIGUSR2, sig_handler); 656 signal(SIGCHLD, sig_handler); 657 658 pfdb[0].fd = sfd[0]; 659 pfdb[0].events = POLLIN; 660 661 for (;;) { 662 size_t i; 663 time_t nexp; 664 time_t texp; 665 int interval; 666 667 notif_check_expiry(); 668 trigger_timeouts(); 669 670 if (!need_dispatch) { 671 nexp = notif_next_expiry(); 672 texp = timeout_next_expiry(); 673 674 if (changed) 675 notif_dump(); 676 changed = 0; 677 678 if (nexp && (!texp || nexp <= texp)) 679 interval = (nexp - time(NULL)) * 1000; 680 else if (texp && (!nexp || texp < nexp)) 681 interval = (texp - time(NULL)) * 1000; 682 else 683 interval = -1; 684 } else { 685 interval = 0; 686 } 687 688 int r = poll(pfdb, nw + 1, interval); 689 690 if (need_dispatch) 691 dbus_connection_dispatch(connection); 692 693 if (pfdb[0].revents) { 694 char s; 695 if (read(pfdb[0].fd, &s, 1) != 1) 696 break; 697 698 if (s == SIGINT || s == SIGTERM) 699 break; 700 701 if (s == SIGUSR1 || s == SIGUSR2) { 702 if (notifications) 703 notif_close(notifications->id, -1, 704 s == SIGUSR1 ? DISMISSED : 705 ACTION); 706 } else if (s == SIGCHLD) { 707 int cstatus; 708 pid_t pid = waitpid(-1, &cstatus, WNOHANG); 709 710 if (pid < 0) 711 groan("waitpid() failed:"); 712 else if (!pid) 713 groan("Spurious SIGCHLD"); 714 else if (WIFEXITED(cstatus) && 715 WEXITSTATUS(cstatus) == 0) 716 notif_close(0, pid, ACTION); 717 else 718 notif_close(0, pid, DISMISSED); 719 } 720 signal(s, sig_handler); 721 } 722 723 for (i = 0; i < nw && r; i++) { 724 if (!pfds[i].revents) 725 continue; 726 dbus_watch_handle(watches[i], 727 dw_flags(pfds[i].revents)); 728 toggle_watch(watches[i], NULL); 729 r--; 730 } 731 } 732 733 dbus_bus_release_name(connection, 734 "org.freedesktop.Notifications", NULL); 735 dbus_connection_remove_filter(connection, handle_message, NULL); 736 dbus_bus_remove_match(connection, 737 "interface=" N_IF "," 738 "path=" N_PATH "," 739 "type=method_call", NULL); 740 dbus_bus_remove_match(connection, 741 "interface=org.freedesktop.DBus.Introspectable," 742 "method=Introspect," 743 "type=method_call", NULL); 744 dbus_connection_close(connection); 745 dbus_connection_unref(connection); 746 747 return 0; 748 }