diff --git a/CHANGELOG.md b/CHANGELOG.md index fa81d03..115c7a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ (https://codeberg.org/dnkl/yambar/issues/10). * river: added documentation (https://codeberg.org/dnkl/yambar/issues/9). +* script: new module, adds support for custom user scripts + (https://codeberg.org/dnkl/yambar/issues/11). ### Deprecated @@ -25,11 +27,14 @@ (https://codeberg.org/dnkl/yambar/issues/12). * mpd: fix compilation with clang (https://codeberg.org/dnkl/yambar/issues/16). +* Crash when the alpha component in a color value was 0. ### Security ### Contributors +* [JorwLNKwpH](https://codeberg.org/JorwLNKwpH) + ## 1.5.0 diff --git a/README.md b/README.md index 8395ad4..8052784 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ bar: For details, see the man pages (**yambar**(5) is a good start). +Example configurations can be found in [examples](examples/configurations). + ## Modules @@ -76,6 +78,9 @@ Available modules: * mpd * network * removables +* river +* script (see script [examples](examples/scripts)) +* sway-xkb * xkb (_XCB backend only_) * xwindow (_XCB backend only_) diff --git a/bar/bar.c b/bar/bar.c index d3abd14..526f80f 100644 --- a/bar/bar.c +++ b/bar/bar.c @@ -36,19 +36,16 @@ calculate_widths(const struct private *b, int *left, int *center, int *right) for (size_t i = 0; i < b->left.count; i++) { struct exposable *e = b->left.exps[i]; - assert(e != NULL); *left += b->left_spacing + e->width + b->right_spacing; } for (size_t i = 0; i < b->center.count; i++) { struct exposable *e = b->center.exps[i]; - assert(e != NULL); *center += b->left_spacing + e->width + b->right_spacing; } for (size_t i = 0; i < b->right.count; i++) { struct exposable *e = b->right.exps[i]; - assert(e != NULL); *right += b->left_spacing + e->width + b->right_spacing; } @@ -82,30 +79,24 @@ expose(const struct bar *_bar) for (size_t i = 0; i < bar->left.count; i++) { struct module *m = bar->left.mods[i]; struct exposable *e = bar->left.exps[i]; - if (e != NULL) e->destroy(e); - bar->left.exps[i] = module_begin_expose(m); } for (size_t i = 0; i < bar->center.count; i++) { struct module *m = bar->center.mods[i]; struct exposable *e = bar->center.exps[i]; - if (e != NULL) e->destroy(e); - bar->center.exps[i] = module_begin_expose(m); } for (size_t i = 0; i < bar->right.count; i++) { struct module *m = bar->right.mods[i]; struct exposable *e = bar->right.exps[i]; - if (e != NULL) e->destroy(e); - bar->right.exps[i] = module_begin_expose(m); } @@ -307,7 +298,6 @@ destroy(struct bar *bar) for (size_t i = 0; i < b->left.count; i++) { struct module *m = b->left.mods[i]; struct exposable *e = b->left.exps[i]; - if (e != NULL) e->destroy(e); m->destroy(m); @@ -315,7 +305,6 @@ destroy(struct bar *bar) for (size_t i = 0; i < b->center.count; i++) { struct module *m = b->center.mods[i]; struct exposable *e = b->center.exps[i]; - if (e != NULL) e->destroy(e); m->destroy(m); @@ -323,7 +312,6 @@ destroy(struct bar *bar) for (size_t i = 0; i < b->right.count; i++) { struct module *m = b->right.mods[i]; struct exposable *e = b->right.exps[i]; - if (e != NULL) e->destroy(e); m->destroy(m); diff --git a/bar/wayland.c b/bar/wayland.c index 32bd87a..f4b3cce 100644 --- a/bar/wayland.c +++ b/bar/wayland.c @@ -121,6 +121,8 @@ seat_destroy(struct seat *seat) if (seat == NULL) return; + free(seat->name); + if (seat->pointer.theme != NULL) wl_cursor_theme_destroy(seat->pointer.theme); if (seat->wl_pointer != NULL) @@ -134,7 +136,9 @@ seat_destroy(struct seat *seat) void * bar_backend_wayland_new(void) { - return calloc(1, sizeof(struct wayland_backend)); + struct wayland_backend *backend = calloc(1, sizeof(struct wayland_backend)); + backend->pipe_fds[0] = backend->pipe_fds[1] = -1; + return backend; } static void @@ -947,6 +951,11 @@ cleanup(struct bar *_bar) struct private *bar = _bar->private; struct wayland_backend *backend = bar->backend.data; + if (backend->pipe_fds[0] >= 0) + close(backend->pipe_fds[0]); + if (backend->pipe_fds[1] >= 0) + close(backend->pipe_fds[1]); + tll_foreach(backend->buffers, it) { if (it->item.wl_buf != NULL) wl_buffer_destroy(it->item.wl_buf); diff --git a/config.c b/config.c index 556891c..743a1a3 100644 --- a/config.c +++ b/config.c @@ -53,6 +53,9 @@ conf_to_color(const struct yml_node *node) uint16_t blue = hex_byte(&hex[4]); uint16_t alpha = hex_byte(&hex[6]); + if (alpha == 0) + return (pixman_color_t){0, 0, 0, 0}; + alpha |= alpha << 8; int alpha_div = 0xffff / alpha; diff --git a/doc/yambar-modules.5.scd b/doc/yambar-modules.5.scd index 330a9f8..0e02495 100644 --- a/doc/yambar-modules.5.scd +++ b/doc/yambar-modules.5.scd @@ -714,11 +714,107 @@ bar: true: string: margin: 5 - text: "{id}: {state}"``` + text: "{id}: {state}" +``` + +# SCRIPT + +This module executes a user-provided script (or binary!) that writes +tags on its stdout. + +The script can either exit immediately after writing a set of tags, in +which case yambar will display those tags until yambar is +terminated. Or, the script can continue executing and update yambar +with new tag sets, either periodically, or when there is new data to +feed to yambar. + +Tag sets, or _transactions_, are separated by an empty line. Each +_tag_ is a single line on the format: + +``` +name|type|value +``` + +Where _name_ is what you also use to refer to the tag in the yambar +configuration, _type_ is one of the tag types defined in +*yambar-tags*(5), and _value_ is the tag’s value. + +Example: + +``` +var1|string|hello +var2|int|13 + +var1|string|world +var2|int|37 +``` + +The example above consists of two transactions. Each transaction has +two tags: one string tag and one integer tag. The second transaction +replaces the tags from the first transaction. + +Supported _types_ are: + +- string +- int +- bool +- float +- range:n-m (e.g. *var|range:0-100|57*) + +## TAGS + +User defined. + +## CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:[ *Description* +| path +: string +: yes +: Path to script/binary to execute. Must be an absolute path. +| args +: list of strings +: no +: Arguments to pass to the script/binary. + +## EXAMPLES + +Here is an "hello world" example script: + +``` +#!/bin/sh + +while true; do + echo "test|string|hello" + echo "" + sleep 3 + + echo "test|string|world" + echo "" + sleep 3 +done +``` + +This script will emit a single string tag, _test_, and alternate its +value between *hello* and *world* every three seconds. + +A corresponding yambar configuration could look like this: + +``` +bar: + left: + - script: + path: /path/to/script.sh + args: [] + content: {string: {text: "{test}"}} +``` # SWAY-XKB -This module uses *Sway* extenions to the I3 IPC API to monitor input +This module uses *Sway* extensions to the I3 IPC API to monitor input devices' active XKB layout. As such, it requires Sway to be running. *Note* that the _content_ configuration option is a *template*; diff --git a/examples/laptop.conf b/examples/configurations/laptop.conf similarity index 100% rename from examples/laptop.conf rename to examples/configurations/laptop.conf diff --git a/examples/scripts/cpu.sh b/examples/scripts/cpu.sh new file mode 100755 index 0000000..66615c5 --- /dev/null +++ b/examples/scripts/cpu.sh @@ -0,0 +1,124 @@ +#!/bin/bash + +# cpu.sh - measures CPU usage at a configurable sample interval +# +# Usage: cpu.sh INTERVAL_IN_SECONDS +# +# This script will emit the following tags on stdout (N is the number +# of logical CPUs): +# +# Name Type +# -------------------- +# cpu range 0-100 +# cpu0 range 0-100 +# cpu1 range 0-100 +# ... +# cpuN-1 range 0-100 +# +# I.e. ‘cpu’ is the average (or aggregated) CPU usage, while cpuX is a +# specific CPU’s usage. +# +# Example configuration (update every second): +# +# - script: +# path: /path/to/cpu.sh +# args: [1] +# content: {string: {text: "{cpu}%"}} +# + +interval=${1} + +case ${interval} in + ''|*[!0-9]*) + echo "interval must be an integer" + exit 1 + ;; + *) + ;; +esac + +# Get number of CPUs, by reading /proc/stat +# The output looks like: +# +# cpu A B C D ... +# cpu0 A B C D ... +# cpu1 A B C D ... +# cpuN A B C D ... +# +# The first line is a summary line, accounting *all* CPUs +IFS=$'\n' readarray -t all_cpu_stats < <(grep -e "^cpu" /proc/stat) +cpu_count=$((${#all_cpu_stats[@]} - 1)) + +# Arrays of ‘previous’ idle and total stats, needed to calculate the +# difference between each sample. +prev_idle=() +prev_total=() +for i in $(seq ${cpu_count}); do + prev_idle+=(0) + prev_total+=(0) +done + +prev_average_idle=0 +prev_average_total=0 + +while true; do + IFS=$'\n' readarray -t all_cpu_stats < <(grep -e "^cpu" /proc/stat) + + usage=() # CPU usage in percent, 0 <= x <= 100 + + average_idle=0 # All CPUs idle time since boot + average_total=0 # All CPUs total time since boot + + for i in $(seq 0 $((cpu_count - 1))); do + # Split this CPUs stats into an array + stats=($(echo "${all_cpu_stats[$((i + 1))]}")) + + # man procfs(5) + user=${stats[1]} + nice=${stats[2]} + system=${stats[3]} + idle=${stats[4]} + iowait=${stats[5]} + irq=${stats[6]} + softirq=${stats[7]} + steal=${stats[8]} + guest=${stats[9]} + guestnice=${stats[10]} + + # Guest time already accounted for in user + user=$((user - guest)) + nice=$((nice - guestnice)) + + idle=$((idle + iowait)) + + total=$((user + nice + system + irq + softirq + idle + steal + guest + guestnice)) + + average_idle=$((average_idle + idle)) + average_total=$((average_total + total)) + + # Diff since last sample + diff_idle=$((idle - prev_idle[i])) + diff_total=$((total - prev_total[i])) + + usage[i]=$((100 * (diff_total - diff_idle) / diff_total)) + + prev_idle[i]=${idle} + prev_total[i]=${total} + done + + diff_average_idle=$((average_idle - prev_average_idle)) + diff_average_total=$((average_total - prev_average_total)) + + average_usage=$((100 * (diff_average_total - diff_average_idle) / diff_average_total)) + + prev_average_idle=${average_idle} + prev_average_total=${average_total} + + echo "cpu|range:0-100|${average_usage}" + for i in $(seq 0 $((cpu_count - 1))); do + echo "cpu${i}|range:0-100|${usage[i]}" + done + + echo "" + sleep "${interval}" +done diff --git a/main.c b/main.c index d93b869..f8a8453 100644 --- a/main.c +++ b/main.c @@ -326,7 +326,7 @@ main(int argc, char *const *argv) thrd_t bar_thread; thrd_create(&bar_thread, (int (*)(void *))bar->run, bar); - /* Now unblock. We should be only thread receiving SIGINT */ + /* Now unblock. We should be only thread receiving SIGINT/SIGTERM */ pthread_sigmask(SIG_UNBLOCK, &signal_mask, NULL); if (pid_file != NULL) { @@ -336,15 +336,17 @@ main(int argc, char *const *argv) while (!aborted) { struct pollfd fds[] = {{.fd = abort_fd, .events = POLLIN}}; - int r __attribute__((unused)) = poll(fds, 1, -1); + int r __attribute__((unused)) = poll(fds, sizeof(fds) / sizeof(fds[0]), -1); - /* - * Either the bar aborted (triggering the abort_fd), or user - * killed us (triggering the signal handler which sets - * 'aborted') - */ - assert(aborted || r == 1); - break; + if (fds[0].revents & (POLLIN | POLLHUP)) { + /* + * Either the bar aborted (triggering the abort_fd), or user + * killed us (triggering the signal handler which sets + * 'aborted') + */ + assert(aborted || r == 1); + break; + } } if (aborted) diff --git a/modules/battery.c b/modules/battery.c index 4759a99..99bad3c 100644 --- a/modules/battery.c +++ b/modules/battery.c @@ -291,7 +291,10 @@ update_status(struct module *mod, int capacity_fd, int energy_fd, int power_fd, const char *status = readline_from_fd(status_fd); enum state state; - if (strcmp(status, "Full") == 0) + if (status == NULL) { + LOG_WARN("failed to read battery state"); + state = STATE_DISCHARGING; + } else if (strcmp(status, "Full") == 0) state = STATE_FULL; else if (strcmp(status, "Charging") == 0) state = STATE_CHARGING; diff --git a/modules/meson.build b/modules/meson.build index e05362f..5b480d7 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -9,7 +9,7 @@ mpd = dependency('libmpdclient') xcb_xkb = dependency('xcb-xkb', required: get_option('backend-x11')) # Module name -> (source-list, dep-list) -deps = { +mod_data = { 'alsa': [[], [m, alsa]], 'backlight': [[], [m, udev]], 'battery': [[], [udev]], @@ -19,11 +19,12 @@ deps = { 'mpd': [[], [mpd]], 'network': [[], []], 'removables': [[], [dynlist, udev]], - 'sway_xkb': [['i3-common.c', 'i3-common.h'], [dynlist, json]], + 'script': [[], []], + 'sway-xkb': [['i3-common.c', 'i3-common.h'], [dynlist, json]], } if backend_x11 - deps += { + mod_data += { 'xkb': [[], [xcb_stuff, xcb_xkb]], 'xwindow': [[], [xcb_stuff]], } @@ -48,25 +49,25 @@ if backend_wayland command: [wscanner_prog, 'private-code', '@INPUT@', '@OUTPUT@']) endforeach - deps += { - 'river': [[wl_proto_src + wl_proto_headers + river_proto_src + river_proto_headers], []], + mod_data += { + 'river': [[wl_proto_src + wl_proto_headers + river_proto_src + river_proto_headers], [dynlist]], } endif -foreach mod, data : deps +foreach mod, data : mod_data sources = data[0] - dep = data[1] + deps = data[1] if plugs_as_libs shared_module(mod, '@0@.c'.format(mod), sources, - dependencies: [module_sdk] + dep, + dependencies: [module_sdk] + deps, name_prefix: 'module_', install: true, install_dir: join_paths(get_option('libdir'), 'yambar')) else modules += [declare_dependency( sources: ['@0@.c'.format(mod)] + sources, - dependencies: [module_sdk] + dep, - compile_args: '-DHAVE_PLUGIN_@0@'.format(mod))] + dependencies: [module_sdk] + deps, + compile_args: '-DHAVE_PLUGIN_@0@'.format(mod.underscorify()))] endif endforeach diff --git a/modules/river.c b/modules/river.c index bb6a03b..b6e3db1 100644 --- a/modules/river.c +++ b/modules/river.c @@ -90,7 +90,8 @@ content(struct module *mod) } } - const size_t seat_count = m->title != NULL ? tll_length(m->seats) : 0; + const size_t seat_count = m->title != NULL && !m->is_starting_up + ? tll_length(m->seats) : 0; struct exposable *tag_parts[32 + seat_count]; for (unsigned i = 0; i < 32; i++) { @@ -122,7 +123,7 @@ content(struct module *mod) tag_set_destroy(&tags); } - if (m->title != NULL) { + if (m->title != NULL && !m->is_starting_up) { size_t i = 32; tll_foreach(m->seats, it) { const struct seat *seat = &it->item; @@ -565,6 +566,10 @@ run(struct module *mod) } wl_display_roundtrip(display); + + bool unlock_at_exit = true; + mtx_lock(&mod->lock); + m->is_starting_up = false; tll_foreach(m->outputs, it) @@ -572,6 +577,9 @@ run(struct module *mod) tll_foreach(m->seats, it) instantiate_seat(&it->item); + unlock_at_exit = false; + mtx_unlock(&mod->lock); + while (true) { wl_display_flush(display); @@ -618,6 +626,9 @@ out: wl_registry_destroy(registry); if (display != NULL) wl_display_disconnect(display); + + if (unlock_at_exit) + mtx_unlock(&mod->lock); return ret; } diff --git a/modules/script.c b/modules/script.c new file mode 100644 index 0000000..766ea96 --- /dev/null +++ b/modules/script.c @@ -0,0 +1,639 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#define LOG_MODULE "script" +#define LOG_ENABLE_DBG 0 +#include "../log.h" +#include "../config.h" +#include "../config-verify.h" +#include "../module.h" +#include "../plugin.h" + +struct private { + char *path; + size_t argc; + char **argv; + + struct particle *content; + + struct tag_set tags; + + struct { + char *data; + size_t sz; + size_t idx; + } recv_buf; +}; + +static void +destroy(struct module *mod) +{ + struct private *m = mod->private; + m->content->destroy(m->content); + + struct tag **tag_array = m->tags.tags; + tag_set_destroy(&m->tags); + free(tag_array); + + for (size_t i = 0; i < m->argc; i++) + free(m->argv[i]); + free(m->argv); + free(m->recv_buf.data); + free(m->path); + free(m); + module_default_destroy(mod); +} + +static struct exposable * +content(struct module *mod) +{ + const struct private *m = mod->private; + + mtx_lock(&mod->lock); + struct exposable *e = m->content->instantiate(m->content, &m->tags); + mtx_unlock(&mod->lock); + + return e; +} + +static struct tag * +process_line(struct module *mod, const char *line, size_t len) +{ + char *name = NULL; + char *value = NULL; + + const char *_name = line; + + const char *type = memchr(line, '|', len); + if (type == NULL) + goto bad_tag; + + size_t name_len = type - _name; + type++; + + const char *_value = memchr(type, '|', len - name_len - 1); + if (_value == NULL) + goto bad_tag; + + size_t type_len = _value - type; + _value++; + + size_t value_len = line + len - _value; + + LOG_DBG("%.*s: name=\"%.*s\", type=\"%.*s\", value=\"%.*s\"", + (int)len, line, + (int)name_len, _name, (int)type_len, type, (int)value_len, _value); + + name = malloc(name_len + 1); + memcpy(name, _name, name_len); + name[name_len] = '\0'; + + value = malloc(value_len + 1); + memcpy(value, _value, value_len); + value[value_len] = '\0'; + + struct tag *tag = NULL; + + if (type_len == 6 && memcmp(type, "string", 6) == 0) + tag = tag_new_string(mod, name, value); + + else if (type_len == 3 && memcmp(type, "int", 3) == 0) { + errno = 0; + char *end; + long v = strtol(value, &end, 0); + + if (errno != 0 || *end != '\0') { + LOG_ERR("tag value is not an integer: %s", value); + goto bad_tag; + } + tag = tag_new_int(mod, name, v); + } + + else if (type_len == 4 && memcmp(type, "bool", 4) == 0) { + bool v; + if (strcmp(value, "true") == 0) + v = true; + else if (strcmp(value, "false") == 0) + v = false; + else { + LOG_ERR("tag value is not a boolean: %s", value); + goto bad_tag; + } + tag = tag_new_bool(mod, name, v); + } + + else if (type_len == 5 && memcmp(type, "float", 5) == 0) { + errno = 0; + char *end; + double v = strtod(value, &end); + + if (errno != 0 || *end != '\0') { + LOG_ERR("tag value is not a float: %s", value); + goto bad_tag; + } + + tag = tag_new_float(mod, name, v); + } + + else if ((type_len > 6 && memcmp(type, "range:", 6) == 0) || + (type_len > 9 && memcmp(type, "realtime:", 9 == 0))) + { + const char *_start = type + 6; + const char *split = memchr(_start, '-', type_len - 6); + + if (split == NULL || split == _start || (split + 1) - type >= type_len) { + LOG_ERR( + "tag range delimiter ('-') not found in type: %.*s", + (int)type_len, type); + goto bad_tag; + } + + const char *_end = split + 1; + + size_t start_len = split - _start; + size_t end_len = type + type_len - _end; + + long start = 0; + for (size_t i = 0; i < start_len; i++) { + if (!(_start[i] >= '0' && _start[i] <= '9')) { + LOG_ERR( + "tag range start is not an integer: %.*s", + (int)start_len, _start); + goto bad_tag; + } + + start *= 10; + start += _start[i] - '0'; + } + + long end = 0; + for (size_t i = 0; i < end_len; i++) { + if (!(_end[i] >= '0' && _end[i] < '9')) { + LOG_ERR( + "tag range end is not an integer: %.*s", + (int)end_len, _end); + goto bad_tag; + } + + end *= 10; + end += _end[i] - '0'; + } + + if (type_len > 9 && memcmp(type, "realtime:", 9) == 0) { + LOG_ERR("unimplemented: realtime tag"); + goto bad_tag; + } + + errno = 0; + char *vend; + long v = strtol(value, &vend, 0); + if (errno != 0 || *vend != '\0') { + LOG_ERR("tag value is not an integer: %s", value); + goto bad_tag; + } + + if (v < start || v > end) { + LOG_ERR("tag value is outside range: %ld <= %ld <= %ld", + start, v, end); + goto bad_tag; + } + + tag = tag_new_int_range(mod, name, v, start, end); + } + + else { + goto bad_tag; + } + + free(name); + free(value); + return tag; + +bad_tag: + LOG_ERR("invalid tag: %.*s", (int)len, line); + free(name); + free(value); + return NULL; +} + +static void +process_transaction(struct module *mod, size_t size) +{ + struct private *m = mod->private; + mtx_lock(&mod->lock); + + size_t left = size; + const char *line = m->recv_buf.data; + + size_t line_count = 0; + { + const char *p = line; + while ((p = memchr(p, '\n', size - (p - line))) != NULL) { + p++; + line_count++; + } + } + + struct tag **old_tag_array = m->tags.tags; + tag_set_destroy(&m->tags); + free(old_tag_array); + + m->tags.tags = calloc(line_count, sizeof(m->tags.tags[0])); + m->tags.count = line_count; + + size_t idx = 0; + + while (left > 0) { + char *line_end = memchr(line, '\n', left); + assert(line_end != NULL); + + size_t line_len = line_end - line; + + struct tag *tag = process_line(mod, line, line_len); + if (tag != NULL) + m->tags.tags[idx++] = tag; + + left -= line_len + 1; + line += line_len + 1; + } + + m->tags.count = idx; + + mtx_unlock(&mod->lock); + mod->bar->refresh(mod->bar); +} + +static bool +data_received(struct module *mod, const char *data, size_t len) +{ + struct private *m = mod->private; + + if (len > m->recv_buf.sz - m->recv_buf.idx) { + size_t new_sz = m->recv_buf.sz == 0 ? 1024 : m->recv_buf.sz * 2; + char *new_buf = realloc(m->recv_buf.data, new_sz); + + if (new_buf == NULL) + return false; + + m->recv_buf.data = new_buf; + m->recv_buf.sz = new_sz; + } + + assert(m->recv_buf.sz >= m->recv_buf.idx); + assert(m->recv_buf.sz - m->recv_buf.idx >= len); + + memcpy(&m->recv_buf.data[m->recv_buf.idx], data, len); + m->recv_buf.idx += len; + + const char *eot = memmem(m->recv_buf.data, m->recv_buf.idx, "\n\n", 2); + if (eot == NULL) { + /* End of transaction not yet available */ + return true; + } + + const size_t transaction_size = eot - m->recv_buf.data + 1; + process_transaction(mod, transaction_size); + + assert(m->recv_buf.idx >= transaction_size + 1); + memmove(m->recv_buf.data, + &m->recv_buf.data[transaction_size + 1], + m->recv_buf.idx - (transaction_size + 1)); + m->recv_buf.idx -= transaction_size + 1; + + return true; +} + +static int +run_loop(struct module *mod, pid_t pid, int comm_fd) +{ + int ret = 0; + + while (true) { + struct pollfd fds[] = { + {.fd = mod->abort_fd, .events = POLLIN}, + {.fd = comm_fd, .events = POLLIN}, + }; + + int r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1); + if (r < 0) { + if (errno == EINTR) + continue; + LOG_ERRNO("failed to poll"); + break; + } + + if (fds[1].revents & POLLIN) { + char data[4096]; + ssize_t amount = read(comm_fd, data, sizeof(data)); + if (amount < 0) { + LOG_ERRNO("failed to read from script"); + break; + } + + LOG_DBG("recv: \"%.*s\"", (int)amount, data); + + data_received(mod, data, amount); + } + + if (fds[0].revents & POLLHUP) { + /* Aborted */ + break; + } + + if (fds[1].revents & POLLHUP) { + /* Child's stdout closed */ + LOG_DBG("script pipe closed (script terminated?)"); + break; + } + + if (fds[0].revents & POLLIN) { + /* Aborted */ + break; + } + } + + return ret; +} + +static int +run(struct module *mod) +{ + struct private *m = mod->private; + + /* Pipe to detect exec() failures */ + int exec_pipe[2]; + if (pipe2(exec_pipe, O_CLOEXEC) < 0) { + LOG_ERRNO("failed to create pipe"); + return -1; + } + + /* Stdout redirection pipe */ + int comm_pipe[2]; + if (pipe(comm_pipe) < 0) { + LOG_ERRNO("failed to create stdin/stdout redirection pipe"); + close(exec_pipe[0]); + close(exec_pipe[1]); + return -1; + } + + int pid = fork(); + if (pid < 0) { + LOG_ERRNO("failed to fork"); + close(comm_pipe[0]); + close(comm_pipe[1]); + close(exec_pipe[0]); + close(exec_pipe[1]); + return -1; + } + + if (pid == 0) { + /* Child */ + + /* Construct argv for execvp() */ + char *argv[1 + m->argc + 1]; + argv[0] = m->path; + for (size_t i = 0; i < m->argc; i++) + argv[i + 1] = m->argv[i]; + argv[1 + m->argc] = NULL; + + /* Restore signal handlers and signal mask */ + sigset_t mask; + sigemptyset(&mask); + + const struct sigaction sa = {.sa_handler = SIG_DFL}; + if (sigaction(SIGINT, &sa, NULL) < 0 || + sigaction(SIGTERM, &sa, NULL) < 0 || + sigaction(SIGCHLD, &sa, NULL) < 0 || + sigprocmask(SIG_SETMASK, &mask, NULL) < 0) + { + goto fail; + } + + /* New process group, so that we can use killpg() */ + setpgid(0, 0); + + /* Close pipe read ends */ + close(exec_pipe[0]); + close(comm_pipe[0]); + + /* Re-direct stdin/stdout */ + int dev_null = open("/dev/null", O_RDONLY); + if (dev_null < 0) + goto fail; + + if (dup2(dev_null, STDIN_FILENO) < 0 || + dup2(comm_pipe[1], STDOUT_FILENO) < 0) + { + goto fail; + } + + /* We're done with the redirection pipe */ + close(comm_pipe[1]); + comm_pipe[1] = -1; + + /* Close *all* other FDs */ + for (int i = STDERR_FILENO + 1; i < 65536; i++) { + if (i == exec_pipe[1]) { + /* Needed for error reporting. Automatically closed + * when execvp() succeeds */ + continue; + } + close(i); + } + + execvp(m->path, argv); + + fail: + (void)!write(exec_pipe[1], &errno, sizeof(errno)); + close(exec_pipe[1]); + if (comm_pipe[1] >= 0) + close(comm_pipe[1]); + _exit(errno); + } + + /* Close pipe write ends */ + close(exec_pipe[1]); + close(comm_pipe[1]); + + int _errno; + static_assert(sizeof(_errno) == sizeof(errno), "errno size mismatch"); + + /* Wait for errno from child, or FD being closed in execvp() */ + int r = read(exec_pipe[0], &_errno, sizeof(_errno)); + close(exec_pipe[0]); + + if (r < 0) { + LOG_ERRNO("failed to read from pipe"); + close(comm_pipe[0]); + return -1; + } + + if (r > 0) { + LOG_ERRNO_P("%s: failed to start", _errno, m->path); + close(comm_pipe[0]); + waitpid(pid, NULL, 0); + return -1; + } + + /* Pipe was closed. I.e. execvp() succeeded */ + assert(r == 0); + LOG_DBG("script running under PID=%u", pid); + + int ret = run_loop(mod, pid, comm_pipe[0]); + close(comm_pipe[0]); + + if (waitpid(pid, NULL, WNOHANG) == 0) { + static const struct { + int signo; + int timeout; + const char *name; + } sig_info[] = { + {SIGINT, 2, "SIGINT"}, + {SIGTERM, 5, "SIGTERM"}, + {SIGKILL, 0, "SIGKILL"}, + }; + + for (size_t i = 0; i < sizeof(sig_info) / sizeof(sig_info[0]); i++) { + struct timeval start; + gettimeofday(&start, NULL); + + const int signo = sig_info[i].signo; + const int timeout = sig_info[i].timeout; + const char *const name __attribute__((unused)) = sig_info[i].name; + + LOG_DBG("sending %s to PID=%u (timeout=%ds)", name, pid, timeout); + killpg(pid, signo); + + /* + * Child is unlikely to terminate *immediately*. Wait a + * *short* period of time before checking waitpid() the + * first time + */ + usleep(10000); + + pid_t waited_pid; + while ((waited_pid = waitpid( + pid, NULL, timeout > 0 ? WNOHANG : 0)) == 0) + { + struct timeval now; + gettimeofday(&now, NULL); + + struct timeval elapsed; + timersub(&now, &start, &elapsed); + + if (elapsed.tv_sec >= timeout) + break; + + /* Don't spinning */ + thrd_yield(); + usleep(100000); /* 100ms */ + } + + if (waited_pid == pid) { + /* Child finally dead */ + break; + } + } + } else + LOG_DBG("PID=%u already terminated", pid); + + return ret; +} + +static struct module * +script_new(const char *path, size_t argc, const char *const argv[static argc], + struct particle *_content) +{ + struct private *m = calloc(1, sizeof(*m)); + m->path = strdup(path); + m->content = _content; + m->argc = argc; + m->argv = malloc(argc * sizeof(m->argv[0])); + for (size_t i = 0; i < argc; i++) + m->argv[i] = strdup(argv[i]); + + struct module *mod = module_common_new(); + mod->private = m; + mod->run = &run; + mod->destroy = &destroy; + mod->content = &content; + return mod; +} + +static struct module * +from_conf(const struct yml_node *node, struct conf_inherit inherited) +{ + const struct yml_node *path = yml_get_value(node, "path"); + const struct yml_node *args = yml_get_value(node, "args"); + const struct yml_node *c = yml_get_value(node, "content"); + + size_t argc = args != NULL ? yml_list_length(args) : 0; + const char *argv[argc]; + + if (args != NULL) { + size_t i = 0; + for (struct yml_list_iter iter = yml_list_iter(args); + iter.node != NULL; + yml_list_next(&iter), i++) + { + argv[i] = yml_value_as_string(iter.node); + } + } + + return script_new( + yml_value_as_string(path), argc, argv, conf_to_particle(c, inherited)); +} + +static bool +conf_verify_path(keychain_t *chain, const struct yml_node *node) +{ + if (!conf_verify_string(chain, node)) + return false; + + const char *path = yml_value_as_string(node); + if (strlen(path) < 1 || path[0] != '/') { + LOG_ERR("%s: path must be absolute", conf_err_prefix(chain, node)); + return false; + } + + return true; +} + +static bool +conf_verify_args(keychain_t *chain, const struct yml_node *node) +{ + return conf_verify_list(chain, node, &conf_verify_string); +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static const struct attr_info attrs[] = { + {"path", true, &conf_verify_path}, + {"args", false, &conf_verify_args}, + MODULE_COMMON_ATTRS, + }; + + return conf_verify_dict(chain, node, attrs); +} + +const struct module_iface module_script_iface = { + .verify_conf = &verify_conf, + .from_conf = &from_conf, +}; + +#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) +extern const struct module_iface iface __attribute__((weak, alias("module_script_iface"))); +#endif diff --git a/modules/sway_xkb.c b/modules/sway-xkb.c similarity index 100% rename from modules/sway_xkb.c rename to modules/sway-xkb.c diff --git a/particle.c b/particle.c index be98e0e..ffe330f 100644 --- a/particle.c +++ b/particle.c @@ -219,14 +219,25 @@ exposable_default_on_mouse(struct exposable *exposable, struct bar *bar, LOG_DBG("executing on-click handler: %s", cmd); + sigset_t mask; + sigemptyset(&mask); + + const struct sigaction sa = {.sa_handler = SIG_DFL}; + if (sigaction(SIGINT, &sa, NULL) < 0 || + sigaction(SIGTERM, &sa, NULL) < 0 || + sigaction(SIGCHLD, &sa, NULL) < 0 || + sigprocmask(SIG_SETMASK, &mask, NULL) < 0) + { + goto fail; + } + /* Redirect stdin/stdout/stderr to /dev/null */ int dev_null_r = open("/dev/null", O_RDONLY | O_CLOEXEC); int dev_null_w = open("/dev/null", O_WRONLY | O_CLOEXEC); if (dev_null_r == -1 || dev_null_w == -1) { LOG_ERRNO("/dev/null: failed to open"); - (void)!write(pipe_fds[1], &errno, sizeof(errno)); - _exit(1); + goto fail; } if (dup2(dev_null_r, STDIN_FILENO) == -1 || @@ -234,15 +245,20 @@ exposable_default_on_mouse(struct exposable *exposable, struct bar *bar, dup2(dev_null_w, STDERR_FILENO) == -1) { LOG_ERRNO("failed to redirect stdin/stdout/stderr"); - (void)!write(pipe_fds[1], &errno, sizeof(errno)); - _exit(1); + goto fail; } + /* Close *all* other FDs (e.g. script modules' FDs) */ + for (int i = STDERR_FILENO + 1; i < 65536; i++) + close(i); + execvp(argv[0], argv); + fail: /* Signal failure to parent process */ (void)!write(pipe_fds[1], &errno, sizeof(errno)); - _exit(1); + close(pipe_fds[1]); + _exit(errno); break; default: diff --git a/particles/list.c b/particles/list.c index 720e8a5..6f51152 100644 --- a/particles/list.c +++ b/particles/list.c @@ -118,6 +118,7 @@ instantiate(const struct particle *particle, const struct tag_set *tags) for (size_t i = 0; i < p->count; i++) { const struct particle *pp = p->particles[i]; e->exposables[i] = pp->instantiate(pp, tags); + assert(e->exposables[i] != NULL); } char *on_click = tags_expand_template(particle->on_click_template, tags); diff --git a/particles/map.c b/particles/map.c index 2f5c460..57bb0fb 100644 --- a/particles/map.c +++ b/particles/map.c @@ -8,6 +8,7 @@ #include "../config-verify.h" #include "../particle.h" #include "../plugin.h" +#include "dynlist.h" struct particle_map { const char *tag_value; @@ -87,11 +88,12 @@ instantiate(const struct particle *particle, const struct tag_set *tags) { const struct private *p = particle->private; const struct tag *tag = tag_for_name(tags, p->tag); - assert(tag != NULL || p->default_particle != NULL); - - if (tag == NULL) - return p->default_particle->instantiate(p->default_particle, tags); + if (tag == NULL) { + return p->default_particle != NULL + ? p->default_particle->instantiate(p->default_particle, tags) + : dynlist_exposable_new(NULL, 0, 0, 0); + } const char *tag_value = tag->as_string(tag); struct particle *pp = NULL; @@ -106,13 +108,16 @@ instantiate(const struct particle *particle, const struct tag_set *tags) break; } - if (pp == NULL) { - assert(p->default_particle != NULL); - pp = p->default_particle; - } - struct eprivate *e = calloc(1, sizeof(*e)); - e->exposable = pp->instantiate(pp, tags); + + if (pp != NULL) + e->exposable = pp->instantiate(pp, tags); + else if (p->default_particle != NULL) + e->exposable = p->default_particle->instantiate(p->default_particle, tags); + else + e->exposable = dynlist_exposable_new(NULL, 0, 0, 0); + + assert(e->exposable != NULL); char *on_click = tags_expand_template(particle->on_click_template, tags); struct exposable *exposable = exposable_common_new(particle, on_click); diff --git a/particles/meson.build b/particles/meson.build index f571f12..6b31081 100644 --- a/particles/meson.build +++ b/particles/meson.build @@ -1,21 +1,5 @@ particle_sdk = declare_dependency(dependencies: [pixman, tllist, fcft]) -particles = [] -foreach particle : ['empty', 'list', 'map', 'progress-bar', 'ramp', 'string'] - if plugs_as_libs - shared_module('@0@'.format(particle), '@0@.c'.format(particle), - dependencies: particle_sdk, - name_prefix: 'particle_', - install: true, - install_dir: join_paths(get_option('libdir'), 'yambar')) - else - particles += [declare_dependency( - sources: '@0@.c'.format(particle), - dependencies: particle_sdk, - compile_args: '-DHAVE_PLUGIN_@0@'.format(particle.underscorify()))] - endif -endforeach - dynlist_lib = build_target( 'dynlist', 'dynlist.c', 'dynlist.h', dependencies: particle_sdk, target_type: plugs_as_libs ? 'shared_library' : 'static_library', @@ -25,3 +9,29 @@ dynlist_lib = build_target( ) dynlist = declare_dependency(link_with: dynlist_lib) + +# Particle name -> dep-list +deps = { + 'empty': [], + 'list': [], + 'map': [dynlist], + 'progress-bar': [], + 'ramp': [], + 'string': [], +} + +particles = [] +foreach particle, particle_deps : deps + if plugs_as_libs + shared_module('@0@'.format(particle), '@0@.c'.format(particle), + dependencies: [particle_sdk] + particle_deps, + name_prefix: 'particle_', + install: true, + install_dir: join_paths(get_option('libdir'), 'yambar')) + else + particles += [declare_dependency( + sources: '@0@.c'.format(particle), + dependencies: [particle_sdk] + particle_deps, + compile_args: '-DHAVE_PLUGIN_@0@'.format(particle.underscorify()))] + endif +endforeach diff --git a/particles/progress-bar.c b/particles/progress-bar.c index 5c16802..ad5c4cd 100644 --- a/particles/progress-bar.c +++ b/particles/progress-bar.c @@ -179,13 +179,13 @@ instantiate(const struct particle *particle, const struct tag_set *tags) { const struct private *p = particle->private; const struct tag *tag = tag_for_name(tags, p->tag); - assert(tag != NULL); - long value = tag->as_int(tag); - long min = tag->min(tag); - long max = tag->max(tag); + long value = tag != NULL ? tag->as_int(tag) : 0; + long min = tag != NULL ? tag->min(tag) : 0; + long max = tag != NULL ? tag->max(tag) : 0; - LOG_DBG("%s: value=%ld, min=%ld, max=%ld", tag->name(tag), value, min, max); + LOG_DBG("%s: value=%ld, min=%ld, max=%ld", + tag != NULL ? tag->name(tag) : "", value, min, max); long fill_count = max == min ? 0 : p->width * value / (max - min); long empty_count = p->width - fill_count; @@ -210,6 +210,8 @@ instantiate(const struct particle *particle, const struct tag_set *tags) epriv->exposables[idx++] = p->end_marker->instantiate(p->end_marker, tags); assert(idx == epriv->count); + for (size_t i = 0; i < epriv->count; i++) + assert(epriv->exposables[i] != NULL); char *on_click = tags_expand_template(particle->on_click_template, tags); @@ -222,6 +224,9 @@ instantiate(const struct particle *particle, const struct tag_set *tags) exposable->expose = &expose; exposable->on_mouse = &on_mouse; + if (tag == NULL) + return exposable; + enum tag_realtime_unit rt = tag->realtime(tag); if (rt == TAG_REALTIME_NONE) diff --git a/particles/ramp.c b/particles/ramp.c index b513681..45cc277 100644 --- a/particles/ramp.c +++ b/particles/ramp.c @@ -95,13 +95,12 @@ instantiate(const struct particle *particle, const struct tag_set *tags) { const struct private *p = particle->private; const struct tag *tag = tag_for_name(tags, p->tag); - assert(tag != NULL); assert(p->count > 0); - long value = tag->as_int(tag); - long min = tag->min(tag); - long max = tag->max(tag); + long value = tag != NULL ? tag->as_int(tag) : 0; + long min = tag != NULL ? tag->min(tag) : 0; + long max = tag != NULL ? tag->max(tag) : 0; assert(value >= min && value <= max); assert(max >= min); @@ -122,6 +121,7 @@ instantiate(const struct particle *particle, const struct tag_set *tags) struct eprivate *e = calloc(1, sizeof(*e)); e->exposable = pp->instantiate(pp, tags); + assert(e->exposable != NULL); char *on_click = tags_expand_template(particle->on_click_template, tags); struct exposable *exposable = exposable_common_new(particle, on_click); diff --git a/plugin.c b/plugin.c index 7ae2fb4..05a2c1a 100644 --- a/plugin.c +++ b/plugin.c @@ -43,6 +43,7 @@ EXTERN_MODULE(network); EXTERN_MODULE(removables); EXTERN_MODULE(river); EXTERN_MODULE(sway_xkb); +EXTERN_MODULE(script); EXTERN_MODULE(xkb); EXTERN_MODULE(xwindow); @@ -119,6 +120,7 @@ init(void) REGISTER_CORE_MODULE(river, river); #endif REGISTER_CORE_MODULE(sway-xkb, sway_xkb); + REGISTER_CORE_MODULE(script, script); #if defined(HAVE_PLUGIN_xkb) REGISTER_CORE_MODULE(xkb, xkb); #endif