├── .gitignore ├── systembus-notify.desktop ├── notify.h ├── log.c ├── testmessage.sh ├── log.h ├── LICENSE ├── Makefile ├── notify.c ├── .github └── workflows │ └── ci.yml ├── README.md ├── main.c └── bus-diagram.svg /.gitignore: -------------------------------------------------------------------------------- 1 | /systembus-notify 2 | -------------------------------------------------------------------------------- /systembus-notify.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=systembus-notify 4 | Exec=systembus-notify 5 | -------------------------------------------------------------------------------- /notify.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | 3 | #ifndef NOTIFY_H 4 | #define NOTIFY_H 5 | 6 | void notify(sd_bus* bus, const char* summary, const char* body); 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /log.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int quiet = 0; 5 | 6 | void debug(const char* fmt, ...) 7 | { 8 | if (quiet) { 9 | return; 10 | } 11 | va_list vl; 12 | va_start(vl, fmt); 13 | vfprintf(stdout, fmt, vl); 14 | va_end(vl); 15 | return; 16 | } 17 | -------------------------------------------------------------------------------- /testmessage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | ./systembus-notify & 6 | sleep 0.1 7 | dbus-send --system --type=signal / net.nuetzlich.SystemNotifications.Notify \ 8 | "string:systembus-notify test $$" 9 | dbus-send --system --type=signal / net.nuetzlich.SystemNotifications.Notify \ 10 | "string:systembus-notify test $$ with body" "string:$(date)" 11 | sleep 0.1 12 | kill %1 13 | -------------------------------------------------------------------------------- /log.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | #ifndef LOG_H 3 | #define LOG_H 4 | 5 | extern int quiet; 6 | 7 | /* From https://gcc.gnu.org/onlinedocs/gcc-9.2.0/gcc/Common-Function-Attributes.html : 8 | * The format attribute specifies that a function takes printf 9 | * style arguments that should be type-checked against a format string. 10 | */ 11 | void debug(const char* fmt, ...) __attribute__((format(printf, 1, 2))); 12 | 13 | // Exit codes 14 | enum { 15 | FATAL_USAGE = 2, // Usage error 16 | FATAL_SYSTEM_BUS = 3, // Problem connecting to system d-bus 17 | FATAL_USER_BUS = 4, // Problem connecting to user d-bus 18 | FATAL_SYSTEM_BUS_PROCESS = 5, // Problem processing d-bus signals from system d-bus 19 | FATAL_EVENT = 6, // Problem in event loop 20 | FATAL_SEND_NOTIFY = 7, // problem calling notify on the user bus 21 | }; 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jakob Unterwurzacher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS += -Wall -Wextra -Wformat-security -Wconversion -fstack-protector-all -std=gnu99 -g 2 | 3 | # If the user has libelogind installed, we can assume 4 | # that they want to use libelogind instead of libsystemd. 5 | $(shell pkg-config --exists libelogind 2> /dev/null) 6 | ifeq ($(.SHELLSTATUS),0) 7 | $(info libelogind is installed: using libelogind instead of libsystemd) 8 | CFLAGS += $(shell pkg-config --cflags libelogind) 9 | LDLIBS += $(shell pkg-config --libs libelogind) 10 | else 11 | LDLIBS += -lsystemd 12 | endif 13 | 14 | systembus-notify: *.c *.h Makefile 15 | $(CC) $(LDFLAGS) $(CFLAGS) -o systembus-notify *.c $(LDLIBS) 16 | 17 | # Depends on compilation to make sure the syntax is ok. 18 | format: systembus-notify 19 | clang-format --style=WebKit -i *.h *.c 20 | 21 | .PHONY: install 22 | install: systembus-notify 23 | mkdir -p ${HOME}/bin ${HOME}/.config/autostart 24 | cp systembus-notify ${HOME}/bin 25 | cp systembus-notify.desktop ${HOME}/.config/autostart 26 | 27 | .PHONY: uninstall 28 | uninstall: 29 | # delete in reverse order compared to install 30 | rm -f ${HOME}/.config/systemd/user/systembus-notify.service 31 | rm -f ${HOME}/.config/autostart/systembus-notify.desktop 32 | rm -f ${HOME}/bin/systembus-notify 33 | 34 | .PHONY: test 35 | test: systembus-notify 36 | cppcheck -q . || true 37 | ./testmessage.sh 38 | 39 | .PHONY: clean 40 | clean: 41 | rm -f systembus-notify 42 | -------------------------------------------------------------------------------- /notify.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "log.h" 9 | 10 | // notify sends a desktop notification according to the 11 | // Desktop Notifications Specification 12 | // (https://developer.gnome.org/notification-spec/). 13 | void notify(sd_bus* bus, const char* summary, const char* body) 14 | { 15 | sd_bus_message* m = NULL; 16 | debug("sending d-bus desktop notification on user bus: "); 17 | int ret = sd_bus_message_new_method_call(bus, &m, "org.freedesktop.Notifications", 18 | "/org/freedesktop/Notifications", 19 | "org.freedesktop.Notifications", "Notify"); 20 | if (ret < 0) { 21 | fprintf(stderr, "sd_bus_message_new_method_call: %s\n", strerror(-ret)); 22 | return; 23 | } 24 | // Fill out the parameters according to 25 | // https://specifications.freedesktop.org/notification-spec/1.3/protocol.html#command-notify 26 | ret = sd_bus_message_append(m, "susssasa{sv}i", 27 | "systembus-notify", // STRING app_name 28 | 0, // UINT32 replaces_id 29 | "utilities-system-monitor", // STRING app_icon 30 | summary, // STRING summary 31 | body, // STRING body 32 | 0, // ARRAY actions 33 | 0, // DICT hints 34 | -1 // INT32 expire_timeout 35 | ); 36 | if (ret < 0) { 37 | fprintf(stderr, "sd_bus_message_append: %s\n", strerror(-ret)); 38 | sd_bus_message_unref(m); 39 | return; 40 | } 41 | ret = sd_bus_call(bus, m, 0, NULL, NULL); 42 | sd_bus_message_unref(m); 43 | if (ret == -ENOTCONN) { 44 | fprintf(stderr, "fatal: sd_bus_call: %s\n", strerror(-ret)); 45 | exit(FATAL_SEND_NOTIFY); 46 | } else if (ret < 0) { 47 | fprintf(stderr, "sd_bus_call: %s\n", strerror(-ret)); 48 | return; 49 | } 50 | debug("ok\n"); 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 23 * * 5' # Every Friday at 23:00 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | # Build and test in Ubuntu VM provided by Github CI 15 | - uses: actions/checkout@v3 16 | - run: | 17 | set -ex 18 | sudo apt-get -qq update > /dev/null < /dev/null 19 | sudo apt-get -qq install libsystemd-dev > /dev/null < /dev/null 20 | - run: make 21 | - run: make test 22 | - run: git clean -dxff 23 | 24 | # Build in Debian container with libsystemd 25 | # ("make test" does not work in container) 26 | - name: Build in Debian container with libsystemd 27 | uses: addnab/docker-run-action@v3 28 | with: 29 | image: debian:bullseye 30 | options: -v ${{ github.workspace }}:/work 31 | run: | 32 | set -ex 33 | apt-get -qq update > /dev/null < /dev/null 34 | apt-get -qq install make gcc libsystemd-dev > /dev/null < /dev/null 35 | cd /work 36 | make 37 | - run: git clean -dxff 38 | 39 | # Build in Debian container with libelogind 40 | # ("make test" does not work in container) 41 | - name: Build in Debian container with libelogind 42 | uses: addnab/docker-run-action@v3 43 | with: 44 | image: debian:bullseye 45 | options: -v ${{ github.workspace }}:/work 46 | run: | 47 | set -ex 48 | apt-get -qq update > /dev/null < /dev/null 49 | apt-get -qq install make gcc pkg-config libelogind-dev > /dev/null < /dev/null 50 | cd /work 51 | make 52 | - run: git clean -dxff 53 | 54 | # Build in Fedora container with libsystemd 55 | # ("make test" does not work in container) 56 | - name: Build in Fedora container 57 | uses: addnab/docker-run-action@v3 58 | with: 59 | image: fedora:36 60 | options: -v ${{ github.workspace }}:/work 61 | run: | 62 | set -ex 63 | dnf -qy install make gcc systemd-devel 64 | cd /work 65 | make 66 | - run: git clean -dxff 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | systembus-notify - system bus notification daemon 2 | ================================================= 3 | 4 | [![ci](https://github.com/rfjakob/systembus-notify/actions/workflows/ci.yml/badge.svg)](https://github.com/rfjakob/systembus-notify/actions/workflows/ci.yml) 5 | 6 | Tiny daemon that listens for `net.nuetzlich.SystemNotifications.Notify` 7 | signals on the D-Bus **system** bus and shows them as 8 | [desktop notifications](https://developer.gnome.org/notification-spec/) 9 | using the **user** bus. Works on Linux with Gnome, Cinnamon, KDE, LXQT, ... 10 | 11 | ![Bus Diagram](bus-diagram.svg) 12 | 13 | See https://github.com/rfjakob/earlyoom/issues/183 for the raison d'être. 14 | 15 | 16 | Compile 17 | ------- 18 | 19 | Just run 20 | 21 | ``` 22 | make 23 | ``` 24 | 25 | If you want `systembus-notify` to automatically start when you log in, 26 | also run (no sudo required): 27 | 28 | ``` 29 | make install 30 | ``` 31 | 32 | Test 33 | ---- 34 | 35 | Just run 36 | 37 | ``` 38 | make test 39 | ``` 40 | 41 | You should see "systembus-notify test" as a desktop notification. 42 | 43 | Or manually: 44 | 45 | ``` 46 | ./systembus-notify & 47 | dbus-send --system / net.nuetzlich.SystemNotifications.Notify 'string:summary text only' 48 | dbus-send --system / net.nuetzlich.SystemNotifications.Notify 'string:summary text' 'string:and body text' 49 | ``` 50 | 51 | Dependencies 52 | ------------ 53 | 54 | systembus-notify uses the `sd-bus` D-Bus library that is part of `libsystemd`. 55 | Install it like this: 56 | 57 | Debian / Ubuntu 58 | 59 | ``` 60 | sudo apt install libsystemd-dev 61 | ``` 62 | 63 | Fedora 64 | 65 | ``` 66 | sudo dnf install systemd-devel 67 | ``` 68 | 69 | Send notifications from your service 70 | ------------------------------------ 71 | 72 | The simplest way is to call the `dbus-send` tool. 73 | 74 | Shell: 75 | ```sh 76 | dbus-send --system / net.nuetzlich.SystemNotifications.Notify "string:hello world" 77 | 78 | ``` 79 | 80 | C (don't use `system()`, it's insecure!): 81 | ```C 82 | int pid = fork(); 83 | if (pid == 0) 84 | execl("/usr/bin/dbus-send", "dbus-send", "--system", "/", "net.nuetzlich.SystemNotifications.Notify", "string:hello world", NULL); 85 | ``` 86 | 87 | You can also use libdbus or another d-bus library to send the signal. 88 | In any case, systembus-notify must be running for the notification to pop up. 89 | 90 | See Also 91 | -------- 92 | 93 | Original idea is from https://wiki.debianforum.de/Desktop-Notification_von_Systemservice_mittels_dbus (German). 94 | 95 | Similar thing implemented in Python: https://github.com/xundeenergie/system-notification 96 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "log.h" 11 | #include "notify.h" 12 | 13 | static sd_bus* user_bus = NULL; 14 | 15 | // Did we alread send a notification this decisecond (tenth of a second)? 16 | bool over_ratelimit() { 17 | static long decisec_last = 0; 18 | 19 | struct timespec now; 20 | clock_gettime(CLOCK_MONOTONIC, &now); 21 | long decisec_now = now.tv_sec*10+now.tv_nsec/100000000; 22 | 23 | // Decisec has rolled over? 24 | if (decisec_last != decisec_now) { 25 | decisec_last = decisec_now; 26 | return false; 27 | } 28 | 29 | return true; 30 | } 31 | 32 | // handle_dbus_signal is called when a d-bus "Notify" signal is received. 33 | int handle_dbus_signal(sd_bus_message* m, void* userdata, sd_bus_error* ret_error) 34 | { 35 | // unused 36 | (void)userdata; 37 | (void)ret_error; 38 | 39 | // summary is optional. It stays NULL when the signal does not contain 40 | // a string value. 41 | const char* summary = NULL; 42 | sd_bus_message_read_basic(m, 's', &summary); 43 | // body is optional. It stays NULL when the signal does not contain 44 | // a second string value. 45 | const char* body = NULL; 46 | sd_bus_message_read_basic(m, 's', &body); 47 | debug("received d-bus signal on system bus: summary=%s body=%s\n", summary, body); 48 | if(over_ratelimit()) { 49 | fprintf(stderr, "over ratelimit, dropping message\n"); 50 | return 0; 51 | } 52 | notify(user_bus, summary, body); 53 | return 0; 54 | } 55 | 56 | int main(int argc, char* argv[]) 57 | { 58 | // Don't buffer our debug output to prevent interleaving 59 | // with stderr. 60 | setvbuf(stdout, NULL, _IONBF, 0); 61 | setlinebuf(stdout); 62 | // Parse command line 63 | int c = 0; 64 | const char* short_opt = "hq"; 65 | struct option long_opt[] = { 66 | { "help", no_argument, NULL, 'h' }, 67 | { 0, 0, NULL, 0 } /* end-of-array marker */ 68 | }; 69 | while ((c = getopt_long(argc, argv, short_opt, long_opt, NULL)) != -1) { 70 | switch (c) { 71 | case -1: /* no more arguments */ 72 | case 0: /* long option toggles */ 73 | break; 74 | case 'q': 75 | quiet = 1; 76 | break; 77 | case 'h': 78 | fprintf(stderr, "Usage: %s [-q] [-h]\n", argv[0]); 79 | fprintf(stderr, " -q ........... quiet (show errors only)\n"); 80 | fprintf(stderr, " -h, --help ... this help text\n"); 81 | exit(0); 82 | break; 83 | case '?': 84 | default: 85 | // getopt already printed out something like this: 86 | // ./systembus-notify: unrecognized option '--asdfasdf' 87 | fprintf(stderr, "Try '--help' for more information.\n"); 88 | exit(FATAL_USAGE); 89 | } 90 | } 91 | if (optind < argc) { 92 | fprintf(stderr, "Extra argument not understood: '%s'\n", argv[optind]); 93 | exit(FATAL_USAGE); 94 | } 95 | 96 | // Connect to D-Buses 97 | while(1) { 98 | debug("connecting to d-bus user bus: "); 99 | int ret = sd_bus_default_user(&user_bus); 100 | if(ret >= 0) { 101 | const char* a = NULL; 102 | sd_bus_get_address(user_bus, &a); 103 | debug("ok: %s\n", a); 104 | sd_bus_set_exit_on_disconnect(user_bus, 1); 105 | break; 106 | } 107 | fprintf(stderr, "sd_bus_default_user: %s. Retrying\n", strerror(-ret)); 108 | sleep(1); 109 | } 110 | 111 | sd_bus* system_bus = NULL; 112 | while(1) { 113 | debug("connecting to d-bus system bus: "); 114 | int ret = sd_bus_default_system(&system_bus); 115 | if(ret >= 0) { 116 | const char* a = NULL; 117 | sd_bus_get_address(system_bus, &a); 118 | debug("ok: %s\n", a); 119 | sd_bus_set_exit_on_disconnect(system_bus, 1); 120 | break; 121 | } 122 | fprintf(stderr, "sd_bus_default_system: %s. Retrying\n", strerror(-ret)); 123 | sleep(1); 124 | } 125 | 126 | // Connect D-Bus signal handler 127 | const char* match_rule = "type='signal',interface='net.nuetzlich.SystemNotifications',member='Notify'"; 128 | int ret = sd_bus_add_match(system_bus, NULL, match_rule, handle_dbus_signal, NULL); 129 | if (ret < 0) { 130 | fprintf(stderr, "fatal: sd_bus_match_signal: %s\n", strerror(-ret)); 131 | exit(FATAL_SYSTEM_BUS); 132 | } 133 | debug("waiting for d-bus signals on system bus: %s\n", match_rule); 134 | 135 | // Set up event loop 136 | sd_event* event = NULL; 137 | ret = sd_event_default(&event); 138 | if (ret < 0) { 139 | fprintf(stderr, "fatal: sd_event_default: %s\n", strerror(-ret)); 140 | exit(FATAL_EVENT); 141 | } 142 | ret = sd_bus_attach_event(user_bus, event, 0); 143 | if (ret < 0) { 144 | fprintf(stderr, "fatal: sd_bus_attach_event: %s\n", strerror(-ret)); 145 | exit(FATAL_EVENT); 146 | } 147 | ret = sd_bus_attach_event(system_bus, event, 0); 148 | if (ret < 0) { 149 | fprintf(stderr, "fatal: sd_bus_attach_event: %s\n", strerror(-ret)); 150 | exit(FATAL_EVENT); 151 | } 152 | 153 | // Run event loop (should not return) 154 | ret = sd_event_loop(event); 155 | fprintf(stderr, "fatal: sd_event_loop: %s\n", strerror(-ret)); 156 | exit(FATAL_EVENT); 157 | } 158 | -------------------------------------------------------------------------------- /bus-diagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 45 | 47 | 50 | 54 | 58 | 62 | 63 | 69 | 74 | 75 | 81 | 86 | 87 | 93 | 99 | 104 | 105 | 111 | 116 | 117 | 123 | 128 | 129 | 135 | 140 | 141 | 147 | 152 | 153 | 159 | 164 | 165 | 171 | 176 | 177 | 183 | 188 | 189 | 198 | 199 | 201 | 202 | 204 | image/svg+xml 205 | 207 | 208 | 209 | 210 | 211 | 214 | 221 | systembus-notify 235 | 242 | service 1 252 | 259 | service 2 269 | 276 | ... 286 | 293 | 300 | 304 | 308 | 312 | 316 | 320 | 324 | 328 | 332 | 336 | 340 | 344 | 348 | 352 | 356 | 360 | > user session 368 | system services < 378 | 383 | 387 | 392 | 399 | 403 | 410 | 414 | 421 | 425 | 432 | 436 | 441 | 445 | 450 | 454 | 459 | 463 | 468 | 472 | 477 | 481 | 486 | 490 | 494 | 501 | 505 | system 515 | 520 | 524 | 529 | 536 | 540 | 547 | 551 | 558 | 562 | 569 | 573 | 578 | 582 | 587 | 591 | 596 | 600 | 605 | 609 | 614 | 618 | 623 | 627 | 631 | 638 | 642 | user 652 | 657 | 662 | dbus-send--system 678 | 682 | libdbus 693 | 697 | ... 708 | D-Bussignal 724 | D-Buscall 740 | DesktopNotification 756 | 757 | 758 | --------------------------------------------------------------------------------