Repository: shdown/luastatus Branch: master Commit: dda015e2f979 Files: 805 Total size: 1.2 MB Directory structure: gitextract_ja3_jqjt/ ├── .circleci/ │ ├── config.yml │ └── run-pt.sh ├── .gitignore ├── .luacheckrc ├── CMakeLists.txt ├── COPYING.LESSER.txt ├── COPYING.txt ├── DEPENDS.txt ├── DOCS/ │ ├── CUSTOM_DATA_SRC_WIDGET.md │ ├── MIGRATION_GUIDE.md │ ├── WRITING_BARLIB_OR_PLUGIN.md │ ├── c_notes/ │ │ ├── eintr-policy.md │ │ └── empty-ranges-and-c-stdlib.md │ └── design/ │ ├── locking-patterns.md │ └── map_get.md ├── README.md ├── barlibs/ │ ├── dwm/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── dwm.c │ ├── i3/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ ├── escape_json_str.c │ │ ├── escape_json_str.h │ │ ├── event_watcher.c │ │ ├── event_watcher.h │ │ ├── fuzz_esc_json/ │ │ │ ├── .gitignore │ │ │ ├── build.sh │ │ │ ├── clear.sh │ │ │ ├── fuzz.sh │ │ │ ├── gen_testcases.sh │ │ │ ├── harness.c │ │ │ └── testcases/ │ │ │ ├── testcase_000 │ │ │ ├── testcase_001 │ │ │ ├── testcase_002 │ │ │ ├── testcase_003 │ │ │ ├── testcase_004 │ │ │ ├── testcase_005 │ │ │ ├── testcase_006 │ │ │ ├── testcase_007 │ │ │ ├── testcase_008 │ │ │ └── testcase_009 │ │ ├── fuzz_esc_pango/ │ │ │ ├── .gitignore │ │ │ ├── build.sh │ │ │ ├── clear.sh │ │ │ ├── fuzz.sh │ │ │ ├── gen_testcases.sh │ │ │ ├── harness.c │ │ │ └── testcases/ │ │ │ ├── testcase_000 │ │ │ ├── testcase_001 │ │ │ ├── testcase_002 │ │ │ ├── testcase_003 │ │ │ ├── testcase_004 │ │ │ ├── testcase_005 │ │ │ ├── testcase_006 │ │ │ ├── testcase_007 │ │ │ ├── testcase_008 │ │ │ └── testcase_009 │ │ ├── i3.c │ │ ├── luastatus-i3-wrapper │ │ ├── pango_escape.c │ │ ├── pango_escape.h │ │ └── priv.h │ ├── lemonbar/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ ├── fuzz_esc/ │ │ │ ├── .gitignore │ │ │ ├── build.sh │ │ │ ├── clear.sh │ │ │ ├── fuzz.sh │ │ │ ├── gen_testcases.sh │ │ │ ├── harness.c │ │ │ └── testcases/ │ │ │ ├── testcase_000 │ │ │ ├── testcase_001 │ │ │ ├── testcase_002 │ │ │ ├── testcase_003 │ │ │ ├── testcase_004 │ │ │ ├── testcase_005 │ │ │ ├── testcase_006 │ │ │ ├── testcase_007 │ │ │ ├── testcase_008 │ │ │ └── testcase_009 │ │ ├── fuzz_sanitize/ │ │ │ ├── .gitignore │ │ │ ├── build.sh │ │ │ ├── clear.sh │ │ │ ├── fuzz.sh │ │ │ ├── gen_testcases.sh │ │ │ ├── harness.c │ │ │ └── testcases/ │ │ │ ├── X_testcase_000 │ │ │ ├── X_testcase_001 │ │ │ ├── X_testcase_002 │ │ │ ├── X_testcase_003 │ │ │ ├── X_testcase_004 │ │ │ ├── X_testcase_005 │ │ │ ├── X_testcase_fps │ │ │ ├── Y_testcase_000 │ │ │ ├── Y_testcase_001 │ │ │ ├── Y_testcase_002 │ │ │ ├── Y_testcase_003 │ │ │ ├── Z_testcase_000 │ │ │ ├── Z_testcase_001 │ │ │ ├── Z_testcase_002 │ │ │ ├── Z_testcase_003 │ │ │ └── Z_testcase_004 │ │ ├── lemonbar.c │ │ ├── luastatus-lemonbar-launcher │ │ ├── markup_utils.c │ │ └── markup_utils.h │ └── stdout/ │ ├── CMakeLists.txt │ ├── README.rst │ ├── fuzz/ │ │ ├── .gitignore │ │ ├── build.sh │ │ ├── clear.sh │ │ ├── fuzz.sh │ │ ├── gen_testcases.sh │ │ ├── harness.c │ │ └── testcases/ │ │ ├── testcase_000 │ │ ├── testcase_001 │ │ ├── testcase_002 │ │ ├── testcase_003 │ │ ├── testcase_004 │ │ ├── testcase_005 │ │ ├── testcase_006 │ │ ├── testcase_007 │ │ ├── testcase_008 │ │ └── testcase_009 │ ├── luastatus-dvtm │ ├── luastatus-stdout-wrapper │ ├── open_stdio_file.c │ ├── open_stdio_file.h │ ├── sanitize.c │ ├── sanitize.h │ └── stdout.c ├── check_final_newline.py ├── check_includes.sh ├── check_style.sh ├── contrib/ │ ├── luastatus-9999.ebuild │ └── luastatus.spec ├── debian/ │ ├── changelog │ ├── compat │ ├── control │ ├── copyright │ └── rules ├── examples/ │ ├── dwm/ │ │ ├── alsa-gauge.lua │ │ ├── alsa.lua │ │ ├── backlight.lua │ │ ├── battery.lua │ │ ├── bluetooth.lua │ │ ├── btc-price.lua │ │ ├── cpu-freq.lua │ │ ├── cpu-temperature.lua │ │ ├── cpu-usage.lua │ │ ├── disk-io.lua │ │ ├── file-contents.lua │ │ ├── fs.lua │ │ ├── gmail.lua │ │ ├── ip.lua │ │ ├── loadavg-linux.lua │ │ ├── media-player-mpris.lua │ │ ├── mem-usage.lua │ │ ├── mpd.lua │ │ ├── network-rate.lua │ │ ├── pulse-gauge.lua │ │ ├── pulse.lua │ │ ├── systemd-unit.lua │ │ ├── time-battery-combined.lua │ │ ├── time-date.lua │ │ ├── tor.lua │ │ ├── uptime-linux.lua │ │ ├── weather-rus.lua │ │ ├── weather.lua │ │ ├── wireless.lua │ │ └── xkb.lua │ ├── i3/ │ │ ├── alsa-gauge.lua │ │ ├── alsa-interactive-gauge.lua │ │ ├── alsa.lua │ │ ├── backlight.lua │ │ ├── battery.lua │ │ ├── bluetooth.lua │ │ ├── btc-price.lua │ │ ├── cpu-freq.lua │ │ ├── cpu-temperature.lua │ │ ├── cpu-usage.lua │ │ ├── disk-io.lua │ │ ├── file-contents.lua │ │ ├── fs.lua │ │ ├── gmail.lua │ │ ├── ip.lua │ │ ├── loadavg-linux.lua │ │ ├── media-player-mpris.lua │ │ ├── mem-usage.lua │ │ ├── mpd-progressbar.lua │ │ ├── network-rate.lua │ │ ├── pulse-gauge.lua │ │ ├── pulse-interactive-gauge.lua │ │ ├── pulse.lua │ │ ├── systemd-unit.lua │ │ ├── time-battery-combined.lua │ │ ├── time-date.lua │ │ ├── tor.lua │ │ ├── update-on-click.lua │ │ ├── uptime-linux.lua │ │ ├── weather-rus.lua │ │ ├── weather.lua │ │ ├── wireless.lua │ │ └── xkb.lua │ └── lemonbar/ │ ├── alsa-gauge.lua │ ├── alsa.lua │ ├── backlight.lua │ ├── battery.lua │ ├── bluetooth.lua │ ├── btc-price.lua │ ├── cpu-freq.lua │ ├── cpu-temperature.lua │ ├── cpu-usage.lua │ ├── disk-io.lua │ ├── file-contents.lua │ ├── fs.lua │ ├── gmail.lua │ ├── ip.lua │ ├── loadavg-linux.lua │ ├── media-player-mpris.lua │ ├── mem-usage.lua │ ├── mpd-progressbar.lua │ ├── network-rate.lua │ ├── pulse-gauge.lua │ ├── pulse.lua │ ├── systemd-unit.lua │ ├── time-battery-combined.lua │ ├── time-date.lua │ ├── tor.lua │ ├── uptime-linux.lua │ ├── weather-rus.lua │ ├── weather.lua │ ├── wireless.lua │ └── xkb.lua ├── fuzz_utils/ │ ├── do_fuzz_everything.sh │ ├── find_interesting.sh │ ├── fuzz_utils.h │ └── gen_testcases/ │ ├── .gitignore │ ├── __init__.py │ ├── gen_testcases.py │ ├── mod_generator.py │ ├── mod_mutator.py │ └── mod_mutators.py ├── generate-man.sh ├── include/ │ ├── barlib_data.h │ ├── barlib_data_v1.h │ ├── barlib_v1.h │ ├── common.h │ ├── plugin_data.h │ ├── plugin_data_v1.h │ ├── plugin_v1.h │ └── sayf_macros.h ├── libhackyfix/ │ ├── CMakeLists.txt │ ├── README.txt │ ├── fatal.c │ ├── fatal.h │ ├── get_rtld_next_handle.c │ ├── get_rtld_next_handle.h │ └── libhackyfix.c ├── libls/ │ ├── CMakeLists.txt │ ├── ls_algo.h │ ├── ls_alloc_utils.c │ ├── ls_alloc_utils.h │ ├── ls_compdep.h │ ├── ls_cstring_utils.c │ ├── ls_cstring_utils.h │ ├── ls_evloop_lfuncs.h │ ├── ls_fifo_device.h │ ├── ls_freemem.h │ ├── ls_getenv_r.c │ ├── ls_getenv_r.h │ ├── ls_io_utils.c │ ├── ls_io_utils.h │ ├── ls_lua_compat.h │ ├── ls_osdep.c │ ├── ls_osdep.h │ ├── ls_panic.c │ ├── ls_panic.h │ ├── ls_parse_int.c │ ├── ls_parse_int.h │ ├── ls_probes.in.h │ ├── ls_strarr.h │ ├── ls_string.c │ ├── ls_string.h │ ├── ls_time_utils.h │ ├── ls_tls_ebuf.c │ ├── ls_tls_ebuf.h │ ├── ls_xallocf.c │ └── ls_xallocf.h ├── libmoonvisit/ │ ├── CMakeLists.txt │ ├── README.txt │ ├── moonvisit.c │ └── moonvisit.h ├── libprocalive/ │ ├── CMakeLists.txt │ ├── procalive_lfuncs.c │ └── procalive_lfuncs.h ├── librunshell/ │ ├── CMakeLists.txt │ ├── README.txt │ ├── runshell.c │ └── runshell.h ├── libsafe/ │ ├── CMakeLists.txt │ ├── mut_safev.h │ ├── safe_common.c │ ├── safe_common.h │ └── safev.h ├── libwidechar/ │ ├── CMakeLists.txt │ ├── README.txt │ ├── config.in.h │ ├── libwidechar.c │ ├── libwidechar.h │ ├── libwidechar_compdep.h │ └── libwidechar_xspan.h ├── plugins/ │ ├── alsa/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── alsa.c │ ├── backlight-linux/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── backlight-linux.lua │ ├── battery-linux/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── battery-linux.lua │ ├── cpu-freq-linux/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── cpu-freq-linux.lua │ ├── cpu-usage-linux/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── cpu-usage-linux.lua │ ├── dbus/ │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ ├── README_FN_CALL_METHOD.rst │ │ ├── README_FN_MKVAL.rst │ │ ├── README_FN_PROP.rst │ │ ├── bustype2idx.h │ │ ├── cstringify.sh │ │ ├── cvt.c │ │ ├── cvt.h │ │ ├── dbus.c │ │ ├── load_lualib.c │ │ ├── load_lualib.h │ │ ├── lualib.lua │ │ └── zoo/ │ │ ├── README.txt │ │ ├── zoo.c │ │ ├── zoo.h │ │ ├── zoo_call_params.c │ │ ├── zoo_call_params.h │ │ ├── zoo_call_prot.c │ │ ├── zoo_call_prot.h │ │ ├── zoo_checkudata.c │ │ ├── zoo_checkudata.h │ │ ├── zoo_mt.c │ │ ├── zoo_mt.h │ │ ├── zoo_uncvt_type.c │ │ ├── zoo_uncvt_type.h │ │ ├── zoo_uncvt_val.c │ │ └── zoo_uncvt_val.h │ ├── disk-io-linux/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── disk-io-linux.lua │ ├── file-contents-linux/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── file-contents-linux.lua │ ├── fs/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ ├── fs.c │ │ ├── strlist.c │ │ └── strlist.h │ ├── imap/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── imap.lua │ ├── inotify/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ ├── inotify.c │ │ ├── inotify_compat.h │ │ └── probes.in.h │ ├── is-program-running/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── is-program-running.lua │ ├── mem-usage-linux/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── mem-usage-linux.lua │ ├── mpd/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ ├── connect.c │ │ ├── connect.h │ │ ├── fuzz/ │ │ │ ├── .gitignore │ │ │ ├── build.sh │ │ │ ├── clear.sh │ │ │ ├── fuzz.sh │ │ │ ├── fuzz_all.sh │ │ │ ├── gen_testcases.sh │ │ │ ├── harness │ │ │ ├── harness.c │ │ │ ├── testcases_append_kv/ │ │ │ │ ├── testcase_000 │ │ │ │ ├── testcase_001 │ │ │ │ ├── testcase_002 │ │ │ │ ├── testcase_003 │ │ │ │ ├── testcase_004 │ │ │ │ ├── testcase_005 │ │ │ │ ├── testcase_006 │ │ │ │ ├── testcase_007 │ │ │ │ ├── testcase_008 │ │ │ │ └── testcase_009 │ │ │ ├── testcases_check_greeting/ │ │ │ │ ├── testcase_000 │ │ │ │ ├── testcase_001 │ │ │ │ ├── testcase_002 │ │ │ │ ├── testcase_003 │ │ │ │ ├── testcase_004 │ │ │ │ ├── testcase_005 │ │ │ │ ├── testcase_006 │ │ │ │ ├── testcase_007 │ │ │ │ ├── testcase_008 │ │ │ │ ├── testcase_009 │ │ │ │ └── testcase_pfx_only │ │ │ ├── testcases_get_resp_type/ │ │ │ │ ├── ack_testcase_000 │ │ │ │ ├── ack_testcase_001 │ │ │ │ ├── ack_testcase_002 │ │ │ │ ├── ack_testcase_003 │ │ │ │ ├── ack_testcase_004 │ │ │ │ ├── ok_testcase_000 │ │ │ │ ├── ok_testcase_001 │ │ │ │ ├── ok_testcase_002 │ │ │ │ ├── ok_testcase_003 │ │ │ │ ├── ok_testcase_004 │ │ │ │ ├── ok_testcase_pfx_only │ │ │ │ ├── other_testcase_000 │ │ │ │ ├── other_testcase_001 │ │ │ │ ├── other_testcase_002 │ │ │ │ ├── other_testcase_003 │ │ │ │ └── other_testcase_004 │ │ │ └── testcases_write_quoted/ │ │ │ ├── testcase_000 │ │ │ ├── testcase_001 │ │ │ ├── testcase_002 │ │ │ ├── testcase_003 │ │ │ ├── testcase_004 │ │ │ ├── testcase_005 │ │ │ ├── testcase_006 │ │ │ ├── testcase_007 │ │ │ ├── testcase_008 │ │ │ └── testcase_009 │ │ ├── line_reader.c │ │ ├── line_reader.h │ │ ├── mpd.c │ │ ├── safe_haven.c │ │ └── safe_haven.h │ ├── mpris/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── mpris.lua │ ├── multiplex/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ ├── config.in.h │ │ ├── conq.c │ │ ├── conq.h │ │ ├── external_context.h │ │ ├── map_ref.c │ │ ├── map_ref.h │ │ ├── multiplex.c │ │ ├── priv.h │ │ ├── runner.c │ │ ├── runner.h │ │ ├── runtime_data.c │ │ ├── runtime_data.h │ │ ├── wspec_list.c │ │ └── wspec_list.h │ ├── network-linux/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ ├── ethernet_info.c │ │ ├── ethernet_info.h │ │ ├── iface_type.c │ │ ├── iface_type.h │ │ ├── network.c │ │ ├── string_set.c │ │ ├── string_set.h │ │ ├── wireless_info.c │ │ └── wireless_info.h │ ├── network-rate-linux/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── network-rate-linux.lua │ ├── pipe/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── pipe.lua │ ├── pipev2/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ ├── launch.c │ │ ├── launch.h │ │ ├── pipev2.c │ │ ├── sigdb.c │ │ ├── sigdb.h │ │ ├── utils.c │ │ └── utils.h │ ├── pulse/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── pulse.c │ ├── systemd-unit/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── systemd-unit.lua │ ├── temperature-linux/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── temperature-linux.lua │ ├── timer/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── timer.c │ ├── udev/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ └── udev.c │ ├── unixsock/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ ├── cloexec_accept.c │ │ ├── cloexec_accept.h │ │ ├── probes.in.h │ │ ├── server.c │ │ ├── server.h │ │ └── unixsock.c │ ├── web/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ ├── compat_lua_resume.c │ │ ├── compat_lua_resume.h │ │ ├── fuzz_esc_json/ │ │ │ ├── .gitignore │ │ │ ├── build.sh │ │ │ ├── clear.sh │ │ │ ├── fuzz.sh │ │ │ ├── gen_testcases.sh │ │ │ ├── harness.c │ │ │ └── testcases/ │ │ │ ├── testcase_000 │ │ │ ├── testcase_001 │ │ │ ├── testcase_002 │ │ │ ├── testcase_003 │ │ │ ├── testcase_004 │ │ │ ├── testcase_005 │ │ │ ├── testcase_006 │ │ │ ├── testcase_007 │ │ │ ├── testcase_008 │ │ │ └── testcase_009 │ │ ├── fuzz_urldecode/ │ │ │ ├── .gitignore │ │ │ ├── build.sh │ │ │ ├── fuzz.sh │ │ │ ├── gen_testcases.sh │ │ │ ├── harness.c │ │ │ └── testcases/ │ │ │ ├── testcase_000 │ │ │ ├── testcase_001 │ │ │ ├── testcase_002 │ │ │ ├── testcase_003 │ │ │ ├── testcase_004 │ │ │ ├── testcase_005 │ │ │ ├── testcase_006 │ │ │ ├── testcase_007 │ │ │ ├── testcase_008 │ │ │ ├── testcase_009 │ │ │ ├── testcase_extra1 │ │ │ ├── testcase_extra2 │ │ │ └── testcase_extra3 │ │ ├── fuzz_urlencode/ │ │ │ ├── .gitignore │ │ │ ├── build.sh │ │ │ ├── fuzz.sh │ │ │ ├── fuzz_all.sh │ │ │ ├── gen_testcases.sh │ │ │ ├── harness.c │ │ │ └── testcases/ │ │ │ ├── testcase_000 │ │ │ ├── testcase_001 │ │ │ ├── testcase_002 │ │ │ ├── testcase_003 │ │ │ ├── testcase_004 │ │ │ ├── testcase_005 │ │ │ ├── testcase_006 │ │ │ ├── testcase_007 │ │ │ ├── testcase_008 │ │ │ └── testcase_009 │ │ ├── get_min_curl_ver.sh │ │ ├── json_config.in.h │ │ ├── make_request.c │ │ ├── make_request.h │ │ ├── mod_json/ │ │ │ ├── json_decode.c │ │ │ ├── json_decode.h │ │ │ ├── json_encode_num.c │ │ │ ├── json_encode_num.h │ │ │ ├── json_encode_str.c │ │ │ ├── json_encode_str.h │ │ │ ├── mod_json.c │ │ │ └── mod_json.h │ │ ├── mod_urlencode/ │ │ │ ├── mod_urlencode.c │ │ │ ├── mod_urlencode.h │ │ │ ├── urldecode.c │ │ │ ├── urldecode.h │ │ │ ├── urlencode.c │ │ │ └── urlencode.h │ │ ├── next_request_params.h │ │ ├── opts.c │ │ ├── opts.h │ │ ├── parse_opts.c │ │ ├── parse_opts.h │ │ ├── set_error.c │ │ ├── set_error.h │ │ └── web.c │ ├── xkb/ │ │ ├── CMakeLists.txt │ │ ├── README.rst │ │ ├── somehow.c │ │ ├── somehow.h │ │ ├── wrongly.c │ │ ├── wrongly.h │ │ └── xkb.c │ └── xtitle/ │ ├── CMakeLists.txt │ ├── README.rst │ └── xtitle.c ├── release.sh ├── run_literally_all_tests.sh ├── run_luacheck.sh └── tests/ ├── .gitignore ├── CMakeLists.txt ├── README.txt ├── barlib-runners/ │ └── runner-redirect-34 ├── dlopen.supp ├── glib.supp ├── httpserv/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── argparser.c │ ├── argparser.h │ ├── buffer.c │ ├── buffer.h │ ├── common.c │ ├── common.h │ ├── main.c │ ├── server.c │ ├── server.h │ ├── sleep_millis.c │ └── sleep_millis.h ├── kcov_wrapper.c ├── listnets.c ├── minstd.h ├── mock_barlib.c ├── mock_plugin.c ├── optional/ │ └── dbus_srv.py ├── parrot.c ├── pt.sh ├── pt_dbus_daemon.lib.bash ├── pt_pulseaudio_daemon.lib.bash ├── pt_stopwatch.lib.bash ├── pt_tests/ │ ├── _misc/ │ │ └── 01-simul.lib.bash │ ├── add_test.sh │ ├── barlib-i3/ │ │ ├── 00-common.lib.bash │ │ ├── 01-output.lib.bash │ │ ├── 02-input.lib.bash │ │ └── 03-lfunc-pango-escape.lib.bash │ ├── barlib-lemonbar/ │ │ ├── 00-common.lib.bash │ │ ├── 01-output.lib.bash │ │ ├── 02-input.lib.bash │ │ └── 03-lfunc-pango-escape.lib.bash │ ├── barlib-stdout/ │ │ ├── 00-common.lib.bash │ │ ├── 01-output.lib.bash │ │ ├── 03-input-filename.lib.bash │ │ └── 04-input-fd.lib.bash │ ├── luastatus/ │ │ ├── 00-common.lib.bash │ │ ├── 01-misc.lib.bash │ │ ├── 02-getenv.lib.bash │ │ ├── 03-exit.lib.bash │ │ ├── 04-setlocale.lib.bash │ │ ├── 05-libwidechar.lib.bash │ │ └── 06-execute.lib.bash │ ├── plugin-battery-linux/ │ │ ├── 00-common.lib.bash │ │ ├── 01-full-status-full.lib.bash │ │ ├── 02-full-status-not-charging.lib.bash │ │ ├── 03-dischaging.lib.bash │ │ ├── 04-charging.lib.bash │ │ └── 05-nofile.lib.bash │ ├── plugin-cpu-freq-linux/ │ │ ├── 00-common.lib.bash │ │ ├── 01-simple.lib.bash │ │ └── 02-please-reload.lib.bash │ ├── plugin-cpu-usage-linux/ │ │ ├── 00-common.lib.bash │ │ ├── 01-total.lib.bash │ │ ├── 02-cpu1.lib.bash │ │ ├── 03-cpu2.lib.bash │ │ ├── 04-cpu3.lib.bash │ │ └── 05-cpu4.lib.bash │ ├── plugin-dbus/ │ │ ├── 00-common.lib.bash │ │ ├── 01-simple.lib.bash │ │ ├── 02-timeout.lib.bash │ │ ├── 03-greet.lib.bash │ │ ├── 04-get-prop.lib.bash │ │ ├── 05-get-all-prop.lib.bash │ │ ├── 06-OPTIONAL-getset-prop.lib.bash │ │ ├── 07-OPTIONAL-getall-prop.lib.bash │ │ ├── 08-OPTIONAL-call-method-str.lib.bash │ │ ├── 09-OPTIONAL-call-method-DTLL.lib.bash │ │ ├── 10-OPTIONAL-call-method-arr.lib.bash │ │ ├── 11-OPTIONAL-call-method-dict.lib.bash │ │ ├── 12-OPTIONAL-call-method-tuple.lib.bash │ │ ├── 13-OPTIONAL-call-method-misc.lib.bash │ │ ├── 14-OPTIONAL-call-method-handle.lib.bash │ │ ├── 15-report-when-ready.lib.bash │ │ └── skip-me-if.lib.bash │ ├── plugin-disk-io-linux/ │ │ ├── 00-common.lib.bash │ │ └── 01-simple.lib.bash │ ├── plugin-file-contents-linux/ │ │ ├── 00-common.lib.bash │ │ └── 01-simple.lib.bash │ ├── plugin-fs/ │ │ ├── 00-common.lib.bash │ │ ├── 01-default.lib.bash │ │ ├── 02-root.lib.bash │ │ ├── 03-glob-yesmatch.lib.bash │ │ ├── 04-glob-nomatch.lib.bash │ │ ├── 05-bad-path.lib.bash │ │ ├── 06-wakeup-fifo.lib.bash │ │ └── 07-dyn-paths.lib.bash │ ├── plugin-imap/ │ │ ├── 00-common.lib.bash │ │ ├── 01-simple.lib.bash │ │ ├── 02-retry-timeout.lib.bash │ │ └── 03-idle-timeout.lib.bash │ ├── plugin-inotify/ │ │ ├── 00-common.lib.bash │ │ ├── 01-simple.lib.bash │ │ ├── 02-lfunc-get-initial-wds.lib.bash │ │ ├── 03-lfunc-add-watch.lib.bash │ │ ├── 04-lfunuc-rm-watch.lib.bash │ │ ├── 05-timeout.lib.bash │ │ ├── 06-dir.lib.bash │ │ ├── 07-lfunc-push-timeout.lib.bash │ │ ├── 08-lfunc-get-supported-events.lib.bash │ │ └── 09-access.lib.bash │ ├── plugin-is-program-running/ │ │ ├── 00-common.lib.bash │ │ ├── 01-pidfile-devnull.lib.bash │ │ ├── 02-pidfile.lib.bash │ │ ├── 03-pidfile-fileerr.lib.bash │ │ ├── 04-fileexists.lib.bash │ │ ├── 05-dirnonempty-root.lib.bash │ │ ├── 06-dirnonempty-normal-files.lib.bash │ │ ├── 07-dirnonempty-nonexisting.lib.bash │ │ └── 08-dirnonempty-with-hidden.lib.bash │ ├── plugin-mem-usage-linux/ │ │ ├── 00-common.lib.bash │ │ └── 01-simple.lib.bash │ ├── plugin-mpd/ │ │ ├── 00-common.lib.bash │ │ ├── 01-bad-answer.lib.bash │ │ ├── 02-simple-template.lib.bash │ │ ├── 03-simple.lib.bash │ │ ├── 04-simple-bind.lib.bash │ │ ├── 05-simple-bind-invalid.lib.bash │ │ ├── 06-simple-bind-invalid-ipv6.lib.bash │ │ ├── 07-keepalive.lib.bash │ │ ├── 08-retry-in.lib.bash │ │ ├── 09-retry-fifo.lib.bash │ │ ├── 10-timeout.lib.bash │ │ ├── 11-password-and-events.lib.bash │ │ ├── 12-connect-via-unix-socket.lib.bash │ │ ├── 13-bad-password.lib.bash │ │ └── 14-bad-hostname.lib.bash │ ├── plugin-multiplex/ │ │ ├── 00-common.lib.bash │ │ ├── 01-simple.lib.bash │ │ ├── 02-two-data-sources.lib.bash │ │ └── 03-call-event.lib.bash │ ├── plugin-network-linux/ │ │ ├── 00-common.lib.bash │ │ ├── 01-simple.lib.bash │ │ └── 02-simple-newfmt.lib.bash │ ├── plugin-network-rate-linux/ │ │ ├── 00-common.lib.bash │ │ ├── 01-simple.lib.bash │ │ ├── 02-in-array-form.lib.bash │ │ └── 03-iface-filter.lib.bash │ ├── plugin-pipe/ │ │ ├── 00-common.lib.bash │ │ ├── 01-simple.lib.bash │ │ └── 02-func-shell-escape.lib.bash │ ├── plugin-pipev2/ │ │ ├── 00-common.lib.bash │ │ ├── 01-simple.lib.bash │ │ ├── 02-greet.lib.bash │ │ ├── 03-bye.lib.bash │ │ ├── 04-write-to-stdin.lib.bash │ │ ├── 05-kill-default.lib.bash │ │ ├── 06-kill-by-spelling.lib.bash │ │ ├── 07-kill-by-signo.lib.bash │ │ └── 08-sigrt-bounds.lib.bash │ ├── plugin-pulse/ │ │ ├── 00-common.lib.bash │ │ ├── 01-simple.lib.bash │ │ └── 02-lfuncs.lib.bash │ ├── plugin-temperature-linux/ │ │ ├── 00-common.lib.bash │ │ ├── 01-common-carcass.lib.bash │ │ ├── 02-simple.lib.bash │ │ ├── 03-filter-by-kind.lib.bash │ │ └── 04-filter-by-name.lib.bash │ ├── plugin-timer/ │ │ ├── 00-common.lib.bash │ │ ├── 01-default.lib.bash │ │ ├── 02-period.lib.bash │ │ ├── 03-wakeup-fifo.lib.bash │ │ ├── 04-lfuncs.lib.bash │ │ ├── 05-procalive-lfuncs.lib.bash │ │ └── 06-self-pipe.lib.bash │ ├── plugin-unixsock/ │ │ ├── 00-common.lib.bash │ │ ├── 01-simple.lib.bash │ │ ├── 02-greet.lib.bash │ │ ├── 03-do-not-try-unlink-failure.lib.bash │ │ ├── 04-do-try-unlink-success.lib.bash │ │ ├── 05-timeout.lib.bash │ │ └── 06-lfuncs.lib.bash │ └── plugin-web/ │ ├── 00-common.lib.bash │ ├── 01-simple.lib.bash │ ├── 02-post.lib.bash │ ├── 03-call-cb.lib.bash │ ├── 04-with-headers.lib.bash │ ├── 05-json-encode.lib.bash │ ├── 06-urlencode.lib.bash │ ├── 07-urldecode.lib.bash │ ├── 08-json-decode.lib.bash │ ├── 09-json-decode-null.lib.bash │ ├── 10-error.lib.bash │ ├── 11-timeout.lib.bash │ ├── 12-not-avail.lib.bash │ ├── 13-time-now.lib.bash │ └── 14-get-supported-opts.lib.bash ├── pulsetalker.sh ├── stopwatch.c ├── torture.sh └── utils.lib.bash ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ version: 2.1 workflows: nanodesu: jobs: - subetedesu: matrix: parameters: luaver: ["lua5.1/liblua5.1-dev", "luajit/libluajit-5.1-dev", "lua5.2/liblua5.2-dev", "lua5.3/liblua5.3-dev"] jobs: subetedesu: docker: - image: cimg/base:2021.01-20.04 parameters: luaver: type: string steps: - checkout - run: ./check_style.sh - run: sudo apt-get update - run: v=<< parameters.luaver >>; sudo apt-get install -y ${v#*/} lua-socket lua-sec python3-docutils python3-dbus cmake valgrind pulseaudio pulseaudio-utils dbus dbus-x11 jq libxcb1-dev libyajl-dev libasound2-dev libglib2.0-dev libpulse-dev libudev-dev libnl-3-dev libnl-genl-3-dev libx11-dev libxcb1-dev libxcb-ewmh-dev libxcb-icccm4-dev libxcb-util0-dev libwebsockets-dev libcurl4-gnutls-dev libcjson-dev - run: v=<< parameters.luaver >>; cmake -DWITH_LUA_LIBRARY=${v%/*} -DBUILD_PLUGIN_PULSE=ON -DBUILD_PLUGIN_UNIXSOCK=ON -DBUILD_PLUGIN_WEB=ON -DBUILD_TESTS=ON . - run: make -j - run: ./tests/torture.sh . - run: v=<< parameters.luaver >>; LUAVER=$v PLUGIN_DBUS_OPTIONAL=1 ./.circleci/run-pt.sh ================================================ FILE: .circleci/run-pt.sh ================================================ #!/usr/bin/env bash set -e atexit=() trap ' # We need to run finalizers in reversed order. for (( i = ${#atexit[@]} - 1; i >= 0; --i )); do func=${atexit[$i]} $func done ' EXIT pa_start() { atexit+=(pa_end) pulseaudio --daemonize=no --disallow-exit=yes --exit-idle-time=-1 & while ! pactl info; do echo >&2 "Waiting for PulseAudio daemon..." sleep 1 done } pa_end() { pulseaudio --kill || true } dbus_start() { DBUS_DAEMON_PID= atexit+=(dbus_end) local addr_file=/tmp/dbus-session-addr.txt true > "$addr_file" || return $? dbus-daemon --session --nofork --print-address=3 3>"$addr_file" & DBUS_DAEMON_PID=$! local x while ! IFS= read -r x < "$addr_file"; do echo >&2 "Waiting for D-Bus daemon..." sleep 1 || return $? done rm -f "$addr_file" || return $? export DBUS_SESSION_BUS_ADDRESS="$x" } dbus_end() { if [[ -n "$DBUS_DAEMON_PID" ]]; then kill "$DBUS_DAEMON_PID" || true fi } dbus_start sleep 1 pa_start #PT_TOOL=valgrind PT_MAX_LAG=250 ./tests/pt.sh . PT_MAX_LAG=300 ./tests/pt.sh . ================================================ FILE: .gitignore ================================================ *.o *.so /_gitignored/ # CMake CMakeCache.txt CMakeFiles CMakeScripts Makefile cmake_install.cmake install_manifest.txt CTestTestfile.cmake build/ # generated headers *.generated.[ch] # generated man pages luastatus*.[1-8] ================================================ FILE: .luacheckrc ================================================ new_globals = {'widget'} new_read_globals = {'luastatus'} ignore = { '631', -- "Line is too long" } ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required (VERSION 3.1.3...3.10) project (luastatus C) if (NOT CMAKE_BUILD_TYPE) set (CMAKE_BUILD_TYPE Release) endif () set (CMAKE_C_STANDARD 99) set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra") #------------------------------------------------------------------------------ option (WITH_UBSAN "build with UBSAN (undefined behavior sanitizer)" OFF) if (${WITH_UBSAN}) set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=undefined -fsanitize-trap=all -fno-sanitize-recover=all") if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=local-bounds") endif () endif () option (WITH_ASAN "build with ASAN (address sanitizer)" OFF) if (${WITH_ASAN}) set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fsanitize-address-use-after-scope -fsanitize-trap=all -fno-sanitize-recover=all") endif () option (WITH_TSAN "build with TSAN (thread sanitizer)" OFF) if (${WITH_TSAN}) set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=thread -fsanitize-trap=all -fno-sanitize-recover=all") endif () option (WITH_LSAN "build with LSAN (memory leak sanitizer)" OFF) if (${WITH_LSAN}) set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=leak -fsanitize-trap=all -fno-sanitize-recover=all") endif () #------------------------------------------------------------------------------ find_package (PkgConfig REQUIRED) set (WITH_LUA_LIBRARY "-" CACHE STRING "Lua library name") if (NOT "${WITH_LUA_LIBRARY}" STREQUAL "-") pkg_search_module (LUA REQUIRED "${WITH_LUA_LIBRARY}") else () pkg_search_module (LUA REQUIRED lua54 lua-5.4 lua5.4 lua53 lua-5.3 lua5.3 lua52 lua-5.2 lua5.2 lua51 lua-5.1 lua5.1 luajit lua) endif () function (luastatus_target_compile_with target var) target_include_directories (${target} SYSTEM PUBLIC ${${var}_INCLUDE_DIRS}) target_compile_options (${target} PUBLIC ${${var}_CFLAGS_OTHER}) endfunction () function (luastatus_target_build_with target var) luastatus_target_compile_with ("${target}" "${var}") target_link_libraries (${target} PUBLIC ${${var}_LIBRARIES}) endfunction () #------------------------------------------------------------------------------ include (GNUInstallDirs) set (BARLIBS_DIR "${CMAKE_INSTALL_FULL_LIBDIR}/luastatus/barlibs") set (PLUGINS_DIR "${CMAKE_INSTALL_FULL_LIBDIR}/luastatus/plugins") set (LUA_PLUGINS_DIR "${CMAKE_INSTALL_FULL_DATAROOTDIR}/luastatus/plugins") function (luastatus_add_barlib_or_plugin destdir name) set (sources ${ARGV}) list (REMOVE_AT sources 0 1) add_library ("${name}" MODULE ${sources}) set_target_properties ("${name}" PROPERTIES PREFIX "") if (NOT "${destdir}" STREQUAL "-") install (TARGETS "${name}" DESTINATION "${destdir}") endif () endfunction () function (luastatus_add_barlib) luastatus_add_barlib_or_plugin ("${BARLIBS_DIR}" ${ARGV}) endfunction () function (luastatus_add_plugin) luastatus_add_barlib_or_plugin ("${PLUGINS_DIR}" ${ARGV}) endfunction () function (luastatus_add_barlib_noinstall) luastatus_add_barlib_or_plugin ("-" ${ARGV}) endfunction () function (luastatus_add_plugin_noinstall) luastatus_add_barlib_or_plugin ("-" ${ARGV}) endfunction () option (BUILD_DOCS "build man pages" ON) function (luastatus_add_man_page src basename section) if (NOT BUILD_DOCS) return () endif () set (dest "${basename}.${section}") add_custom_command ( OUTPUT "${dest}" COMMAND "${PROJECT_SOURCE_DIR}/generate-man.sh" ARGS "${CMAKE_CURRENT_SOURCE_DIR}/${src}" "${dest}" WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" MAIN_DEPENDENCY "${src}" VERBATIM) add_custom_target ("man${section}-${basename}" ALL DEPENDS "${dest}") install ( FILES "${CMAKE_CURRENT_BINARY_DIR}/${dest}" DESTINATION "${CMAKE_INSTALL_MANDIR}/man${section}") endfunction () #------------------------------------------------------------------------------ add_subdirectory (libls) add_subdirectory (libsafe) add_subdirectory (libhackyfix) add_subdirectory (librunshell) add_subdirectory (libmoonvisit) add_subdirectory (libwidechar) add_subdirectory (libprocalive) add_subdirectory (luastatus) #------------------------------------------------------------------------------ macro (DEF_OPT optname subdir defvalue) option (${optname} "build ${subdir}" ${defvalue}) if (${optname}) add_subdirectory (${subdir}) endif () endmacro () DEF_OPT (BUILD_BARLIB_DWM "barlibs/dwm" ON) DEF_OPT (BUILD_BARLIB_I3 "barlibs/i3" ON) DEF_OPT (BUILD_BARLIB_LEMONBAR "barlibs/lemonbar" ON) DEF_OPT (BUILD_BARLIB_STDOUT "barlibs/stdout" ON) DEF_OPT (BUILD_PLUGIN_ALSA "plugins/alsa" ON) DEF_OPT (BUILD_PLUGIN_BACKLIGHT_LINUX "plugins/backlight-linux" ON) DEF_OPT (BUILD_PLUGIN_BATTERY_LINUX "plugins/battery-linux" ON) DEF_OPT (BUILD_PLUGIN_CPU_FREQ_LINUX "plugins/cpu-freq-linux" ON) DEF_OPT (BUILD_PLUGIN_CPU_USAGE_LINUX "plugins/cpu-usage-linux" ON) DEF_OPT (BUILD_PLUGIN_DBUS "plugins/dbus" ON) DEF_OPT (BUILD_PLUGIN_DISK_IO_LINUX "plugins/disk-io-linux" ON) DEF_OPT (BUILD_PLUGIN_FILE_CONTENTS_LINUX "plugins/file-contents-linux" ON) DEF_OPT (BUILD_PLUGIN_FS "plugins/fs" ON) DEF_OPT (BUILD_PLUGIN_IMAP "plugins/imap" ON) DEF_OPT (BUILD_PLUGIN_INOTIFY "plugins/inotify" ON) DEF_OPT (BUILD_PLUGIN_IS_PROGRAM_RUNNING "plugins/is-program-running" ON) DEF_OPT (BUILD_PLUGIN_MEM_USAGE_LINUX "plugins/mem-usage-linux" ON) DEF_OPT (BUILD_PLUGIN_MPD "plugins/mpd" ON) DEF_OPT (BUILD_PLUGIN_MPRIS "plugins/mpris" ON) DEF_OPT (BUILD_PLUGIN_MULTIPLEX "plugins/multiplex" ON) DEF_OPT (BUILD_PLUGIN_NETWORK_LINUX "plugins/network-linux" ON) DEF_OPT (BUILD_PLUGIN_NETWORK_RATE_LINUX "plugins/network-rate-linux" ON) DEF_OPT (BUILD_PLUGIN_PIPE "plugins/pipe" OFF) DEF_OPT (BUILD_PLUGIN_PIPEV2 "plugins/pipev2" ON) DEF_OPT (BUILD_PLUGIN_PULSE "plugins/pulse" OFF) DEF_OPT (BUILD_PLUGIN_SYSTEMD_UNIT "plugins/systemd-unit" OFF) DEF_OPT (BUILD_PLUGIN_TEMPERATURE_LINUX "plugins/temperature-linux" ON) DEF_OPT (BUILD_PLUGIN_TIMER "plugins/timer" ON) DEF_OPT (BUILD_PLUGIN_UDEV "plugins/udev" ON) DEF_OPT (BUILD_PLUGIN_UNIXSOCK "plugins/unixsock" OFF) DEF_OPT (BUILD_PLUGIN_WEB "plugins/web" OFF) DEF_OPT (BUILD_PLUGIN_XKB "plugins/xkb" ON) DEF_OPT (BUILD_PLUGIN_XTITLE "plugins/xtitle" ON) DEF_OPT (BUILD_TESTS "tests" OFF) ================================================ FILE: COPYING.LESSER.txt ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ================================================ FILE: COPYING.txt ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: DEPENDS.txt ================================================ luastatus itself, without any barlibs and/or plugins, has the following dependencies: * docutils >=0.11 (rst2man program) * CMake >=3.1.3 * pkg-config >=0.25 * either Lua 5.1, Lua 5.2, Lua 5.3, Lua 5.4, or LuaJIT >=2.0.0 For running tests, you will also need: * valgrind (some older versions don't like LuaJIT; >=3.11.0 are known to be OK) * For 'i3' barlib tests: 'jq' binary * For 'dbus' plugin tests: a D-Bus daemon running in session mode; 'dbus-send' binary * For 'pulse' plugin tests: a running PulseAudio daemon; 'pactl' and 'pacmd' binaries * For 'web' plugin tests: 'libwebsockets' library (to build 'httpserv' program) Barlib 'dwm' has the following dependencies: * xcb >=1.10 Barlib 'i3' has the following dependencies: * yajl >=2.0.4 Plugin 'alsa' has the following dependencies: * alsa >=1.0.27.2 Plugin 'backlight-linux' has the following dependencies: * plugin 'udev' Plugin 'battery-linux' has the following dependencies: * plugin 'udev' Plugin 'cpu-freq-linux' has the following dependencies: * plugin 'timer' Plugin 'cpu-usage-linux' has the following dependencies: * plugin 'timer' Plugin 'dbus' has the following dependencies: * glib-2.0 >=2.40.2 * gio-2.0 >=2.40.2 Plugin 'disk-io-linux' has the following dependencies: * plugin 'timer' Plugin 'file-contents-linux' has the following dependencies: * plugin 'inotify' Plugin 'imap' has the following dependencies: * LuaSocket library for the Lua version that luastatus was built with * LuaSec library for the Lua version that luastatus was built with Plugin 'is-program-running' has the following dependencies: * plugin 'timer' Plugin 'inotify' has the following dependencies: * a Linux system with a libc that provides (preferably glibc) Plugin 'mem-usage-linux' has the following dependencies: * plugin 'timer' Plugin 'mpris' has the following dependencies: * plugin 'dbus' Plugin 'network-linux' has the following dependencies: * a Linux system with Linux kernel headers * libnl >=3.0 * libnl-genl >=3.0 Plugin 'network-rate-linux' has the following dependencies: * plugin 'timer' Plugin 'pipe' has the following dependencies: * plugin 'timer' Plugin 'pulse' has the following dependencies: * libpulse >=4.0 Plugin 'systemd-unit' has the following dependencies: * plugin 'dbus' Plugin 'temperature-linux' has the following dependencies: * plugin 'timer' Plugin 'udev' has the following dependencies: * libudev >=204 Plugin 'web' has the following dependencies: * cJSON >=1.7.10 * libcurl >=7.8 Plugin 'xkb' has the following dependencies: * x11 >=1.6.2 Plugin 'xtitle' has the following dependencies: * xcb >=1.10 * xcb-ewmh >=0.4.1 * xcb-icccm >=0.4.1 * xcb-event >=0.3.8 ================================================ FILE: DOCS/CUSTOM_DATA_SRC_WIDGET.md ================================================ Writing a widget that uses custom data source === A “custom data source” widget is a fancy way to refer to a widget that wants to wait for events and/or receive data using some blocking Lua operation (most commonly using some external Lua module), rather than using some luastatus plugin. The luastatus project explicitly supports this flow; in fact, the “separate state” thing is there exactly to help such widgets react to events timely. The basic mechanism is (details may vary for different widgets): * Specify `timer` as a plugin, with `period = 0` in plugin options (`opts`). * Do the following in the callback function (`cb`): + wait for next event (unless this is the first time `cb` is called); + generate data for barlib and return it. * If need to handle events, use separate-state event handler. Tricks to know about --- * Use `luastatus.plugin.push_period(seconds)` function of the `timer` plugin to force it to sleep for a specified number of seconds after barlib receives your data. Note that this is a “*push* timeout” operation, not “*set* timeout”, meaning that it will only be respected for a single iteration, and then “popped” (forgotten). * Use the argument to `cb` provided by the `timer` plugin to tell if this is the first time `cb` is called: the argument is a string, and it is `"hello"` if and only if this is the first time. * The `timer` plugin supports wakeup FIFO. This might be useful if `push_period` is used in case of error, as a way to force an earlier re-try. The FIFO may be touched from inside the event handler, or from anywhere else. List of derived plugins using custom data sources --- * imap; * pipe. List of widget examples using custom data sources --- * btc-price; * gmail; * update-on-click; * weather. ================================================ FILE: DOCS/MIGRATION_GUIDE.md ================================================ 0.2.0 to 0.3.0 === * Semantics of the `greet` option of `inotify` plugin has changed; see `plugins/inotify/README.md` for details. 0.1.0 to 0.2.0 === * `luastatus.spawn`, `luastatus.rc` and `luastatus.dollar` functions have been removed. Use `os.execute` and/or `io.popen` instead — and please don’t forget to properly escape the arguments: ````lua function shell_escape(s) return "'" .. s:gsub("'", "'\\''") .. "'" end ```` * `pipe` plugin has been removed. Use the `timer` plugin and `io.open` instead: ````lua f = io.popen('your command', 'r') wdiget = { plugin = 'timer', cb = function() local line = f:read('*line') -- ... end, } ```` ================================================ FILE: DOCS/WRITING_BARLIB_OR_PLUGIN.md ================================================ Thread-safety === Each non-thread-safe thing must be synchronized with other entities by means of the `map_get` function (see `DOCS/design/map_get.md`). Environment variables === Your plugin or barlib must not modify environment variables (this includes `unsetenv()`, `setenv()`, `putenv()`, and modifying `environ`). Signals === Your plugin or barlib can install signal handlers, but: 1. this must be done with `sigaction()`, and `sa_mask` field of `struct sigaction` must include `SA_RESTART`; 2. use the `map_get` facility to ensure there are no conflicting handlers installed; use, for example, the key of `"flag:signal_handled:SIGUSR1"` for signal `SIGUSR1`; the value behind the key should be checked to be null and then set to some non-null pointer. For POSIX real-time signals, use `"SIGRTMIN+%d"` nomenclature, where `%d` is a non-negative offset in decimal. Your plugin or barlib can call `pthread_sigmask()` (and, consequently, `pselect()`). Writing a plugin === Copy `include/plugin_data.h`, `include/plugin_data_v1.h`, `include/plugin_v1.h` and `include/common.h`; include `include/plugin_v1.h` and start reading `include/plugin_data.h`. Then, declare a global `const LuastatusIfacePlugin luastatus_iface_plugin_v1` variable. Writing a barlib === Copy `include/barlib_data.h`, `include/barlib_data_v1.h`, `include/barlib_v1.h` and `include/common.h`; include `include/barlib_v1.h` and start reading `include/barlib_data.h`. Then, declare a global `const LuastatusIfaceBarlib luastatus_iface_barlib_v1` variable. ================================================ FILE: DOCS/c_notes/eintr-policy.md ================================================ The main problem is that all the stdio functions may, according to POSIX, fail with `EINTR`, and there is no way to restart some of them, e.g. `fprintf`. Our solution is to ensure that `SA_RESTART` is set for all the signals that could be raised (without terminating the program). See also: http://man7.org/linux/man-pages/man7/signal.7.html, section "Interruption of system calls and library functions by signal handlers". ================================================ FILE: DOCS/c_notes/empty-ranges-and-c-stdlib.md ================================================ http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf Subclause 7.1.4 Use of library functions, point 1: > Each of the following statements applies unless explicitly stated otherwise in the > detailed descriptions that follow: If an argument to a function has an invalid value (such > as a value outside the domain of the function, or a pointer outside the address space of > the program, or a null pointer, or a pointer to non-modifiable storage when the > corresponding parameter is not const-qualified) or a type (after promotion) not expected > by a function with variable number of arguments, the behavior is undefined. If a function > argument is described as being an array, the pointer actually passed to the function shall > have a value such that all address computations and accesses to objects (that would be > valid if the pointer did point to the first element of such an array) are in fact valid. It’s not OK to call any function defined in `` with a pointer that can’t be dereferenced: Subclause 7.21.1 String handling conventions, point 2: > Where an argument declared as `size_t n` specifies the length of the array for a function, `n` > can have the value zero on a call to that function. Unless explicitly stated otherwise in > the description of a particular function in this subclause, pointer arguments on such a > call shall still have valid values, as described in 7.1.4. On such a call, a function > that locates a character finds no occurrence, a function that compares two character > sequences returns zero, and a function that copies characters copies zero characters. It’s not OK to call `qsort`/`bsearch` with a pointer that can’t be dereferenced: Subclause 7.20.5 Searching and sorting utilites, point 1: > These utilities make use of a comparison function to search or sort arrays of unspecified > type. Where an argument declared as `size_t nmemb` specifies the length of the array for a > function, `nmemb` can have the value zero on a call to that function; the comparison function > is not called, a search finds no matching element, and sorting performs no rearrangement. > Pointer arguments on such a call shall still have valid values, as described in 7.1.4. However, it’s OK to call `snprintf`/`vsnprintf` with a pointer that can’t be dereferenced, as long as `n` is zero: Subclause 7.19.6.5 The `snprintf` function, point 2: > The `snprintf` function is equivalent to `fprintf`, except that the output is written into an > array (specified by argument `s`) rather than to a stream. If `n` is zero, nothing is written, and > `s` may be a null pointer. Otherwise, output characters beyond the `n-1`st are discarded rather > than being written to the array, and a null character is written at the end of the characters > actually written into the array. If copying takes place between objects that overlap, the behavior > is undefined. ================================================ FILE: DOCS/design/locking-patterns.md ================================================ The list of locking patterns of the main luastatus binary; run grep -Ew '(UN)?LOCK_[A-Z]' luastatus/luastatus.c to verify it is up-to-date. It can be said that our order is: E < L < B. We also don't lock the same mutex twice in any of the “procedures”. This suffices to say there are no deadlocks. (We also have the `tests/torture.sh` test!) cb-gets-called() { lock L lock B unlock B unlock L } plugin-begins-call-and-cancels() { lock L unlock L } #----------------------------------------------- event-gets-called-and-raises-error-E() { lock E lock B unlock B unlock E } events-gets-called-and-succeeds-E() { lock E unlock E } barlib-ew-begins-call-and-cancels-E() { lock E unlock E } #----------------------------------------------- event-gets-called-and-raises-error-L() { lock L lock B unlock B unlock L } events-gets-called-and-succeeds-L() { lock L unlock L } barlib-ew-begins-call-and-cancels-L() { lock L unlock L } #----------------------------------------------- set-error-when-plugin-run-returned() { lock B unlock B } set-error-when-widget-init-failed() { lock B unlock B } ================================================ FILE: DOCS/design/map_get.md ================================================ Overview === luastatus provides the following function to barlibs and plugins: ```c void ** (*map_get)(void *userdata, const char *key); ``` This function is **not** thread-safe and should only be used in the `init` function. luastatus maintains a global mapping from zero-terminated strings to pointers (`void *`). `map_get` returns a pointer to the pointer corresponding to the given key; if a map entry with the given key does not exist, it creates one with a null pointer value. You can read and/or write from/to this pointer-to-pointer; it is guaranteed to be persistent across other calls to `map_get` and other functions. Its intended use is for synchronization. ================================================ FILE: README.md ================================================ [![CircleCI build status](https://circleci.com/gh/shdown/luastatus.svg?style=shield)](https://circleci.com/gh/shdown/luastatus) ![Since 2016](https://img.shields.io/badge/Since-2016-lightblue) **luastatus** is a universal status bar content generator. It allows you to configure the way the data from event sources is processed and shown, with Lua. Its main feature is that the content can be updated immediately as some event occurs, be it a change of keyboard layout, active window title, volume or a song in your favorite music player (provided that there is a plugin for it) — a thing rather uncommon for tiling window managers. Its motto is: > No more heavy-forking, second-lagging shell-script status bar generators! Screenshot === ![Screenshot](https://user-images.githubusercontent.com/5462697/39099519-092459aa-4685-11e8-94fe-0ac1cf706d82.gif) Above is i3bar with luastatus with Bitcoin price, time, volume, and keyboard layout widgets. Key concepts === ![Explanation](https://user-images.githubusercontent.com/5462697/42400208-5b54f5f2-8179-11e8-9836-70d4e46d5c13.png) In short: * plugin is a thing that decides when to call the callback function `widget.cb` and what to pass to it; * barlib (**bar** **lib**rary) is a thing that decides what to with values that `widget.cb` function returns; * there are also *derived plugins*, which are plugins written in Lua that use regular plugins. Examples === ALSA volume widget: ```lua widget = { plugin = 'alsa', opts = { channel = 'PCM' }, cb = function(t) if t.mute then return {full_text = '[mute]', color = '#e03838'} else local percent = (t.vol.cur - t.vol.min) / (t.vol.max - t.vol.min) * 100 return {full_text = string.format('[%3d%%]', math.floor(0.5 + percent)), color = '#718ba6'} end end, event = function(t) if t.button == 1 then -- left mouse button os.execute('urxvt -e alsamixer &') end end } ``` GMail widget (uses the derived plugin `imap`): ```lua --[[ -- Expects 'credentials.lua' to be present in the current directory; it may contain, e.g., -- return { -- gmail = { -- login = 'john.smith', -- password = 'qwerty' -- } -- } --]] credentials = require 'credentials' widget = luastatus.require_plugin('imap').widget{ host = 'imap.gmail.com', port = 993, mailbox = 'Inbox', use_ssl = true, timeout = 2 * 60, handshake_timeout = 10, login = credentials.gmail.login, password = credentials.gmail.password, error_sleep_period = 60, cb = function(unseen) if unseen == nil then return nil elseif unseen == 0 then return {full_text = '[-]', color = '#595959'} else return {full_text = string.format('[%d unseen]', unseen)} end end, event = [[ -- separate-state event function local t = ... -- obtain argument of this implicit function if t.button == 1 then -- left mouse button os.execute('xdg-open https://gmail.com &') end ]] } ``` See more examples [here](https://github.com/shdown/luastatus/tree/master/examples). Installation === `cmake . && make && sudo make install` You can specify a Lua library to build with: `cmake -DWITH_LUA_LIBRARY=luajit .` You can disable building certain barlibs and plugins, e.g. `cmake -DBUILD_PLUGIN_XTITLE=OFF .` You can disable building man pages: `cmake -DBUILD_DOCS=OFF .` ArchLinux --- ArchLinux users can use one of the following AUR packages: * [luastatus](https://aur.archlinux.org/packages/luastatus) * [luastatus-luajit](https://aur.archlinux.org/packages/luastatus-luajit) * [luastatus-git](https://aur.archlinux.org/packages/luastatus-git) * [luastatus-luajit-git](https://aur.archlinux.org/packages/luastatus-luajit-git) There is also the [luastatus-meta](https://aur.archlinux.org/packages/luastatus-meta) meta package which installs the dependencies of all of luastatus's plugins. Getting started === It is recommended to first have a look at the [luastatus' man page](https://github.com/shdown/luastatus/blob/master/luastatus/README.rst). Then, read the barlib's and plugins' documentation, either via directly viewing `barlibs//README.rst` and `plugins//README.rst` files, or via installing the man pages and reading `luastatus-barlib-(7)` and `luastatus-plugin-(7)`. Barlib-specific notes on usage follow. i3 or sway ---------- `luastatus-i3-wrapper` should be specified as the i3bar's or sway-bar's status command in the config, e.g.: ``` bar { status_command cd ~/.config/luastatus && exec luastatus-i3-wrapper -B no_separators time-battery-combined.lua alsa.lua xkb.lua ``` Since sway-bar is format-compatible with i3wm, the exact same configuration works for both. See also [README for i3](https://github.com/shdown/luastatus/blob/master/barlibs/i3/README.rst) and [examples for i3](https://github.com/shdown/luastatus/tree/master/examples/i3). dwm --- luastatus should simply be launched with `-b dwm`, e.g.: ``` luastatus -b dwm -B separator=' • ' alsa.lua time-battery-combined.lua ``` See also [README for dwm](https://github.com/shdown/luastatus/blob/master/barlibs/dwm/README.rst) and [examples for dwm](https://github.com/shdown/luastatus/tree/master/examples/dwm). lemonbar -------- `lemonbar` should be launched with `luastatus-lemonbar-launcher`, e.g.: ``` luastatus-lemonbar-launcher -p -B#111111 -p -f'Droid Sans Mono for Powerline:pixelsize=12:weight=Bold' -- -Bseparator=' ' alsa.lua time-date.lua ``` See also [README for lemonbar](https://github.com/shdown/luastatus/blob/master/barlibs/lemonbar/README.rst) and [examples for lemonbar](https://github.com/shdown/luastatus/tree/master/examples/lemonbar). stdout ------ luastatus should be launched with `luastatus-stdout-wrapper`; or write your own wrapper, see e.g. the [wrapper for launching dvtm with luastatus](https://github.com/shdown/luastatus/blob/master/barlibs/stdout/luastatus-dvtm). See also [README for stdout](https://github.com/shdown/luastatus/blob/master/barlibs/stdout/README.rst) and and [examples for stdout](https://github.com/shdown/luastatus/tree/master/examples/stdout). Supported Lua versions === * 5.1 * LuaJIT, which is currently 5.1-compatible with "some language and library extensions from Lua 5.2" * 5.2 * 5.3 * 5.4 * 5.5 Reporting bugs, requesting features, suggesting patches === Feel free to open an issue or a pull request. Migrating from older versions === See the [Migration Guide](https://github.com/shdown/luastatus/blob/master/DOCS/MIGRATION_GUIDE.md). Testing === Here, at luastatus, we take code correctness and safety very seriously. We do the following things: * We use best practices for C programming: - All non-trivial string processing goes through libsafe - We mark printf-like functions with appropriate attributes in order to get warnings on illegal format string or wrong argument types - We handle every error possible (except for cases where we can't, and don't want to, do anything about an error) - We pay attention to integer overflows/underflows, conversion of integers to/from floating-point types, and other possible cases of undefined behavior - We use typedef'd enum types instead of "untyped" integers to represent enum's values so that we can get warnings when a switch does not account for some value - We use macros for (re)allocations that make it impossible to get types wrong * We compile with `-Wall -Wextra` * We have a comprehensive test suite; it contains tests for luastatus, barlibs and plugins, and also includes "torture"-style tests (a.k.a. stress tests) which bombard luastatus with a lot of events from a plugin and a barlib simultaneously: * It passes under valgrind [memcheck tool] * It passes under valgrind [helgrind tool] * It passes under UBSAN (Undefined Behavior Sanitizer) * It passes under ASAN (Address Sanitizer) * It passes under LSAN (Leak Sanitizer) * It passes under TSAN (Thread Sanitizer) * Everything that theoretically can be an attack surface is fuzzed, under UBSAN, with both AFL and AFL++; we also have a sophisticated fuzz testcase generator (in `fuzz_utils/gen_testcases/`) We tried to use additional compiler warnings, `-fanalyzer`, and external linters/static analyzers, but these tools only gave false positives, except for PVS-Studio, which found one actual bug, but with a lot of false positives. So we don't use any of these on a regular basis. ================================================ FILE: barlibs/dwm/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_barlib (barlib-dwm $ ${sources}) target_compile_definitions (barlib-dwm PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (barlib-dwm LUA) target_include_directories (barlib-dwm PUBLIC "${PROJECT_SOURCE_DIR}") find_package (PkgConfig REQUIRED) pkg_check_modules (XCB REQUIRED xcb) luastatus_target_build_with (barlib-dwm XCB) luastatus_add_man_page (README.rst luastatus-barlib-dwm 7) ================================================ FILE: barlibs/dwm/README.rst ================================================ .. :X-man-page-only: luastatus-barlib-dwm .. :X-man-page-only: ##################### .. :X-man-page-only: .. :X-man-page-only: ########################## .. :X-man-page-only: dwm barlib for luastatus .. :X-man-page-only: ########################## .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This barlib updates the name of the root window. It joins all non-empty strings returned by widgets by a separator, which defaults to ``" | "``. It does not provide functions and does not support events. ``cb`` return value =================== Either of: * a string An empty string hides the widget. * an array of strings Equivalent to returning a string with all non-empty elements of the array joined by the separator. * ``nil`` Hides the widget. Options ======= The following options are supported: * ``display=`` Set the name of a display to connect to. Default is to use ``DISPLAY`` environment variable. * ``separator=`` Set the separator. ================================================ FILE: barlibs/dwm/dwm.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "include/barlib_v1.h" #include "include/sayf_macros.h" #include #include #include #include #include #include #include "libls/ls_alloc_utils.h" #include "libls/ls_cstring_utils.h" #include "libls/ls_string.h" #include "libls/ls_lua_compat.h" typedef struct { size_t nwidgets; LS_String *bufs; // Temporary buffer for secondary buffering, to avoid unneeded redraws. LS_String tmpbuf; // Buffer for the content of the widgets joined by /sep/. LS_String joined; char *sep; xcb_connection_t *conn; xcb_window_t root; } Priv; static void destroy(LuastatusBarlibData *bd) { Priv *p = bd->priv; for (size_t i = 0; i < p->nwidgets; ++i) ls_string_free(p->bufs[i]); free(p->bufs); ls_string_free(p->tmpbuf); ls_string_free(p->joined); free(p->sep); if (p->conn) xcb_disconnect(p->conn); free(p); } // Returns zero on success, non-zero XCB error code on failure. In either case, /*out_conn/ is // written to, and should be closed with /xcb_disconnect()/. static int do_connect( const char *dpyname, xcb_connection_t **out_conn, xcb_window_t *out_root) { int screenp; *out_conn = xcb_connect(dpyname, &screenp); int r = xcb_connection_has_error(*out_conn); if (r != 0) return r; const xcb_setup_t *setup = xcb_get_setup(*out_conn); xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup); for (int i = 0; i < screenp; ++i) xcb_screen_next(&iter); *out_root = iter.data->root; return 0; } static bool redraw(LuastatusBarlibData *bd) { Priv *p = bd->priv; LS_String *joined = &p->joined; size_t n = p->nwidgets; LS_String *bufs = p->bufs; const char *sep = p->sep; ls_string_clear(joined); for (size_t i = 0; i < n; ++i) { if (bufs[i].size) { if (joined->size) { ls_string_append_s(joined, sep); } ls_string_append_b(joined, bufs[i].data, bufs[i].size); } } xcb_generic_error_t *err = xcb_request_check( p->conn, xcb_change_property_checked( p->conn, XCB_PROP_MODE_REPLACE, p->root, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, joined->size, joined->data ) ); if (err) { LS_FATALF(bd, "XCB error %d occurred", err->error_code); free(err); return false; } return true; } static int init(LuastatusBarlibData *bd, const char *const *opts, size_t nwidgets) { Priv *p = bd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .nwidgets = nwidgets, .bufs = LS_XNEW(LS_String, nwidgets), .tmpbuf = ls_string_new_reserve(512), .joined = ls_string_new_reserve(1024), .sep = NULL, .conn = NULL, }; for (size_t i = 0; i < nwidgets; ++i) p->bufs[i] = ls_string_new_reserve(512); // All the options may be passed multiple times! const char *dpyname = NULL; const char *sep = NULL; for (const char *const *s = opts; *s; ++s) { const char *v; if ((v = ls_strfollow(*s, "display="))) { dpyname = v; } else if ((v = ls_strfollow(*s, "separator="))) { sep = v; } else { LS_FATALF(bd, "unknown option '%s'", *s); goto error; } } p->sep = ls_xstrdup(sep ? sep : " | "); int r = do_connect(dpyname, &p->conn, &p->root); if (r != 0) { LS_FATALF(bd, "can't connect to display: XCB error %d", r); goto error; } // Clear the current name. if (!redraw(bd)) goto error; return LUASTATUS_OK; error: destroy(bd); return LUASTATUS_ERR; } static int set(LuastatusBarlibData *bd, lua_State *L, size_t widget_idx) { Priv *p = bd->priv; LS_String *buf = &p->tmpbuf; ls_string_clear(buf); // L: ? data switch (lua_type(L, -1)) { case LUA_TSTRING: { size_t ns; const char *s = lua_tolstring(L, -1, &ns); ls_string_assign_b(buf, s, ns); } break; case LUA_TNIL: break; case LUA_TTABLE: { const char *sep = p->sep; size_t len = ls_lua_array_len(L, -1); for (size_t i = 1; i <= len; ++i) { lua_rawgeti(L, -1, i); // L: ? data value if (lua_isnil(L, -1)) { goto next; } if (!lua_isstring(L, -1)) { LS_ERRF(bd, "table value: expected string, found %s", luaL_typename(L, -1)); goto invalid_data; } size_t ns; const char *s = lua_tolstring(L, -1, &ns); if (buf->size && ns) { ls_string_append_s(buf, sep); } ls_string_append_b(buf, s, ns); next: lua_pop(L, 1); // L: ? data } // L: ? data } break; default: LS_ERRF(bd, "expected table, string or nil, found %s", luaL_typename(L, -1)); goto invalid_data; } if (!ls_string_eq(*buf, p->bufs[widget_idx])) { ls_string_swap(buf, &p->bufs[widget_idx]); if (!redraw(bd)) { return LUASTATUS_ERR; } } return LUASTATUS_OK; invalid_data: ls_string_clear(&p->bufs[widget_idx]); return LUASTATUS_NONFATAL_ERR; } static int set_error(LuastatusBarlibData *bd, size_t widget_idx) { Priv *p = bd->priv; ls_string_assign_s(&p->bufs[widget_idx], "(Error)"); if (!redraw(bd)) { return LUASTATUS_ERR; } return LUASTATUS_OK; } LuastatusBarlibIface luastatus_barlib_iface_v1 = { .init = init, .set = set, .set_error = set_error, .destroy = destroy, }; ================================================ FILE: barlibs/i3/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_barlib ( barlib-i3 $ $ ${sources} ) target_compile_definitions (barlib-i3 PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (barlib-i3 LUA) target_include_directories (barlib-i3 PUBLIC "${PROJECT_SOURCE_DIR}") find_package (PkgConfig REQUIRED) pkg_check_modules (YAJL REQUIRED yajl>=2.0.4) luastatus_target_build_with (barlib-i3 YAJL) find_library (MATH_LIBRARY m) if (MATH_LIBRARY) target_link_libraries (barlib-i3 PUBLIC ${MATH_LIBRARY}) endif () include (GNUInstallDirs) install (PROGRAMS luastatus-i3-wrapper DESTINATION ${CMAKE_INSTALL_BINDIR}) luastatus_add_man_page (README.rst luastatus-barlib-i3 7) ================================================ FILE: barlibs/i3/README.rst ================================================ .. :X-man-page-only: luastatus-barlib-i3 .. :X-man-page-only: ################### .. :X-man-page-only: .. :X-man-page-only: ############################ .. :X-man-page-only: i3/sway barlib for luastatus .. :X-man-page-only: ############################ .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This barlib talks with i3bar (or sway-bar). To use this barlib, you need to specify ``luastatus-i3-wrapper`` with appropriate arguments as the ``status_command`` parameter of a bar in the i3 (or sway) configuration file. For example:: bar { status_command cd ~/.config/luastatus && exec luastatus-i3-wrapper -B no_separators time-battery-combined.lua alsa.lua xkb.lua Redirections and ``luastatus-i3-wrapper`` ========================================= i3bar and sway-bar require all the data to be written to stdout and read from stdin. This makes it very easy to mess things up: Lua's ``print()`` prints to stdout, processes spawned by widgets/plugins inherit our stdin and stdout, etc. That's why this barlib requires that stdin and stdout file descriptors are manually redirected. A shell wrapper, ``luastatus-i3-wrapper``, is shipped with it; it does all the redirections and executes ``luastatus`` with ``-b i3``, all the required ``-B`` options, and additional arguments passed by you. ``cb`` return value =================== Either of: * an empty table or ``nil`` Hide the widget. * a table with strings keys Is interpreted as a single segment (or a "block"). The keys *should not* include ``name``, as it is set automatically to be able to tell which widget was clicked. For more information, see http://i3wm.org/docs/i3bar-protocol.html#_blocks_in_detail. * an array (table with numeric keys) Is interpreted as an array of segments. To be able to tell which one was clicked, set the ``instance`` field of a segment. ``nil`` elements are ignored. ``event`` argument ================== A table with all click properties i3bar (or sway-bar) provides. For more information, see http://i3wm.org/docs/i3bar-protocol.html#_click_events. Functions ========= The following functions are provided: * ``escaped_str = luastatus.barlib.pango_escape(str)`` Escapes text for the Pango markup. Example ======= An example that uses all possible features:: function get_user_name() local f = io.popen('whoami', 'r') local r = f:read('*line') f:close() return r end i = 0 widget = { plugin = 'timer', opts = {period = 2}, cb = function(t) i = i + 1 if i == 1 then return {} -- hides widget; alternatively, you can return nil. elseif i == 2 then -- no markup unless you specify markup='pango' return {full_text = '', color = '#aaaa00'} elseif i == 3 then -- see https://developer.gnome.org/pygtk/stable/pango-markup-language.html return {full_text = 'Hello, ' .. luastatus.barlib.pango_escape(get_user_name()) .. '!', markup = 'pango'} elseif i == 4 then i = 0 return { {full_text = 'Now,', instance = 'now-segment'}, nil, -- nils are ignored so that you can do -- return {get_time(), get_battery(), get_smth_else()} -- where, for example, get_battery() returns nil if the battery is full. {full_text = 'click me!', instance = 'click-me-segment'}, } end end, event = function(t) local r = {'properties:'} for k, v in pairs(t) do table.insert(r, k .. '=' .. tostring(v)) end os.execute(string.format( "notify-send 'Click event' '%s'", table.concat(r, ' '))) end, } Options ======= The following options are supported: * ``in_fd=`` File descriptor to read i3bar or sway-bar input from. Usually set by the wrapper. * ``out_fd=`` File descriptor to write to. Usually set by the wrapper. * ``no_click_events`` Tell i3bar (or sway-bar) we don't want to receive click events. This changes its behavior in that it will interpret "clicks" on segments as if an empty space on the bar was clicked, particularly, will switch workspaces if you scroll on a segment. * ``no_separators`` Append ``"separator": false`` to a segment, unless it has a ``separator`` key. Also appends it to an ``(Error)`` segment. * ``allow_stopping`` Allow i3bar (or sway-bar) to send luastatus ``SIGSTOP`` when it thinks it becomes invisible, and ``SIGCONT`` when it thinks it becomes visible. Quite a questionable feature. * ``extra_init_json=`` Extra JSON to output in header, e.g. ``"key1":10,"key2":true``. This could be of some use because Sway WM seems to be experimenting with additional header fields (but not yet documenting them), e.g. ``float_event_coords``. ================================================ FILE: barlibs/i3/escape_json_str.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "escape_json_str.h" #include #include "libsafe/safev.h" #include "libsafe/mut_safev.h" static const SAFEV HEX_CHARS = SAFEV_STATIC_INIT_FROM_LITERAL("0123456789ABCDEF"); static inline void append_sv(LS_String *dst, SAFEV v) { ls_string_append_b(dst, SAFEV_ptr_UNSAFE(v), SAFEV_len(v)); } void append_json_escaped_str(LS_String *dst, SAFEV v) { ls_string_append_c(dst, '"'); char esc_arr[] = {'\\', 'u', '0', '0', '#', '#'}; MUT_SAFEV esc = MUT_SAFEV_new_UNSAFE(esc_arr, sizeof(esc_arr)); size_t n = SAFEV_len(v); size_t prev = 0; for (size_t i = 0; i < n; ++i) { unsigned char c = SAFEV_at(v, i); if (c < 32 || c == '\\' || c == '"' || c == '/') { append_sv(dst, SAFEV_subspan(v, prev, i)); MUT_SAFEV_set_at(esc, 4, SAFEV_at(HEX_CHARS, c / 16)); MUT_SAFEV_set_at(esc, 5, SAFEV_at(HEX_CHARS, c % 16)); append_sv(dst, MUT_SAFEV_TO_SAFEV(esc)); prev = i + 1; } } append_sv(dst, SAFEV_subspan(v, prev, n)); ls_string_append_c(dst, '"'); } ================================================ FILE: barlibs/i3/escape_json_str.h ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef escape_json_str_h_ #define escape_json_str_h_ #include "libls/ls_string.h" #include "libsafe/safev.h" // Append to /dst/ JSON-escaped C string /v/. void append_json_escaped_str(LS_String *dst, SAFEV v); #endif ================================================ FILE: barlibs/i3/event_watcher.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "event_watcher.h" #include #include #include #include #include #include #include #include #include "include/sayf_macros.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_parse_int.h" #include "libls/ls_strarr.h" #include "libls/ls_panic.h" #include "libls/ls_lua_compat.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_freemem.h" #include "priv.h" // If this is to be incremented, /lua_checkstack()/ must be called at appropriate times, and the // depth of the recursion in /push_object()/ be potentially limited somehow. enum { DEPTH_LIMIT = 10 }; typedef struct { enum { TYPE_ARRAY_START, TYPE_ARRAY_END, TYPE_MAP_START, TYPE_MAP_END, TYPE_STRING_KEY, TYPE_STRING, TYPE_NUMBER, TYPE_BOOL, TYPE_NULL, } type; union { size_t str_idx; double num; bool flag; } as; } Token; typedef struct { Token *data; size_t size; size_t capacity; } TokenList; static inline TokenList token_list_new(void) { return (TokenList) {NULL, 0, 0}; } static inline void token_list_push(TokenList *x, Token token) { if (x->size == x->capacity) { x->data = LS_M_X2REALLOC(x->data, &x->capacity); } x->data[x->size++] = token; } static inline void token_list_clear(TokenList *x) { x->data = LS_M_FREEMEM(x->data, &x->size, &x->capacity); } static inline void token_list_free(TokenList *x) { free(x->data); } typedef struct { // Current JSON nesting depth. Before the initial '[', depth == -1. int depth; // Whether the last key (at depth == 1) was "name". bool last_key_is_name; // An array in which all the JSON strings, including keys, are stored. LS_StringArray strarr; // A flat list of current event's tokens. TokenList tokens; // Current event's widget index, or a negative value if is not known yet or invalid. int widget; LuastatusBarlibData *bd; LuastatusBarlibEWFuncs funcs; } Context; // Converts a JSON object that starts at the token with index /*idx/ in /ctx->tokens/, to a Lua // object, and pushes it onto /L/'s stack. // Advances /*idx/ so that it points to one token past the last token of the object. static void push_object(lua_State *L, Context *ctx, size_t *idx) { Token t = ctx->tokens.data[*idx]; switch (t.type) { case TYPE_ARRAY_START: lua_newtable(L); // L: table ++*idx; for (size_t n = 1; ctx->tokens.data[*idx].type != TYPE_ARRAY_END; ++n) { push_object(L, ctx, idx); // L: table elem LS_ASSERT(n <= (size_t) LS_LUA_MAXI); lua_rawseti(L, -2, n); // L: table } break; case TYPE_MAP_START: lua_newtable(L); // L: table ++*idx; while (ctx->tokens.data[*idx].type != TYPE_MAP_END) { Token key = ctx->tokens.data[*idx]; LS_ASSERT(key.type == TYPE_STRING_KEY); // To limit the maximum number of slots pushed onto /L/'s stack to /N + O(1)/, where /N/ // is the maximum /ctx->depth/ encountered, we have to push the value first. // Unfortunately, /lua_settable()/ expects the key to be pushed first. So we simply swap // them with /lua_insert()/. ++*idx; push_object(L, ctx, idx); // L: table value size_t ns; const char *s = ls_strarr_at(ctx->strarr, key.as.str_idx, &ns); lua_pushlstring(L, s, ns); // L: table value key lua_insert(L, -2); // L: table key value lua_settable(L, -3); // L: table } break; case TYPE_STRING: { size_t ns; const char *s = ls_strarr_at(ctx->strarr, t.as.str_idx, &ns); lua_pushlstring(L, s, ns); } break; case TYPE_NUMBER: lua_pushnumber(L, t.as.num); break; case TYPE_BOOL: lua_pushboolean(L, t.as.flag); break; case TYPE_NULL: lua_pushnil(L); break; default: LS_MUST_BE_UNREACHABLE(); } // Now, /*idx/ points to the last token of the object; increment it by one. ++*idx; } static void flush(Context *ctx) { Priv *p = ctx->bd->priv; if (ctx->widget >= 0 && (size_t) ctx->widget < p->nwidgets) { lua_State *L = ctx->funcs.call_begin(ctx->bd->userdata, ctx->widget); size_t idx = 0; push_object(L, ctx, &idx); LS_ASSERT(idx == ctx->tokens.size); ctx->funcs.call_end(ctx->bd->userdata, ctx->widget); } // reset the context ctx->last_key_is_name = false; ls_strarr_clear(&ctx->strarr); token_list_clear(&ctx->tokens); ctx->widget = -1; } static int token_helper(Context *ctx, Token token) { if (ctx->depth == -1) { if (token.type != TYPE_ARRAY_START) { LS_ERRF(ctx->bd, "(event watcher) expected '['"); return 0; } ++ctx->depth; } else { if (ctx->depth == 0 && token.type != TYPE_MAP_START) { LS_ERRF(ctx->bd, "(event watcher) expected '{'"); return 0; } token_list_push(&ctx->tokens, token); switch (token.type) { case TYPE_ARRAY_START: case TYPE_MAP_START: if (++ctx->depth >= DEPTH_LIMIT) { LS_ERRF(ctx->bd, "(event watcher) nesting depth limit exceeded"); return 0; } break; case TYPE_ARRAY_END: case TYPE_MAP_END: if (--ctx->depth == 0) { flush(ctx); } break; default: break; } } return 1; } static inline size_t append_to_strarr(Context *ctx, const char *buf, size_t nbuf) { ls_strarr_append(&ctx->strarr, buf, nbuf); return ls_strarr_size(ctx->strarr) - 1; } static int callback_null(void *vctx) { return token_helper(vctx, (Token) {TYPE_NULL, {0}}); } static int callback_boolean(void *vctx, int value) { return token_helper(vctx, (Token) {TYPE_BOOL, {.flag = value}}); } static int callback_integer(void *vctx, long long value) { return token_helper(vctx, (Token) {TYPE_NUMBER, {.num = value}}); } static int callback_double(void *vctx, double value) { return token_helper(vctx, (Token) {TYPE_NUMBER, {.num = value}}); } static int callback_string(void *vctx, const unsigned char *buf, size_t nbuf) { Context *ctx = vctx; if (ctx->depth == 1 && ctx->last_key_is_name) { // parse error is OK here, /ctx->widget/ is checked in /flush()/. ctx->widget = ls_full_strtou_b((const char *) buf, nbuf); } return token_helper(ctx, (Token) { TYPE_STRING, {.str_idx = append_to_strarr(ctx, (const char *) buf, nbuf)} }); } static int callback_start_map(void *vctx) { return token_helper(vctx, (Token) {TYPE_MAP_START, {0}}); } static int callback_map_key(void *vctx, const unsigned char *buf, size_t nbuf) { Context *ctx = vctx; if (ctx->depth == 1) { ctx->last_key_is_name = (nbuf == 4 && memcmp(buf, "name", 4) == 0); } return token_helper(ctx, (Token) { TYPE_STRING_KEY, {.str_idx = append_to_strarr(ctx, (const char *) buf, nbuf)} }); } static int callback_end_map(void *vctx) { return token_helper(vctx, (Token) {TYPE_MAP_END, {0}}); } static int callback_start_array(void *vctx) { return token_helper(vctx, (Token) {TYPE_ARRAY_START, {0}}); } static int callback_end_array(void *vctx) { return token_helper(vctx, (Token) {TYPE_ARRAY_END, {0}}); } int event_watcher(LuastatusBarlibData *bd, LuastatusBarlibEWFuncs funcs) { Priv *p = bd->priv; if (p->noclickev) return LUASTATUS_NONFATAL_ERR; Context ctx = { .depth = -1, .last_key_is_name = false, .strarr = ls_strarr_new(), .tokens = token_list_new(), .widget = -1, .bd = bd, .funcs = funcs, }; yajl_callbacks callbacks = { .yajl_null = callback_null, .yajl_boolean = callback_boolean, .yajl_integer = callback_integer, .yajl_double = callback_double, .yajl_string = callback_string, .yajl_start_map = callback_start_map, .yajl_map_key = callback_map_key, .yajl_end_map = callback_end_map, .yajl_start_array = callback_start_array, .yajl_end_array = callback_end_array, }; yajl_handle hand = yajl_alloc(&callbacks, NULL, &ctx); unsigned char buf[1024]; while (1) { ssize_t nread = read(p->in_fd, buf, sizeof(buf)); if (nread < 0) { LS_ERRF(bd, "(event watcher) read error: %s", ls_tls_strerror(errno)); goto error; } else if (nread == 0) { LS_ERRF(bd, "(event watcher) i3bar closed its end of the pipe"); goto error; } switch (yajl_parse(hand, buf, nread)) { case yajl_status_ok: break; case yajl_status_client_canceled: goto error; case yajl_status_error: { unsigned char *descr = yajl_get_error(hand, /*verbose*/ 1, buf, nread); LS_ERRF(bd, "(event watcher) yajl parse error: %s", (char *) descr); yajl_free_error(hand, descr); } goto error; } } error: ls_strarr_destroy(ctx.strarr); token_list_free(&ctx.tokens); yajl_free(hand); return LUASTATUS_ERR; } ================================================ FILE: barlibs/i3/event_watcher.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef event_watcher_h_ #define event_watcher_h_ #include "include/barlib_data_v1.h" int event_watcher(LuastatusBarlibData *bd, LuastatusBarlibEWFuncs funcs); #endif ================================================ FILE: barlibs/i3/fuzz_esc_json/.gitignore ================================================ harness findings ================================================ FILE: barlibs/i3/fuzz_esc_json/build.sh ================================================ #!/bin/sh if [ -z "$CC" ]; then echo >&2 "You must set the 'CC' environment variable." echo >&2 "Hint: you probably want to set 'CC' to 'some-directory/afl-gcc'." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. $CC -Wall -Wextra -O3 -fsanitize=undefined -std=c99 -D_POSIX_C_SOURCE=200809L \ -I"$luastatus_root" \ ./harness.c \ ../escape_json_str.c \ "$luastatus_root"/libls/ls_string.c \ "$luastatus_root"/libls/ls_alloc_utils.c \ "$luastatus_root"/libls/ls_panic.c \ "$luastatus_root"/libls/ls_cstring_utils.c \ "$luastatus_root"/libsafe/*.c \ -o harness ================================================ FILE: barlibs/i3/fuzz_esc_json/clear.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" rm -rf ./findings ================================================ FILE: barlibs/i3/fuzz_esc_json/fuzz.sh ================================================ #!/bin/sh set -e if [ -z "$XXX_AFL_DIR" ]; then echo >&2 "You must set the 'XXX_AFL_DIR' environment variable." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" mkdir -p ./findings export UBSAN_OPTIONS=halt_on_error=1 export AFL_EXIT_WHEN_DONE=1 "$XXX_AFL_DIR"/afl-fuzz -i testcases -o findings -t 5 ./harness @@ ================================================ FILE: barlibs/i3/fuzz_esc_json/gen_testcases.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases \ --a=1:'\"/' \ --a-range=h:1:0-31 \ --b=1:abc \ --b-range=h:1:127-255 \ --a-is-important \ --length=5-20 \ --num-files=10 \ --random-seed=123 ================================================ FILE: barlibs/i3/fuzz_esc_json/harness.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include "libls/ls_string.h" #include "libsafe/safev.h" #include "fuzz_utils/fuzz_utils.h" #include "../escape_json_str.h" int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "USAGE: harness INPUT_FILE\n"); return 2; } int fd_in = open(argv[1], O_RDONLY | O_CLOEXEC); if (fd_in < 0) { perror(argv[1]); abort(); } FuzzInput input = fuzz_input_new_prealloc(1024); if (fuzz_input_read(fd_in, &input) < 0) { perror("read"); abort(); } LS_String res = ls_string_new_from_s("escape result = "); append_json_escaped_str(&res, SAFEV_new_UNSAFE(input.data, input.size)); fuzz_utils_used(res.data, res.size); fuzz_input_free(input); ls_string_free(res); close(fd_in); return 0; } ================================================ FILE: barlibs/i3/fuzz_esc_json/testcases/testcase_000 ================================================ \"//\ ================================================ FILE: barlibs/i3/fuzz_esc_json/testcases/testcase_001 ================================================ ""\ ================================================ FILE: barlibs/i3/fuzz_esc_json/testcases/testcase_003 ================================================ /\ ================================================ FILE: barlibs/i3/fuzz_esc_json/testcases/testcase_004 ================================================ "/cb\"ac"b\ ================================================ FILE: barlibs/i3/fuzz_esc_json/testcases/testcase_006 ================================================ ba a߫" ================================================ FILE: barlibs/i3/fuzz_esc_json/testcases/testcase_007 ================================================ c"bcc ================================================ FILE: barlibs/i3/fuzz_esc_json/testcases/testcase_008 ================================================ cbcȂbab\ ================================================ FILE: barlibs/i3/fuzz_esc_json/testcases/testcase_009 ================================================ cbƐbccb ================================================ FILE: barlibs/i3/fuzz_esc_pango/.gitignore ================================================ harness findings ================================================ FILE: barlibs/i3/fuzz_esc_pango/build.sh ================================================ #!/bin/sh if [ -z "$CC" ]; then echo >&2 "You must set the 'CC' environment variable." echo >&2 "Hint: you probably want to set 'CC' to 'some-directory/afl-gcc'." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. $CC -Wall -Wextra -O3 -fsanitize=undefined -std=c99 -D_POSIX_C_SOURCE=200809L \ -I"$luastatus_root" \ ./harness.c \ ../pango_escape.c \ "$luastatus_root"/libls/ls_string.c \ "$luastatus_root"/libls/ls_alloc_utils.c \ "$luastatus_root"/libls/ls_panic.c \ "$luastatus_root"/libls/ls_cstring_utils.c \ "$luastatus_root"/libsafe/*.c \ -o harness ================================================ FILE: barlibs/i3/fuzz_esc_pango/clear.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" rm -rf ./findings ================================================ FILE: barlibs/i3/fuzz_esc_pango/fuzz.sh ================================================ #!/bin/sh set -e if [ -z "$XXX_AFL_DIR" ]; then echo >&2 "You must set the 'XXX_AFL_DIR' environment variable." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" mkdir -p ./findings export UBSAN_OPTIONS=halt_on_error=1 export AFL_EXIT_WHEN_DONE=1 "$XXX_AFL_DIR"/afl-fuzz -i testcases -o findings -t 5 ./harness @@ ================================================ FILE: barlibs/i3/fuzz_esc_pango/gen_testcases.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases \ --a=1:'&<>\"' \ --b=1:abc \ --a-is-important \ --length=5-20 \ --num-files=10 \ --random-seed=123 ================================================ FILE: barlibs/i3/fuzz_esc_pango/harness.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include "libls/ls_string.h" #include "libsafe/safev.h" #include "fuzz_utils/fuzz_utils.h" #include "../pango_escape.h" static void append_to_ls_string(void *ud, SAFEV segment) { LS_String *dst = ud; ls_string_append_b(dst, SAFEV_ptr_UNSAFE(segment), SAFEV_len(segment)); } int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "USAGE: harness INPUT_FILE\n"); return 2; } int fd_in = open(argv[1], O_RDONLY | O_CLOEXEC); if (fd_in < 0) { perror(argv[1]); abort(); } FuzzInput input = fuzz_input_new_prealloc(1024); if (fuzz_input_read(fd_in, &input) < 0) { perror("read"); abort(); } LS_String res = ls_string_new_from_s("pango escape result = "); pango_escape( SAFEV_new_UNSAFE(input.data, input.size), append_to_ls_string, &res); fuzz_utils_used(res.data, res.size); fuzz_input_free(input); ls_string_free(res); close(fd_in); return 0; } ================================================ FILE: barlibs/i3/fuzz_esc_pango/testcases/testcase_000 ================================================ &<>\"& ================================================ FILE: barlibs/i3/fuzz_esc_pango/testcases/testcase_001 ================================================ >&&<&\>&b\"">""c\ ================================================ FILE: barlibs/i3/fuzz_esc_pango/testcases/testcase_002 ================================================ >c>>\&\c"\a& ================================================ FILE: barlibs/i3/fuzz_esc_pango/testcases/testcase_003 ================================================ &<\<cbb""<\< ================================================ FILE: barlibs/i3/fuzz_esc_pango/testcases/testcase_004 ================================================ ">&aca""><"bb>"baa& ================================================ FILE: barlibs/i3/fuzz_esc_pango/testcases/testcase_005 ================================================ &\bcbbb>b\>cc>&ab ================================================ FILE: barlibs/i3/fuzz_esc_pango/testcases/testcase_006 ================================================ acb&\ab\a ================================================ FILE: barlibs/i3/fuzz_esc_pango/testcases/testcase_007 ================================================ ac>bc ================================================ FILE: barlibs/i3/fuzz_esc_pango/testcases/testcase_008 ================================================ bbccba< ================================================ FILE: barlibs/i3/fuzz_esc_pango/testcases/testcase_009 ================================================ ababbaabcbbb ================================================ FILE: barlibs/i3/i3.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include #include "include/barlib_v1.h" #include "include/sayf_macros.h" #include "libls/ls_string.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_parse_int.h" #include "libls/ls_cstring_utils.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_io_utils.h" #include "libls/ls_lua_compat.h" #include "libsafe/safev.h" #include "priv.h" #include "event_watcher.h" #include "escape_json_str.h" #include "pango_escape.h" static void destroy(LuastatusBarlibData *bd) { Priv *p = bd->priv; for (size_t i = 0; i < p->nwidgets; ++i) { ls_string_free(p->bufs[i]); } free(p->bufs); ls_string_free(p->tmpbuf); ls_close(p->in_fd); if (p->out) { fclose(p->out); } free(p); } static int init(LuastatusBarlibData *bd, const char *const *opts, size_t nwidgets) { Priv *p = bd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .nwidgets = nwidgets, .bufs = LS_XNEW(LS_String, nwidgets), .tmpbuf = ls_string_new_reserve(1024), .in_fd = -1, .out = NULL, .noclickev = false, .noseps = false, }; for (size_t i = 0; i < nwidgets; ++i) p->bufs[i] = ls_string_new_reserve(1024); // All the options may be passed multiple times! int in_fd = -1; int out_fd = -1; const char *extra_init_json = NULL; bool allow_stopping = false; for (const char *const *s = opts; *s; ++s) { const char *v; if ((v = ls_strfollow(*s, "in_fd="))) { if ((in_fd = ls_full_strtou(v)) < 0) { LS_FATALF(bd, "in_fd value is not a valid unsigned integer"); goto error; } } else if ((v = ls_strfollow(*s, "out_fd="))) { if ((out_fd = ls_full_strtou(v)) < 0) { LS_FATALF(bd, "out_fd value is not a valid unsigned integer"); goto error; } } else if (strcmp(*s, "no_click_events") == 0) { p->noclickev = true; } else if (strcmp(*s, "no_separators") == 0) { p->noseps = true; } else if (strcmp(*s, "allow_stopping") == 0) { allow_stopping = true; } else if ((v = ls_strfollow(*s, "extra_init_json="))) { extra_init_json = v; } else { LS_FATALF(bd, "unknown option '%s'", *s); goto error; } } // we require /in_fd/ and /out_fd/ to >=3 because making stdin/stdout/stderr CLOEXEC has very // bad consequences, and we just don't want to complicate the logic. if (in_fd < 3) { LS_FATALF(bd, "in_fd is not specified or less than 3"); goto error; } if (out_fd < 3) { LS_FATALF(bd, "out_fd is not specified or less than 3"); goto error; } // assign p->in_fd = in_fd; if (!(p->out = fdopen(out_fd, "w"))) { LS_FATALF(bd, "can't fdopen %d: %s", out_fd, ls_tls_strerror(errno)); goto error; } // make CLOEXEC if (ls_make_cloexec(in_fd) < 0) { LS_FATALF(bd, "can't make fd %d CLOEXEC: %s", in_fd, ls_tls_strerror(errno)); goto error; } if (ls_make_cloexec(out_fd) < 0) { LS_FATALF(bd, "can't make fd %d CLOEXEC: %s", out_fd, ls_tls_strerror(errno)); goto error; } // print header fprintf(p->out, "{\"version\":1,\"click_events\":%s", p->noclickev ? "false" : "true"); if (extra_init_json && extra_init_json[0]) { fprintf(p->out, ",%s", extra_init_json); } if (!allow_stopping) { fprintf(p->out, ",\"stop_signal\":0,\"cont_signal\":0"); } fprintf(p->out, "}\n[\n"); fflush(p->out); if (ferror(p->out)) { LS_FATALF(bd, "write error: %s", ls_tls_strerror(errno)); goto error; } return LUASTATUS_OK; error: destroy(bd); return LUASTATUS_ERR; } static bool redraw(LuastatusBarlibData *bd) { Priv *p = bd->priv; FILE *out = p->out; size_t n = p->nwidgets; LS_String *bufs = p->bufs; putc('[', out); bool first = true; for (size_t i = 0; i < n; ++i) { if (bufs[i].size) { if (!first) { putc(',', out); } fwrite(bufs[i].data, 1, bufs[i].size, out); first = false; } } fputs("],\n", out); fflush(out); if (ferror(out)) { LS_FATALF(bd, "write error: %s", ls_tls_strerror(errno)); return false; } return true; } static void append_to_lua_buf(void *ud, SAFEV segment) { luaL_Buffer *b = ud; luaL_addlstring(b, SAFEV_ptr_UNSAFE(segment), SAFEV_len(segment)); } static int l_pango_escape(lua_State *L) { size_t ns; // WARNING: luaL_check*() functions do a long jump on error! const char *s = luaL_checklstring(L, 1, &ns); luaL_Buffer b; luaL_buffinit(L, &b); SAFEV v = SAFEV_new_UNSAFE(s, ns); pango_escape(v, append_to_lua_buf, &b); luaL_pushresult(&b); return 1; } static void register_funcs(LuastatusBarlibData *bd, lua_State *L) { (void) bd; // L: table lua_pushcfunction(L, l_pango_escape); // L: table l_pango_escape lua_setfield(L, -2, "pango_escape"); // L: table } static inline bool append_json_number(LS_String *s, double value) { if (!isfinite(value)) { return false; } ls_string_append_f(s, "%.20g", value); return true; } // Appends a JSON segment generated from table at the top of /L/'s stack, to // /((Priv *) bd->priv)->tmpbuf/. static bool append_segment(LuastatusBarlibData *bd, lua_State *L, size_t widget_idx) { Priv *p = bd->priv; LS_String *dst = &p->tmpbuf; // add a "prologue" if (dst->size) { ls_string_append_c(dst, ','); } ls_string_append_f(dst, "{\"name\":\"%zu\"", widget_idx); bool has_separator_key = false; // L: ? table lua_pushnil(L); // L: ? table nil while (lua_next(L, -2)) { // L: ? table key value if (!lua_isstring(L, -2)) { LS_ERRF(bd, "segment key: expected string, found %s", luaL_typename(L, -2)); return false; } const char *key = lua_tostring(L, -2); if (strcmp(key, "name") == 0) { LS_WARNF(bd, "segment: ignoring 'name', it is set automatically; use 'instance' " "instead"); goto next_entry; } if (strcmp(key, "separator") == 0) { has_separator_key = true; } ls_string_append_c(dst, ','); append_json_escaped_str(dst, SAFEV_new_from_cstr_UNSAFE(key)); ls_string_append_c(dst, ':'); switch (lua_type(L, -1)) { case LUA_TNUMBER: { double val = lua_tonumber(L, -1); if (!append_json_number(dst, val)) { LS_ERRF(bd, "segment entry '%s': invalid number (NaN/Inf)", key); return false; } } break; case LUA_TSTRING: { size_t ns; const char *s = lua_tolstring(L, -1, &ns); append_json_escaped_str(dst, SAFEV_new_UNSAFE(s, ns)); } break; case LUA_TBOOLEAN: { bool val = lua_toboolean(L, -1); ls_string_append_s(dst, val ? "true" : "false"); } break; case LUA_TNIL: ls_string_append_s(dst, "null"); break; default: LS_ERRF(bd, "segment entry '%s': expected string, number, boolean or nil, found %s", key, luaL_typename(L, -1)); return false; } next_entry: lua_pop(L, 1); // L: ? table key } // L: ? table // add an "epilogue" if (p->noseps && !has_separator_key) { ls_string_append_s(dst, ",\"separator\":false"); } ls_string_append_c(dst, '}'); return true; } typedef enum { TC_EMPTY, TC_ARRAY, TC_DICT, } TableClass; static inline TableClass classify_table(lua_State *L) { // L: ? table lua_pushnil(L); // L: ? table nil if (!lua_next(L, -2)) { // L: ? table return TC_EMPTY; } // L: ? table key value bool is_array = lua_isnumber(L, -2); lua_pop(L, 2); // L: ? table return is_array ? TC_ARRAY : TC_DICT; } static int set(LuastatusBarlibData *bd, lua_State *L, size_t widget_idx) { Priv *p = bd->priv; ls_string_clear(&p->tmpbuf); // L: ? data switch (lua_type(L, -1)) { case LUA_TNIL: break; case LUA_TTABLE: switch (classify_table(L)) { case TC_EMPTY: break; case TC_DICT: if (!append_segment(bd, L, widget_idx)) { goto invalid_data; } break; case TC_ARRAY: { size_t len = ls_lua_array_len(L, -1); for (size_t i = 1; i <= len; ++i) { lua_rawgeti(L, -1, i); // L: ? data value switch (lua_type(L, -1)) { case LUA_TTABLE: if (!append_segment(bd, L, widget_idx)) { goto invalid_data; } break; case LUA_TNIL: break; default: LS_ERRF(bd, "array value: expected table or nil, found %s", luaL_typename(L, -1)); goto invalid_data; } lua_pop(L, 1); // L: ? data } } } // L: ? data break; default: LS_ERRF(bd, "expected table or nil, found %s", luaL_typename(L, -1)); goto invalid_data; } if (!ls_string_eq(p->tmpbuf, p->bufs[widget_idx])) { ls_string_swap(&p->tmpbuf, &p->bufs[widget_idx]); if (!redraw(bd)) { return LUASTATUS_ERR; } } return LUASTATUS_OK; invalid_data: ls_string_clear(&p->bufs[widget_idx]); return LUASTATUS_NONFATAL_ERR; } static int set_error(LuastatusBarlibData *bd, size_t widget_idx) { Priv *p = bd->priv; LS_String *s = &p->bufs[widget_idx]; ls_string_assign_s( s, "{\"full_text\":\"(Error)\",\"color\":\"#ff0000\",\"background\":\"#000000\""); if (p->noseps) { ls_string_append_s(s, ",\"separator\":false"); } ls_string_append_c(s, '}'); if (!redraw(bd)) { return LUASTATUS_ERR; } return LUASTATUS_OK; } LuastatusBarlibIface luastatus_barlib_iface_v1 = { .init = init, .register_funcs = register_funcs, .set = set, .set_error = set_error, .event_watcher = event_watcher, .destroy = destroy, }; ================================================ FILE: barlibs/i3/luastatus-i3-wrapper ================================================ #!/bin/sh exec ${LUASTATUS:-luastatus} -b i3 -B in_fd=3 -B out_fd=4 "$@" 3<&0 0&1 1>&2 ================================================ FILE: barlibs/i3/pango_escape.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "pango_escape.h" #include #include "libsafe/safev.h" #define LIT(StrLit_) SAFEV_new_from_literal(StrLit_) void pango_escape( SAFEV v, void (*append)(void *ud, SAFEV segment), void *ud) { size_t n = SAFEV_len(v); size_t prev = 0; for (size_t i = 0; i < n; ++i) { SAFEV esc = {0}; switch (SAFEV_at(v, i)) { case '&': esc = LIT("&"); break; case '<': esc = LIT("<"); break; case '>': esc = LIT(">"); break; case '\'': esc = LIT("'"); break; case '"': esc = LIT("""); break; default: continue; } append(ud, SAFEV_subspan(v, prev, i)); append(ud, esc); prev = i + 1; } append(ud, SAFEV_suffix(v, prev)); } ================================================ FILE: barlibs/i3/pango_escape.h ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef pango_escape_h_ #define pango_escape_h_ #include "libsafe/safev.h" void pango_escape( SAFEV v, void (*append)(void *ud, SAFEV segment), void *ud); #endif ================================================ FILE: barlibs/i3/priv.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef priv_h_ #define priv_h_ #include #include #include #include "libls/ls_string.h" typedef struct { size_t nwidgets; LS_String *bufs; // Temporary buffer for secondary buffering, to avoid unneeded redraws. LS_String tmpbuf; // Input file descriptor. int in_fd; // /fdopen/'ed output file descritor. FILE *out; bool noclickev; bool noseps; } Priv; #endif ================================================ FILE: barlibs/lemonbar/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_barlib ( barlib-lemonbar $ $ ${sources} ) target_compile_definitions (barlib-lemonbar PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (barlib-lemonbar LUA) target_include_directories (barlib-lemonbar PUBLIC "${PROJECT_SOURCE_DIR}") include (GNUInstallDirs) install (PROGRAMS luastatus-lemonbar-launcher DESTINATION ${CMAKE_INSTALL_BINDIR}) luastatus_add_man_page (README.rst luastatus-barlib-lemonbar 7) ================================================ FILE: barlibs/lemonbar/README.rst ================================================ .. :X-man-page-only: luastatus-barlib-lemonbar .. :X-man-page-only: ######################### .. :X-man-page-only: .. :X-man-page-only: ############################# .. :X-man-page-only: lemonbar barlib for luastatus .. :X-man-page-only: ############################# .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This barlib talks with ``lemonbar``. It joins all non-empty strings returned by widgets by a separator, which defaults to ``" | "``. Redirections and ``luastatus-lemonbar-launcher`` ================================================ ``lemonbar`` is not capable of creating a bidirectional pipe itself; instead, it requires all the data to be written to its stdin and read from its stdout. That's why the input/output file descriptors of this barlib must be manually redirected. A launcher, ``luastatus-lemonbar-launcher``, is shipped with it; it spawns ``lemonbar`` connected to a bidirectional pipe and executes ``luastatus`` with ``-b lemonbar``, all the required ``-B`` options, and additional arguments passed by you. Pass each ``lemonbar`` argument with ``-p``, then pass ``--``, then pass luastatus arguments, e.g.:: luastatus-lemonbar-launcher -p -B#111111 -p -f'Droid Sans Mono for Powerline:pixelsize=12:weight=Bold' -- -Bseparator=' ' widget1.lua widget2.lua ``cb`` return value =================== Either of: * a string with lemonbar markup An empty string hides the widget. * an array of such strings Equivalent to returning a string with all non-empty elements of the array joined by the separator. * ``nil`` Hides the widget. Functions ========= The following functions are provided: * ``escaped_str = luastatus.barlib.escape(str)`` Escapes text for lemonbar markup. ``event`` argument ================== A string with the name of the command. Options ======= The following options are supported: * ``in_fd=`` File descriptor to read ``lemonbar`` input from. Usually set by the launcher. * ``out_fd=`` File descriptor to write to. Usually set by the launcher. * ``separator=`` Set the separator. ================================================ FILE: barlibs/lemonbar/fuzz_esc/.gitignore ================================================ harness findings ================================================ FILE: barlibs/lemonbar/fuzz_esc/build.sh ================================================ #!/bin/sh if [ -z "$CC" ]; then echo >&2 "You must set the 'CC' environment variable." echo >&2 "Hint: you probably want to set 'CC' to 'some-directory/afl-gcc'." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. $CC -Wall -Wextra -O3 -fsanitize=undefined -std=c99 -D_POSIX_C_SOURCE=200809L \ -I"$luastatus_root" \ ./harness.c \ ../markup_utils.c \ "$luastatus_root"/libls/ls_string.c \ "$luastatus_root"/libls/ls_alloc_utils.c \ "$luastatus_root"/libls/ls_panic.c \ "$luastatus_root"/libls/ls_cstring_utils.c \ "$luastatus_root"/libls/ls_parse_int.c \ "$luastatus_root"/libsafe/*.c \ -o harness ================================================ FILE: barlibs/lemonbar/fuzz_esc/clear.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" rm -rf ./findings ================================================ FILE: barlibs/lemonbar/fuzz_esc/fuzz.sh ================================================ #!/bin/sh set -e if [ -z "$XXX_AFL_DIR" ]; then echo >&2 "You must set the 'XXX_AFL_DIR' environment variable." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" mkdir -p ./findings export UBSAN_OPTIONS=halt_on_error=1 export AFL_EXIT_WHEN_DONE=1 "$XXX_AFL_DIR"/afl-fuzz -i testcases -o findings -t 5 ./harness @@ ================================================ FILE: barlibs/lemonbar/fuzz_esc/gen_testcases.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases \ --a=1:abc \ --b=1:'%' \ --length=5-20 \ --num-files=10 \ --random-seed=123 ================================================ FILE: barlibs/lemonbar/fuzz_esc/harness.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include "libls/ls_string.h" #include "libsafe/safev.h" #include "fuzz_utils/fuzz_utils.h" #include "../markup_utils.h" static void append_to_ls_string(void *ud, SAFEV segment) { LS_String *dst = ud; ls_string_append_b(dst, SAFEV_ptr_UNSAFE(segment), SAFEV_len(segment)); } int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "USAGE: harness INPUT_FILE\n"); return 2; } int fd_in = open(argv[1], O_RDONLY | O_CLOEXEC); if (fd_in < 0) { perror(argv[1]); abort(); } FuzzInput input = fuzz_input_new_prealloc(1024); if (fuzz_input_read(fd_in, &input) < 0) { perror("read"); abort(); } LS_String res = ls_string_new_from_s("escape result = "); escape( append_to_ls_string, &res, SAFEV_new_UNSAFE(input.data, input.size)); fuzz_utils_used(res.data, res.size); fuzz_input_free(input); ls_string_free(res); close(fd_in); return 0; } ================================================ FILE: barlibs/lemonbar/fuzz_esc/testcases/testcase_000 ================================================ acabca ================================================ FILE: barlibs/lemonbar/fuzz_esc/testcases/testcase_001 ================================================ abbc%cccbbacacb%cc ================================================ FILE: barlibs/lemonbar/fuzz_esc/testcases/testcase_002 ================================================ ccacb%a%%aacbcc ================================================ FILE: barlibs/lemonbar/fuzz_esc/testcases/testcase_003 ================================================ %c%abb%%%ccb%abcb ================================================ FILE: barlibs/lemonbar/fuzz_esc/testcases/testcase_004 ================================================ %a%ca%b% ================================================ FILE: barlibs/lemonbar/fuzz_esc/testcases/testcase_005 ================================================ %a%cbc%%% ================================================ FILE: barlibs/lemonbar/fuzz_esc/testcases/testcase_006 ================================================ %b%b%b%%c%%%%b% ================================================ FILE: barlibs/lemonbar/fuzz_esc/testcases/testcase_007 ================================================ %%%%%%%%bb ================================================ FILE: barlibs/lemonbar/fuzz_esc/testcases/testcase_008 ================================================ %%%%b%%%%%%%%%c% ================================================ FILE: barlibs/lemonbar/fuzz_esc/testcases/testcase_009 ================================================ %%%%%% ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/.gitignore ================================================ harness findings ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/build.sh ================================================ #!/bin/sh if [ -z "$CC" ]; then echo >&2 "You must set the 'CC' environment variable." echo >&2 "Hint: you probably want to set 'CC' to 'some-directory/afl-gcc'." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. $CC -Wall -Wextra -O3 -fsanitize=undefined -std=c99 -D_POSIX_C_SOURCE=200809L \ -I"$luastatus_root" \ ./harness.c \ ../markup_utils.c \ "$luastatus_root"/libls/ls_string.c \ "$luastatus_root"/libls/ls_alloc_utils.c \ "$luastatus_root"/libls/ls_panic.c \ "$luastatus_root"/libls/ls_cstring_utils.c \ "$luastatus_root"/libls/ls_parse_int.c \ "$luastatus_root"/libsafe/*.c \ -o harness ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/clear.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" rm -rf ./findings ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/fuzz.sh ================================================ #!/bin/sh set -e if [ -z "$XXX_AFL_DIR" ]; then echo >&2 "You must set the 'XXX_AFL_DIR' environment variable." exit 1 fi # We add '-d' argument for original AFL because otherwise it # does not terminate within 24 hours. # # '-d' means "quick & dirty mode (skips deterministic steps)". case "$XXX_AFL_IS_PP" in 0) extra_opts='-d' ;; 1) extra_opts= ;; *) echo >&2 "You must set 'XXX_AFL_IS_PP' environment variable to either 0 or 1." echo >&2 "Set it to 0 if AFL is the original Google's version (not AFL++)." echo >&2 "Set it to 1 if AFL is actually AFL++." exit 1 esac # We also set AFL_NO_ARITH=1 because it's a text-based format. # This potentially speeds up fuzzing. export AFL_NO_ARITH=1 cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" mkdir -p ./findings export UBSAN_OPTIONS=halt_on_error=1 export AFL_EXIT_WHEN_DONE=1 "$XXX_AFL_DIR"/afl-fuzz $extra_opts -i testcases -o findings -t 5 ./harness @@ ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/gen_testcases.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. nl=$(printf '\nx') nl=${nl%x} # 'fps' means 'final percent sign' "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases \ --file-prefix='X_' \ --a=1:x \ --b=1:x \ --mut-substrings="|${nl}|}|%{A|%{x|%x|%%" \ --length=10 \ --num-files=6 \ --random-seed=123 \ --extra-testcase='fps:x%' "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases \ --file-prefix='Y_' \ --a=1:x \ --b=1:x \ --mut-prefix='%{A' \ --mut-substrings='|:' \ --length=10 \ --num-files=4 \ --random-seed=123 "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases \ --file-prefix='Z_' \ --a=1:x \ --b=1:x \ --mut-prefix='x%{A' \ --mut-substrings='|:' \ --length=10 \ --num-files=5 \ --random-seed=123 ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/harness.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include "libls/ls_string.h" #include "libsafe/safev.h" #include "fuzz_utils/fuzz_utils.h" #include "../markup_utils.h" int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "USAGE: harness INPUT_FILE\n"); return 2; } int fd_in = open(argv[1], O_RDONLY | O_CLOEXEC); if (fd_in < 0) { perror(argv[1]); abort(); } FuzzInput input = fuzz_input_new_prealloc(1024); if (fuzz_input_read(fd_in, &input) < 0) { perror("read"); abort(); } LS_String res = ls_string_new_reserve(2048); ls_string_append_s(&res, "sanitize result = "); append_sanitized( &res, 123, SAFEV_new_UNSAFE(input.data, input.size)); fuzz_utils_used(res.data, res.size); fuzz_input_free(input); ls_string_free(res); close(fd_in); return 0; } ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/X_testcase_000 ================================================ xxxx xxxxxx ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/X_testcase_001 ================================================ xxxxxxx}xxx ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/X_testcase_002 ================================================ xxxxxxxxxx%{A ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/X_testcase_003 ================================================ xxxxx%{xxxxxx ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/X_testcase_004 ================================================ xxxxxxxx%xxx ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/X_testcase_005 ================================================ xxxxxxxxx%%x ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/X_testcase_fps ================================================ x% ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/Y_testcase_000 ================================================ xxxx:xxxxxx ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/Y_testcase_001 ================================================ %xxxxxxx:xxx ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/Y_testcase_002 ================================================ %{xxxxxxxxxx: ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/Y_testcase_003 ================================================ %{Axxxxx:xxxxx ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/Z_testcase_000 ================================================ xxxx:xxxxxx ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/Z_testcase_001 ================================================ xxxxxxxx:xxx ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/Z_testcase_002 ================================================ x%xxxxxxxxxx: ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/Z_testcase_003 ================================================ x%{xxxxx:xxxxx ================================================ FILE: barlibs/lemonbar/fuzz_sanitize/testcases/Z_testcase_004 ================================================ x%{Axxxxxxxx:xx ================================================ FILE: barlibs/lemonbar/lemonbar.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include #include "include/barlib_v1.h" #include "include/sayf_macros.h" #include "libls/ls_string.h" #include "libls/ls_cstring_utils.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_parse_int.h" #include "libls/ls_io_utils.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_lua_compat.h" #include "libsafe/safev.h" #include "markup_utils.h" typedef struct { size_t nwidgets; LS_String *bufs; // Temporary buffer for secondary buffering, to avoid unneeded redraws. LS_String tmpbuf; char *sep; // /fdopen/'ed input file descriptor. FILE *in; // /fdopen/'ed output file descriptor. FILE *out; } Priv; static void destroy(LuastatusBarlibData *bd) { Priv *p = bd->priv; for (size_t i = 0; i < p->nwidgets; ++i) ls_string_free(p->bufs[i]); free(p->bufs); ls_string_free(p->tmpbuf); free(p->sep); if (p->in) fclose(p->in); if (p->out) fclose(p->out); free(p); } static int init(LuastatusBarlibData *bd, const char *const *opts, size_t nwidgets) { Priv *p = bd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .nwidgets = nwidgets, .bufs = LS_XNEW(LS_String, nwidgets), .tmpbuf = ls_string_new_reserve(512), .sep = NULL, .in = NULL, .out = NULL, }; for (size_t i = 0; i < nwidgets; ++i) p->bufs[i] = ls_string_new_reserve(512); // All the options may be passed multiple times! const char *sep = NULL; int in_fd = -1; int out_fd = -1; for (const char *const *s = opts; *s; ++s) { const char *v; if ((v = ls_strfollow(*s, "in_fd="))) { if ((in_fd = ls_full_strtou(v)) < 0) { LS_FATALF(bd, "in_fd value is not a valid unsigned integer"); goto error; } } else if ((v = ls_strfollow(*s, "out_fd="))) { if ((out_fd = ls_full_strtou(v)) < 0) { LS_FATALF(bd, "out_fd value is not a valid unsigned integer"); goto error; } } else if ((v = ls_strfollow(*s, "separator="))) { sep = v; } else { LS_FATALF(bd, "unknown option '%s'", *s); goto error; } } p->sep = ls_xstrdup(sep ? sep : " | "); // we require /in_fd/ and /out_fd/ to be >=3 because making stdin/stdout/stderr CLOEXEC has very // bad consequences, and we just don't want to complicate the logic. if (in_fd < 3) { LS_FATALF(bd, "in_fd is not specified or less than 3"); goto error; } if (out_fd < 3) { LS_FATALF(bd, "out_fd is not specified or less than 3"); goto error; } // open if (!(p->in = fdopen(in_fd, "r"))) { LS_FATALF(bd, "can't fdopen %d: %s", in_fd, ls_tls_strerror(errno)); goto error; } if (!(p->out = fdopen(out_fd, "w"))) { LS_FATALF(bd, "can't fdopen %d: %s", out_fd, ls_tls_strerror(errno)); goto error; } // make CLOEXEC if (ls_make_cloexec(in_fd) < 0) { LS_FATALF(bd, "can't make fd %d CLOEXEC: %s", in_fd, ls_tls_strerror(errno)); goto error; } if (ls_make_cloexec(out_fd) < 0) { LS_FATALF(bd, "can't make fd %d CLOEXEC: %s", out_fd, ls_tls_strerror(errno)); goto error; } return LUASTATUS_OK; error: destroy(bd); return LUASTATUS_ERR; } static void append_to_lua_buf_callback(void *ud, SAFEV v) { luaL_Buffer *b = ud; luaL_addlstring(b, SAFEV_ptr_UNSAFE(v), SAFEV_len(v)); } static int l_escape(lua_State *L) { size_t ns; // WARNING: /luaL_check*()/ functions do a long jump on error! const char *s = luaL_checklstring(L, 1, &ns); luaL_Buffer b; luaL_buffinit(L, &b); escape(append_to_lua_buf_callback, &b, SAFEV_new_UNSAFE(s, ns)); luaL_pushresult(&b); // L: result return 1; } static void register_funcs(LuastatusBarlibData *bd, lua_State *L) { (void) bd; // L: table lua_pushcfunction(L, l_escape); // L: table l_escape lua_setfield(L, -2, "escape"); // L: table } static bool redraw(LuastatusBarlibData *bd) { Priv *p = bd->priv; FILE *out = p->out; size_t n = p->nwidgets; LS_String *bufs = p->bufs; const char *sep = p->sep; bool first = true; for (size_t i = 0; i < n; ++i) { if (bufs[i].size) { if (!first) { fputs(sep, out); } fwrite(bufs[i].data, 1, bufs[i].size, out); first = false; } } putc('\n', out); fflush(out); if (ferror(out)) { LS_FATALF(bd, "write error: %s", ls_tls_strerror(errno)); return false; } return true; } static int set(LuastatusBarlibData *bd, lua_State *L, size_t widget_idx) { Priv *p = bd->priv; LS_String *buf = &p->tmpbuf; ls_string_clear(buf); // L: ? data switch (lua_type(L, -1)) { case LUA_TNIL: break; case LUA_TSTRING: { size_t ns; const char *s = lua_tolstring(L, -1, &ns); append_sanitized(buf, widget_idx, SAFEV_new_UNSAFE(s, ns)); } break; case LUA_TTABLE: { const char *sep = p->sep; size_t len = ls_lua_array_len(L, -1); for (size_t i = 1; i <= len; ++i) { lua_rawgeti(L, -1, i); // L: ? data value if (lua_isnil(L, -1)) { goto next; } if (!lua_isstring(L, -1)) { LS_ERRF(bd, "table value: expected string, found %s", luaL_typename(L, -1)); goto invalid_data; } size_t ns; const char *s = lua_tolstring(L, -1, &ns); if (buf->size && ns) { ls_string_append_s(buf, sep); } append_sanitized(buf, widget_idx, SAFEV_new_UNSAFE(s, ns)); next: lua_pop(L, 1); // L: ? data } // L: ? data } break; default: LS_ERRF(bd, "expected string, table or nil, found %s", luaL_typename(L, -1)); goto invalid_data; } if (!ls_string_eq(*buf, p->bufs[widget_idx])) { ls_string_swap(buf, &p->bufs[widget_idx]); if (!redraw(bd)) { return LUASTATUS_ERR; } } return LUASTATUS_OK; invalid_data: ls_string_clear(&p->bufs[widget_idx]); return LUASTATUS_NONFATAL_ERR; } static int set_error(LuastatusBarlibData *bd, size_t widget_idx) { Priv *p = bd->priv; ls_string_assign_s(&p->bufs[widget_idx], "%{B#f00}%{F#fff}(Error)%{B-}%{F-}"); if (!redraw(bd)) { return LUASTATUS_ERR; } return LUASTATUS_OK; } static int event_watcher(LuastatusBarlibData *bd, LuastatusBarlibEWFuncs funcs) { Priv *p = bd->priv; char *buf = NULL; size_t nbuf = 256; for (ssize_t nread; (nread = getline(&buf, &nbuf, p->in)) >= 0;) { if (nread == 0 || buf[nread - 1] != '\n') continue; size_t ncommand; size_t widget_idx; const char *command = parse_command(buf, nread - 1, &ncommand, &widget_idx); if (!command) continue; if (widget_idx >= p->nwidgets) { continue; } lua_State *L = funcs.call_begin(bd->userdata, widget_idx); lua_pushlstring(L, command, ncommand); funcs.call_end(bd->userdata, widget_idx); } if (feof(p->in)) { LS_ERRF(bd, "lemonbar closed its pipe end"); } else { LS_ERRF(bd, "read error: %s", ls_tls_strerror(errno)); } free(buf); return LUASTATUS_ERR; } LuastatusBarlibIface luastatus_barlib_iface_v1 = { .init = init, .register_funcs = register_funcs, .set = set, .set_error = set_error, .event_watcher = event_watcher, .destroy = destroy, }; ================================================ FILE: barlibs/lemonbar/luastatus-lemonbar-launcher ================================================ #!/usr/bin/env bash set -e if (( BASH_VERSINFO[0] < 4 )); then echo >&2 "bash 4.0 or higher is required (this one is $BASH_VERSION)." exit 1 fi usage() { printf '%s\n' >&2 "$* USAGE: ${0##*/} [-p lemonbar_argument [-p ...]] -- [luastatus_argument [...]] Note that '--' is mandatory." exit 2 } lemonbar_args=() # There is no easy way to require '--' with getopts, so we parse $@ manually. while true; do if (( $# == 0 )); then usage "'--' argument not found." fi case "$1" in --) shift break ;; -p) if (( $# == 1 )); then usage "'-p' option requires an argument." fi shift lemonbar_args+=("$1") ;; -p*) lemonbar_args+=("${1:2}") ;; *) usage "Unexpected argument '$1' found before '--'." ;; esac shift done coproc ${LEMONBAR:-lemonbar} "${lemonbar_args[@]}" exec <&${COPROC[0]} >&${COPROC[1]} exec ${LUASTATUS:-luastatus} -b lemonbar -B in_fd=3 -B out_fd=4 "$@" 3<&0 0&1 1>&2 ================================================ FILE: barlibs/lemonbar/markup_utils.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "markup_utils.h" #include "libls/ls_string.h" #include "libls/ls_parse_int.h" #include "libsafe/safev.h" #include #include void escape( void (*append)(void *ud, SAFEV segment), void *ud, SAFEV v) { // just replace all "%"s with "%%" for (;;) { size_t i = SAFEV_index_of(v, '%'); if (i == (size_t) -1) { break; } append(ud, SAFEV_subspan(v, 0, i)); append(ud, SAFEV_new_from_literal("%%")); v = SAFEV_suffix(v, i + 1); } append(ud, v); } static inline void append_sv(LS_String *dst, SAFEV v) { ls_string_append_b(dst, SAFEV_ptr_UNSAFE(v), SAFEV_len(v)); } void append_sanitized(LS_String *buf, size_t widget_idx, SAFEV v) { size_t n = SAFEV_len(v); size_t prev = 0; bool a_tag = false; for (size_t i = 0; i < n; ++i) { #define DO_PREV(WhetherToIncludeThis_) \ do { \ size_t j__ = i + ((WhetherToIncludeThis_) ? 1 : 0); \ append_sv(buf, SAFEV_subspan(v, prev, j__)); \ prev = i + 1; \ } while (0) #define PEEK(Offset_) SAFEV_at_or(v, i + (Offset_), '\0') switch (SAFEV_at(v, i)) { case '\n': DO_PREV(false); break; case '%': if (PEEK(1) == '{' && PEEK(2) == 'A') { a_tag = true; } else if (PEEK(1) == '%') { ++i; } break; case ':': if (a_tag) { DO_PREV(true); ls_string_append_f(buf, "%zu_", widget_idx); a_tag = false; } break; case '}': a_tag = false; break; } #undef DO_PREV #undef PEEK } append_sv(buf, SAFEV_suffix(v, prev)); } const char *parse_command(const char *line, size_t nline, size_t *ncommand, size_t *widget_idx) { const char *endptr; int idx = ls_strtou_b(line, nline, &endptr); if (idx < 0 || endptr == line || endptr == line + nline || *endptr != '_') { return NULL; } const char *command = endptr + 1; *ncommand = nline - (command - line); *widget_idx = idx; return command; } ================================================ FILE: barlibs/lemonbar/markup_utils.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef markup_utils_h_ #define markup_utils_h_ #include #include "libls/ls_string.h" #include "libsafe/safev.h" void escape( void (*append)(void *ud, SAFEV segment), void *ud, SAFEV v); void append_sanitized(LS_String *buf, size_t widget_idx, SAFEV v); const char *parse_command(const char *line, size_t nline, size_t *ncommand, size_t *widget_idx); #endif ================================================ FILE: barlibs/stdout/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_barlib ( barlib-stdout $ $ ${sources} ) target_compile_definitions (barlib-stdout PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (barlib-stdout LUA) target_include_directories (barlib-stdout PUBLIC "${PROJECT_SOURCE_DIR}") include (GNUInstallDirs) install (PROGRAMS luastatus-stdout-wrapper DESTINATION ${CMAKE_INSTALL_BINDIR}) luastatus_add_man_page (README.rst luastatus-barlib-stdout 7) ================================================ FILE: barlibs/stdout/README.rst ================================================ .. :X-man-page-only: luastatus-barlib-stdout .. :X-man-page-only: ####################### .. :X-man-page-only: .. :X-man-page-only: ########################### .. :X-man-page-only: stdout barlib for luastatus .. :X-man-page-only: ########################### .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This barlib simply writes lines to a file descriptor. It can be used for status bars such as **dzen**/**dzen2**, **xmobar**, **yabar**, **dvtm**, and others. It joins all non-empty strings returned by widgets by a separator, which defaults to ``" | "``. It does not provide functions. Redirections and ``luastatus-stdout-wrapper`` ============================================= Since we need to write to stdout, it is very easy to mess things up: Lua's ``print()`` prints to stdout, processes spawned by widgets/plugins inherit our stdin and stdout, etc. That's why this barlib requires that stdout file descriptor is manually redirected. A shell wrapper, ``luastatus-stdout-wrapper``, is shipped with it; it does all the redirections needed and executes ``luastatus`` with ``-b stdout`` and additional arguments passed by you. It does not redirect stdin and/or pass ``in_fd=`` option. Make your own wrapper if this is needed. ``cb`` return value =================== Either of: * a string An empty string hides the widget. * an array of strings Equivalent to returning a string with all non-empty elements of the array joined by the separator. * ``nil`` Hides the widget. Options ======= The following options are supported: * ``out_fd=`` File descriptor to write to. Usually set by the wrapper. * ``separator=`` Set the separator. * ``error=`` Set the content of an "error" segment. Defaults to ``"(Error)"``. * ``in_filename=`` or ``in_fd=`` Enable event watcher. If ``in_filename=`` is specified, this barlib will open the specified filename for reading and then read from. If ``in_fd=`` is specified, the specified file descriptor will be read from. It is invalid to pass both options; in this case, this barlib will fail to initialize. This barilib will then read lines from the specified source. Each line will be treated as an event. This barlib doesn't try to interpret the content of the line; instead, the event will be broadcast to all widgets. Thus, if this option is used, it is the responsibility of each widget (that listens to the events) to check if the event is somehow related to it. ``event`` argument ================== Events are only reported if either ``in_filename=`` or ``in_fd=`` option was specified (see above). In this case, the argument is the line that was read from the specified file, without the trailing newline character. ================================================ FILE: barlibs/stdout/fuzz/.gitignore ================================================ harness findings ================================================ FILE: barlibs/stdout/fuzz/build.sh ================================================ #!/bin/sh if [ -z "$CC" ]; then echo >&2 "You must set the 'CC' environment variable." echo >&2 "Hint: you probably want to set 'CC' to 'some-directory/afl-gcc'." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. $CC -Wall -Wextra -O3 -fsanitize=undefined -std=c99 -D_POSIX_C_SOURCE=200809L \ -I"$luastatus_root" \ ./harness.c \ ../sanitize.c \ "$luastatus_root"/libls/ls_string.c \ "$luastatus_root"/libls/ls_alloc_utils.c \ "$luastatus_root"/libls/ls_panic.c \ "$luastatus_root"/libls/ls_cstring_utils.c \ "$luastatus_root"/libsafe/*.c \ -o harness ================================================ FILE: barlibs/stdout/fuzz/clear.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" rm -rf ./findings ================================================ FILE: barlibs/stdout/fuzz/fuzz.sh ================================================ #!/bin/sh set -e if [ -z "$XXX_AFL_DIR" ]; then echo >&2 "You must set the 'XXX_AFL_DIR' environment variable." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" mkdir -p ./findings export UBSAN_OPTIONS=halt_on_error=1 export AFL_EXIT_WHEN_DONE=1 "$XXX_AFL_DIR"/afl-fuzz -i testcases -o findings -t 5 ./harness @@ ================================================ FILE: barlibs/stdout/fuzz/gen_testcases.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. nl=$(printf '\nx') nl=${nl%x} "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases \ --a=1:"$nl" \ --b=1:xyz \ --a-is-important \ --length=5-20 \ --num-files=10 \ --random-seed=123 ================================================ FILE: barlibs/stdout/fuzz/harness.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include "libls/ls_string.h" #include "libsafe/safev.h" #include "fuzz_utils/fuzz_utils.h" #include "../sanitize.h" int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "USAGE: harness INPUT_FILE\n"); return 2; } int fd_in = open(argv[1], O_RDONLY | O_CLOEXEC); if (fd_in < 0) { perror(argv[1]); abort(); } FuzzInput input = fuzz_input_new_prealloc(1024); if (fuzz_input_read(fd_in, &input) < 0) { perror("read"); abort(); } LS_String res = ls_string_new_from_s("sanitize result = "); append_sanitized(&res, SAFEV_new_UNSAFE(input.data, input.size)); fuzz_utils_used(res.data, res.size); fuzz_input_free(input); ls_string_free(res); close(fd_in); return 0; } ================================================ FILE: barlibs/stdout/fuzz/testcases/testcase_000 ================================================ ================================================ FILE: barlibs/stdout/fuzz/testcases/testcase_001 ================================================ y ================================================ FILE: barlibs/stdout/fuzz/testcases/testcase_002 ================================================ z y x x ================================================ FILE: barlibs/stdout/fuzz/testcases/testcase_003 ================================================ xz z z y z ================================================ FILE: barlibs/stdout/fuzz/testcases/testcase_004 ================================================ y x z z ================================================ FILE: barlibs/stdout/fuzz/testcases/testcase_005 ================================================ z xx z y ================================================ FILE: barlibs/stdout/fuzz/testcases/testcase_006 ================================================ x y z yy yyy yxx ================================================ FILE: barlibs/stdout/fuzz/testcases/testcase_007 ================================================ xyyxyx zy zy zxyy ================================================ FILE: barlibs/stdout/fuzz/testcases/testcase_008 ================================================ xxxxzxx ================================================ FILE: barlibs/stdout/fuzz/testcases/testcase_009 ================================================ zyyyyzxyyxyxxx ================================================ FILE: barlibs/stdout/luastatus-dvtm ================================================ #!/bin/sh FIFO=~/.luastatus-dvtm set -e rm -f "$FIFO" mkfifo -m600 "$FIFO" exec ${LUASTATUS:-luastatus} -b stdout -B out_fd=3 3>"$FIFO" "$@" & status_pid=$! ${DVTM:-dvtm} -s "$FIFO" 2>/dev/null kill "$status_pid" rm -f "$FIFO" ================================================ FILE: barlibs/stdout/luastatus-stdout-wrapper ================================================ #!/bin/sh exec ${LUASTATUS:-luastatus} -b stdout -B out_fd=3 "$@" 3>&1 1>&2 ================================================ FILE: barlibs/stdout/open_stdio_file.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "open_stdio_file.h" #include #include #include #include #include "libls/ls_io_utils.h" #include "libls/ls_tls_ebuf.h" #include "include/barlib_data_v1.h" #include "include/sayf_macros.h" static bool open_input_fd( LuastatusBarlibData *bd, FILE **dst, int fd) { if (ls_make_cloexec(fd) < 0) { LS_FATALF(bd, "can't make fd %d (in_fd) CLOEXEC: %s", fd, ls_tls_strerror(errno)); return false; } *dst = fdopen(fd, "r"); if (!*dst) { LS_FATALF(bd, "can't fdopen %d (in_fd): %s", fd, ls_tls_strerror(errno)); return false; } return true; } static bool open_input_filename( LuastatusBarlibData *bd, FILE **dst, const char *filename) { int fd = open(filename, O_RDONLY | O_CLOEXEC); if (fd < 0) { LS_FATALF(bd, "can't open '%s' (in_filename): %s", filename, ls_tls_strerror(errno)); return false; } *dst = fdopen(fd, "r"); if (!*dst) { LS_FATALF(bd, "can't fdopen %d (opened in_filename): %s", fd, ls_tls_strerror(errno)); close(fd); return false; } return true; } bool open_input( LuastatusBarlibData *bd, FILE **dst, int fd, const char *filename) { if (fd >= 0 && filename) { LS_FATALF(bd, "both in_fd and in_filename were specified"); return false; } if (fd >= 0) { return open_input_fd(bd, dst, fd); } if (filename) { return open_input_filename(bd, dst, filename); } return true; } bool open_output( LuastatusBarlibData *bd, FILE **dst, int fd) { if (ls_make_cloexec(fd) < 0) { LS_FATALF(bd, "can't make fd %d (out_fd) CLOEXEC: %s", fd, ls_tls_strerror(errno)); return false; } *dst = fdopen(fd, "w"); if (!*dst) { LS_FATALF(bd, "can't fdopen %d (out_fd): %s", fd, ls_tls_strerror(errno)); return false; } return true; } ================================================ FILE: barlibs/stdout/open_stdio_file.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef open_stdio_file_ #define open_stdio_file_ #include #include #include "include/barlib_data_v1.h" bool open_input( LuastatusBarlibData *bd, FILE **dst, int fd, const char *filename); bool open_output( LuastatusBarlibData *bd, FILE **dst, int fd); #endif ================================================ FILE: barlibs/stdout/sanitize.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "sanitize.h" #include #include "libls/ls_string.h" #include "libsafe/safev.h" static inline void append_sv(LS_String *dst, SAFEV v) { ls_string_append_b(dst, SAFEV_ptr_UNSAFE(v), SAFEV_len(v)); } void append_sanitized(LS_String *buf, SAFEV v) { for (;;) { size_t i = SAFEV_index_of(v, '\n'); if (i == (size_t) -1) { break; } append_sv(buf, SAFEV_subspan(v, 0, i)); v = SAFEV_suffix(v, i + 1); } append_sv(buf, v); } ================================================ FILE: barlibs/stdout/sanitize.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef sanitize_h_ #define sanitize_h_ #include "libls/ls_string.h" #include "libsafe/safev.h" void append_sanitized(LS_String *buf, SAFEV v); #endif ================================================ FILE: barlibs/stdout/stdout.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include "include/barlib_v1.h" #include "include/sayf_macros.h" #include "libls/ls_string.h" #include "libls/ls_cstring_utils.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_parse_int.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_lua_compat.h" #include "libsafe/safev.h" #include "sanitize.h" #include "open_stdio_file.h" typedef struct { size_t nwidgets; LS_String *bufs; // Temporary buffer for secondary buffering, to avoid unneeded redraws. LS_String tmpbuf; char *sep; // Content of an "error" segment. char *error; // /fdopen/'ed output file descriptor. FILE *out; // Value of /in_filename/ option. FILE *in; } Priv; static void destroy(LuastatusBarlibData *bd) { Priv *p = bd->priv; for (size_t i = 0; i < p->nwidgets; ++i) { ls_string_free(p->bufs[i]); } free(p->bufs); ls_string_free(p->tmpbuf); free(p->sep); free(p->error); if (p->out) { fclose(p->out); } if (p->in) { fclose(p->in); } free(p); } static int init(LuastatusBarlibData *bd, const char *const *opts, size_t nwidgets) { Priv *p = bd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .nwidgets = nwidgets, .bufs = LS_XNEW(LS_String, nwidgets), .tmpbuf = ls_string_new_reserve(512), .sep = NULL, .error = NULL, .out = NULL, .in = NULL, }; for (size_t i = 0; i < nwidgets; ++i) p->bufs[i] = ls_string_new_reserve(512); // All the options may be passed multiple times! const char *sep = NULL; const char *error = NULL; const char *in_filename = NULL; int out_fd = -1; int in_fd = -1; for (const char *const *s = opts; *s; ++s) { const char *v; if ((v = ls_strfollow(*s, "out_fd="))) { if ((out_fd = ls_full_strtou(v)) < 0) { LS_FATALF(bd, "out_fd value is not a valid unsigned integer"); goto error; } } else if ((v = ls_strfollow(*s, "in_fd="))) { if ((in_fd = ls_full_strtou(v)) < 0) { LS_FATALF(bd, "in_fd value is not a valid unsigned integer"); goto error; } } else if ((v = ls_strfollow(*s, "separator="))) { sep = v; } else if ((v = ls_strfollow(*s, "error="))) { error = v; } else if ((v = ls_strfollow(*s, "in_filename="))) { in_filename = v; } else { LS_FATALF(bd, "unknown option '%s'", *s); goto error; } } p->sep = ls_xstrdup(sep ? sep : " | "); p->error = ls_xstrdup(error ? error : "(Error)"); // we require /out_fd/ to be >=3 because making stdin/stdout/stderr CLOEXEC has very bad // consequences, and we just don't want to complicate the logic. if (out_fd < 3) { LS_FATALF(bd, "out_fd is not specified or less than 3"); goto error; } // same goes for /in_fd/, if specified if (in_fd >= 0 && in_fd < 3) { LS_FATALF(bd, "in_fd is less than 3"); goto error; } // open output if (!open_output(bd, &p->out, out_fd)) { goto error; } // open input if (!open_input(bd, &p->in, in_fd, in_filename)) { goto error; } return LUASTATUS_OK; error: destroy(bd); return LUASTATUS_ERR; } static bool redraw(LuastatusBarlibData *bd) { Priv *p = bd->priv; FILE *out = p->out; size_t n = p->nwidgets; LS_String *bufs = p->bufs; const char *sep = p->sep; bool first = true; for (size_t i = 0; i < n; ++i) { if (bufs[i].size) { if (!first) { fputs(sep, out); } fwrite(bufs[i].data, 1, bufs[i].size, out); first = false; } } putc('\n', out); fflush(out); if (ferror(out)) { LS_FATALF(bd, "write error: %s", ls_tls_strerror(errno)); return false; } return true; } static int set(LuastatusBarlibData *bd, lua_State *L, size_t widget_idx) { Priv *p = bd->priv; LS_String *buf = &p->tmpbuf; ls_string_clear(buf); // L: ? data switch (lua_type(L, -1)) { case LUA_TNIL: break; case LUA_TSTRING: { size_t ns; const char *s = lua_tolstring(L, -1, &ns); append_sanitized(buf, SAFEV_new_UNSAFE(s, ns)); } break; case LUA_TTABLE: { const char *sep = p->sep; size_t len = ls_lua_array_len(L, -1); for (size_t i = 1; i <= len; ++i) { lua_rawgeti(L, -1, i); // L: ? data value if (lua_isnil(L, -1)) { goto next; } if (!lua_isstring(L, -1)) { LS_ERRF(bd, "table value: expected string, found %s", luaL_typename(L, -1)); goto invalid_data; } size_t ns; const char *s = lua_tolstring(L, -1, &ns); if (buf->size && ns) { ls_string_append_s(buf, sep); } append_sanitized(buf, SAFEV_new_UNSAFE(s, ns)); next: lua_pop(L, 1); // L: ? data value } // L: ? data } break; default: LS_ERRF(bd, "expected string, table or nil, found %s", luaL_typename(L, -1)); goto invalid_data; } if (!ls_string_eq(*buf, p->bufs[widget_idx])) { ls_string_swap(buf, &p->bufs[widget_idx]); if (!redraw(bd)) { return LUASTATUS_ERR; } } return LUASTATUS_OK; invalid_data: ls_string_clear(&p->bufs[widget_idx]); return LUASTATUS_NONFATAL_ERR; } static int set_error(LuastatusBarlibData *bd, size_t widget_idx) { Priv *p = bd->priv; ls_string_assign_s(&p->bufs[widget_idx], p->error); if (!redraw(bd)) { return LUASTATUS_ERR; } return LUASTATUS_OK; } static int event_watcher(LuastatusBarlibData *bd, LuastatusBarlibEWFuncs funcs) { Priv *p = bd->priv; if (!p->in) { LS_DEBUGF(bd, "event watcher: in_fd/in_filename not specified, returning"); return LUASTATUS_NONFATAL_ERR; } char *line = NULL; size_t line_buf_n = 1024; ssize_t line_n; while ((line_n = getline(&line, &line_buf_n, p->in)) >= 0) { if (line_n && line[line_n - 1] == '\n') { --line_n; } for (size_t i = 0; i < p->nwidgets; ++i) { lua_State *L = funcs.call_begin(bd->userdata, i); lua_pushlstring(L, line, line_n); funcs.call_end(bd->userdata, i); } } if (feof(p->in)) { LS_FATALF(bd, "event watcher: the other end of pipe/FIFO/something has been closed"); } else { LS_FATALF(bd, "event watcher: I/O error: %s", ls_tls_strerror(errno)); } free(line); return LUASTATUS_ERR; } LuastatusBarlibIface luastatus_barlib_iface_v1 = { .init = init, .set = set, .set_error = set_error, .event_watcher = event_watcher, .destroy = destroy, }; ================================================ FILE: check_final_newline.py ================================================ #!/usr/bin/env python3 import os import sys def say(s: str) -> None: print(s, file=sys.stderr) def main() -> None: args = sys.argv[1:] if not args: say('USAGE: check_final_newline.py FILE [FILE ...]') sys.exit(2) everything_ok = True for arg in args: with open(arg, 'rb') as f: f.seek(0, os.SEEK_END) file_size = f.tell() if not file_size: continue f.seek(-1, os.SEEK_END) final_byte = f.read(1) if final_byte != b'\n': say(f'File "{arg}" has no trailing newline!') everything_ok = False if everything_ok: sys.exit(0) else: sys.exit(1) if __name__ == '__main__': main() ================================================ FILE: check_includes.sh ================================================ #!/usr/bin/env bash # USAGE: check_includes.sh DIRECTORY [EXTRA CFLAGS...] # Requires 'include-what-you-use' tool. set -e set -o pipefail check_entity=${1?}; shift if [[ -d $check_entity ]]; then check_dir=$check_entity check_file= else check_dir=$(dirname -- "$check_entity") check_file=$check_entity fi extra_cflags=( -D_POSIX_C_SOURCE=200809L "$@" ) luastatus_dir="$check_dir" luastatus_dir_found=0 for (( i = 0; i < 10; ++i )); do if [[ -e "$luastatus_dir"/generate-man.sh ]]; then luastatus_dir_found=1 break fi luastatus_dir+='/..' done if (( ! luastatus_dir_found )); then echo >&2 "Cannot find luastatus dir" exit 1 fi cmakelists_dir=${CMAKELISTS_DIR:-$check_dir} modules=() if [[ -e "$cmakelists_dir"/CMakeLists.txt ]]; then modules_raw=$(sed -rn 's/^\s*pkg_check_modules\s*\(.*\s+REQUIRED\s+(.*)\)\s*$/\1/p' "$cmakelists_dir"/CMakeLists.txt) # Replace all whitespace with newlines modules_raw=$(sed -r 's/\s+/\n/g' <<< "$modules_raw") # Remove version specifications (e.g. "yajl>=2.0.4" -> "yajl") modules_raw=$(sed -r 's/^([-a-zA-Z0-9_.]+).*/\1/' <<< "$modules_raw") # Split by whitespace, assign to 'modules' array modules=( $modules_raw ) fi if (( ${#modules[@]} )); then echo >&2 "Modules: ${modules[*]}" else echo >&2 "Modules: (none)" fi my_filter() { awk ' /^The full include-list for / { skip = 1 } /^---$/ { skip = 0 } # print if and only if (!skip) !skip ' } cflags=$(pkg-config --cflags ${LUA_LIB:-lua} "${modules[@]}") do_check_specific_file() { include-what-you-use -I"$luastatus_dir" $cflags "${extra_cflags[@]}" "$1" 2>&1 | my_filter } if [[ -n $check_file ]]; then do_check_specific_file "$check_file" else find "$check_dir" -name '*.[ch]' | while IFS= read -r src_file; do if [[ $src_file == *.in.h ]]; then continue fi do_check_specific_file "$src_file" done fi ================================================ FILE: check_style.sh ================================================ #!/usr/bin/env bash set -e set -o pipefail cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" say() { printf '%s\n' "$*" >&2 } exts=( c h lua py sh bash md rst txt ) find_name_clause=() for ext in "${exts[@]}"; do if (( ${#find_name_clause[@]} )); then find_name_clause+=( -or ) fi find_name_clause+=( -name "*.$ext" ) done ignore_db=( f:CMakeCache.txt D:CMakeFiles D:CMakeScripts f:Makefile f:cmake_install.cmake f:install_manifest.txt f:CTestTestfile.cmake D:build ) check_against_ignore_db() { local entry local entry_basename for entry in "${ignore_db[@]}"; do entry_basename=${entry#?:} case "$entry" in f:*) if [[ $1 == */"$entry_basename" ]]; then return 1 fi ;; D:*) if [[ $1 == */"$entry_basename"/* ]]; then return 1 fi ;; *) say "FATAL: ignore_db contains invalid entry '$entry'."; exit 1 ;; esac done return 0 } paths=() while IFS= read -r path; do if ! check_against_ignore_db "$path"; then continue fi paths+=( "$path" ) done < <(find \( "${find_name_clause[@]}" \) -and -type f) if (( ! ${#paths[@]} )); then say 'No files found with extensions of interest.' say 'This means something is wrong, so exiting with non-zero code.' exit 1 fi printf '%s\n' "${paths[@]}" | xargs -d $'\n' ./check_final_newline.py ok=1 for path in "${paths[@]}"; do if grep -E -H -n '\s$' -- "$path"; then ok=0 fi done if (( !ok )); then say 'Some files have lines with trailing whitespace; see above.' exit 1 fi say "Checked ${#paths[@]} file(s), everything is OK." exit 0 ================================================ FILE: contrib/luastatus-9999.ebuild ================================================ # Copyright 1999-2023 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 EAPI=8 CMAKE_IN_SOURCE_BUILD=1 inherit cmake DESCRIPTION="Universal status bar content generator" HOMEPAGE="https://github.com/shdown/luastatus" if [[ ${PV} == *9999* ]]; then inherit git-r3 SRC_URI="" EGIT_REPO_URI="https://github.com/shdown/${PN}.git" KEYWORDS="~amd64 ~x86" else SRC_URI="https://github.com/shdown/${PN}/archive/v${PV}.tar.gz -> ${P}.tar.gz" KEYWORDS="amd64 x86" fi BARLIBS=" ${PN}_barlibs_dwm ${PN}_barlibs_i3 ${PN}_barlibs_lemonbar ${PN}_barlibs_stdout " PROPER_PLUGINS=" +${PN}_plugins_alsa +${PN}_plugins_dbus +${PN}_plugins_fs +${PN}_plugins_inotify +${PN}_plugins_mpd +${PN}_plugins_network-linux +${PN}_plugins_pulse +${PN}_plugins_timer +${PN}_plugins_udev +${PN}_plugins_unixsock +${PN}_plugins_xkb +${PN}_plugins_xtitle " DERIVED_PLUGINS=" +${PN}_plugins_backlight-linux +${PN}_plugins_battery-linux +${PN}_plugins_cpu-usage-linux +${PN}_plugins_file-contents-linux +${PN}_plugins_imap +${PN}_plugins_mem-usage-linux +${PN}_plugins_pipe " PLUGINS=" ${PROPER_PLUGINS} ${DERIVED_PLUGINS} " LICENSE="LGPL-3+" SLOT="0" IUSE="doc examples luajit ${BARLIBS} ${PLUGINS}" REQUIRED_USE=" ${PN}_plugins_backlight-linux? ( ${PN}_plugins_udev ) ${PN}_plugins_battery-linux? ( ${PN}_plugins_udev ) ${PN}_plugins_cpu-usage-linux? ( ${PN}_plugins_timer ) ${PN}_plugins_file-contents-linux? ( ${PN}_plugins_inotify ) ${PN}_plugins_imap? ( ${PN}_plugins_timer ) ${PN}_plugins_mem-usage-linux? ( ${PN}_plugins_timer ) ${PN}_plugins_pipe? ( ${PN}_plugins_timer ) " DEPEND=" doc? ( dev-python/docutils ) " RDEPEND=" luajit? ( dev-lang/luajit:2 ) !luajit? ( dev-lang/lua ) ${PN}_barlibs_dwm? ( x11-libs/libxcb ) ${PN}_barlibs_i3? ( >=dev-libs/yajl-2.0.4 ) ${PN}_plugins_alsa? ( media-libs/alsa-lib ) ${PN}_plugins_dbus? ( dev-libs/glib ) ${PN}_plugins_network-linux? ( sys-kernel/linux-headers dev-libs/libnl ) ${PN}_plugins_pulse? ( media-sound/pulseaudio ) ${PN}_plugins_udev? ( virtual/libudev ) ${PN}_plugins_xkb? ( x11-libs/libX11 ) ${PN}_plugins_xtitle? ( x11-libs/libxcb x11-libs/xcb-util-wm x11-libs/xcb-util ) " src_configure() { local mycmakeargs=( $(use luajit && echo -DWITH_LUA_LIBRARY=luajit) -DBUILD_DOCS=$(usex doc) -DBUILD_BARLIB_DWM=$(usex ${PN}_barlibs_dwm) -DBUILD_BARLIB_I3=$(usex ${PN}_barlibs_i3) -DBUILD_BARLIB_LEMONBAR=$(usex ${PN}_barlibs_lemonbar) -DBUILD_BARLIB_STDOUT=$(usex ${PN}_barlibs_stdout) -DBUILD_PLUGIN_ALSA=$(usex ${PN}_plugins_alsa) -DBUILD_PLUGIN_BACKLIGHT_LINUX=$(usex ${PN}_plugins_backlight-linux) -DBUILD_PLUGIN_BATTERY_LINUX=$(usex ${PN}_plugins_battery-linux) -DBUILD_PLUGIN_CPU_USAGE_LINUX=$(usex ${PN}_plugins_cpu-usage-linux) -DBUILD_PLUGIN_DBUS=$(usex ${PN}_plugins_dbus) -DBUILD_PLUGIN_FILE_CONTENTS_LINUX=$(usex ${PN}_plugins_file-contents-linux) -DBUILD_PLUGIN_FS=$(usex ${PN}_plugins_fs) -DBUILD_PLUGIN_IMAP=$(usex ${PN}_plugins_imap) -DBUILD_PLUGIN_INOTIFY=$(usex ${PN}_plugins_inotify) -DBUILD_PLUGIN_MEM_USAGE_LINUX=$(usex ${PN}_plugins_mem-usage-linux) -DBUILD_PLUGIN_MPD=$(usex ${PN}_plugins_mpd) -DBUILD_PLUGIN_NETWORK_LINUX=$(usex ${PN}_plugins_network-linux) -DBUILD_PLUGIN_PIPE=$(usex ${PN}_plugins_pipe) -DBUILD_PLUGIN_PULSE=$(usex ${PN}_plugins_pulse) -DBUILD_PLUGIN_TIMER=$(usex ${PN}_plugins_timer) -DBUILD_PLUGIN_UDEV=$(usex ${PN}_plugins_udev) -DBUILD_PLUGIN_UNIXSOCK=$(usex ${PN}_plugins_unixsock) -DBUILD_PLUGIN_XKB=$(usex ${PN}_plugins_xkb) -DBUILD_PLUGIN_XTITLE=$(usex ${PN}_plugins_xtitle) ) cmake_src_configure } src_install() { cmake_src_install local i if use examples; then dodir /usr/share/doc/${PF}/examples docinto examples for i in ${BARLIBS//+/}; do if use ${i}; then barlib=${i#${PN}_barlibs_} dodoc -r examples/${barlib} docompress -x /usr/share/doc/${PF}/examples/${barlib} fi done fi } ================================================ FILE: contrib/luastatus.spec ================================================ Name: luastatus Version: 0.3.0 Release: 1%{?dist} Summary: universal statusbar content generator License: LGPL3+ URL: https://github.com/shdown/luastatus Source0: https://github.com/shdown/luastatus/archive/v%version.tar.gz BuildRequires: cmake BuildRequires: luajit-devel BuildRequires: libxcb-devel BuildRequires: yajl-devel BuildRequires: alsa-lib-devel BuildRequires: xcb-util-wm-devel BuildRequires: xcb-util-devel BuildRequires: glib2-devel %description a universal status bar content generator %package plugins Summary: luastatus plugins Requires: %{name} = %{version}-%{release} %description plugins luastatus plugins %prep %setup -q %build %cmake -DWITH_LUA_LIBRARY=luajit . %make_build %install %make_install %files %doc COPYING.txt COPYING.LESSER.txt README.md %{_bindir}/luastatus %{_bindir}/luastatus-i3-wrapper %{_bindir}/luastatus-lemonbar-launcher %{_mandir}/man1/luastatus.1* %{_libdir}/luastatus/barlibs/* %files plugins %{_libdir}/luastatus/plugins/* %changelog ================================================ FILE: debian/changelog ================================================ luastatus (1:9999) UNRELEASED; urgency=medium * local build -- Local User Sat, 08 Feb 2014 01:01:01 -0800 ================================================ FILE: debian/compat ================================================ 10 ================================================ FILE: debian/control ================================================ Source: luastatus Section: misc Priority: optional Maintainer: Viktor Krapivensky Standards-Version: 4.5.0 Build-Depends: cmake (>= 3.1.3), debhelper (>= 10), valgrind, jq, liblua5.3-dev, pkg-config, python3-docutils, libyajl-dev, libasound2-dev, libglib2.0-dev, libpulse-dev, libudev-dev, linux-libc-dev, libnl-3-dev, libnl-genl-3-dev, libx11-dev, libxcb1-dev, libxcb-ewmh-dev, libxcb-icccm4-dev, libxcb-util0-dev Homepage: https://github.com/shdown/luastatus Rules-Requires-Root: no Package: luastatus Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends} Recommends: Suggests: Description: Universal status bar content generator luastatus is a universal status bar content generator. It allows the user to configure the way the data from event sources is processed and shown, with Lua. Its main feature is that the content can be updated immediately as some event occurs. . This package contains the main binary and the following plugins: timer, fs, inotify, udev, backlight-linux, battery-linux, cpu-usage-linux, file-contents-linux, mem-usage-linux, pipe. Package: luastatus-barlib-i3 Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, luastatus Recommends: Suggests: i3-wm Description: i3 barlib for luastatus This package contains the i3 barlib for luastatus. This barlib talks to i3bar (part of the i3 window manager), or any other program compatible with it on the protocol level. This package also provides luastatus-i3-wrapper script. . For more information, see "man 7 luastatus-barlib-i3". Package: luastatus-barlib-dwm Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, luastatus Recommends: Suggests: dwm Description: dwm barlib for luastatus This package contains the dwm barlib for luastatus. It updates the name of the root window. Although named dwm, it can really work with any program that outputs the root window name somewhere. . For more information, see "man 7 luastatus-barlib-dwm". Package: luastatus-barlib-lemonbar Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, luastatus Recommends: Suggests: lemonbar Description: lemonbar barlib for luastatus This package contains the lemonbar barlib for luastatus. The package also provides luastatus-lemonbar-launcher script. . For more information, see "man 7 luastatus-barlib-lemonbar". Package: luastatus-barlib-stdout Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, luastatus Recommends: Suggests: dzen2 | xmobar | yabar | dvtm Description: stdout barlib for luastatus This is stdout barlib for luastatus. It simply outputs lines to given file descriptor. It can be used with, for example, dzen2, xmobar, yabar, dvtm. This package also provides luastatus-stdout-wrapper script. . For more information, see "man 7 luastatus-barlib-stdout". Package: luastatus-plugin-alsa Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, luastatus Recommends: Suggests: Description: ALSA plugin for luastatus This package contains the ALSA plugin for luastatus. It monitors volume and mute state of an ALSA channel. . For more information, see "man 7 luastatus-plugin-alsa". Package: luastatus-plugin-dbus Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, luastatus Recommends: Suggests: Description: D-Bus plugin for luastatus This package contains the D-Bus plugin for luastatus. It subscribes to and reports D-Bus signals. . For more information, see "man 7 luastatus-plugin-dbus". Package: luastatus-plugin-mpd Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, luastatus Recommends: Suggests: mpd Description: MPD plugin for luastatus This package contains the MPD plugin for luastatus. It monitors the state of an MPD server. . For more information, see "man 7 luastatus-plugin-mpd". Package: luastatus-plugin-network-linux Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, luastatus Recommends: Suggests: Description: network-linux plugin for luastatus This package contains the network-linux plugin for luastatus. It monitors network routing and link updates, and can report IP addresses used for outgoing connections by various network interfaces, information about a wireless connection, and speed of an ethernet connection. . For more information, see "man 7 luastatus-plugin-network-linux". Package: luastatus-plugin-pulse Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, luastatus Recommends: Suggests: pulseaudio Description: PulseAudio plugin for luastatus This package contains the PulseAudio plugin for luastatus. It monitors the volume and mute status of a PulseAudio sink. . For more information, see "man 7 luastatus-plugin-pulse". Package: luastatus-plugin-xkb Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, luastatus Recommends: Suggests: Description: XKB plugin for luastatus This package contains the XKB plugin for luastatus. It monitors the current keyboard layout, and, optionally, the state of LED indicators, such as Caps Lock and Num Lock. . For more information, see "man 7 luastatus-plugin-xkb". Package: luastatus-plugin-xtitle Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, luastatus Recommends: Suggests: Description: xtitle plugin for luastatus This package conntains the xtitle plugin for luastatus. It monitors the active window title. . For more information, see "man 7 luastatus-plugin-xtitle". Package: luastatus-plugin-imap Architecture: all Depends: ${misc:Depends}, luastatus, lua5.3-socket, lua5.3-sec Recommends: Suggests: Description: IMAP plugin for luastatus This package contains the IMAP plugin for luastatus. It monitors the number of unread mails in an IMAP mailbox. . For more information, see "man 7 luastatus-plugin-imap". ================================================ FILE: debian/copyright ================================================ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: luastatus Upstream-Contact: Viktor Krapivensky Source: https://github.com/shdown/luastatus Files: * Copyright: 2015-2021, luastatus developers License: LGPL-3.0+ License: LGPL-3.0+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. . This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. . You should have received a copy of the GNU Lesser General Public License along with this package. If not, see . . On Debian systems, the full text of the GNU Lesser General Public License version 3 can be found in the file `/usr/share/common-licenses/LGPL-3'. ================================================ FILE: debian/rules ================================================ #! /usr/bin/make -f export V:=1 export DEB_BUILD_MAINT_OPTIONS:=hardening=+all BUILD_DIR := _deb_build override_dh_auto_clean: rm -rf $(BUILD_DIR) override_dh_installexamples: dh_installexamples examples/* override_dh_installchangelogs: set -e; if [ -e RELEASE_NOTES ]; then \ dh_installchangelogs RELEASE_NOTES; \ else \ dh_installchangelogs; \ fi override_dh_auto_configure: set -e; mkdir $(BUILD_DIR); cd $(BUILD_DIR); cmake \ -DCMAKE_INSTALL_PREFIX=/usr \ -DWITH_LUA_LIBRARY=lua5.3 \ -DBUILD_PLUGIN_PULSE=on \ -DBUILD_PLUGIN_UNIXSOCK=on \ -DBUILD_TESTS=on \ -S .. -B . override_dh_auto_build: $(MAKE) -C $(BUILD_DIR) override_dh_auto_install: # luastatus itself $(MAKE) -C $(BUILD_DIR)/luastatus install \ DESTDIR=../../debian/luastatus # barlibs set -e; for x in dwm i3 lemonbar stdout; do \ $(MAKE) -C $(BUILD_DIR)/barlibs/$$x install \ DESTDIR=../../../debian/luastatus-barlib-$$x; \ done # "core" plugins (that should go into luastatus pkg) set -e; for x in \ fs timer inotify udev \ backlight-linux \ battery-linux \ cpu-usage-linux \ file-contents-linux \ mem-usage-linux \ pipe \ unixsock; \ do \ $(MAKE) -C $(BUILD_DIR)/plugins/$$x install \ DESTDIR=../../../debian/luastatus; \ done # other plugins set -e; for x in \ alsa dbus inotify mpd network-linux pulse xkb xtitle \ imap; \ do \ $(MAKE) -C $(BUILD_DIR)/plugins/$$x install \ DESTDIR=../../../debian/luastatus-plugin-$$x; \ done # man page symlink: luastatus-i3-wrapper(1) -> luastatus-barlib-i3(7) mkdir debian/luastatus-barlib-i3/usr/share/man/man1 ln -s ../man7/luastatus-barlib-i3.7 \ debian/luastatus-barlib-i3/usr/share/man/man1/luastatus-i3-wrapper.1 # man page symlink: luastatus-lemonbar-launcher(1) -> luastatus-barlib-lemonbar(7) mkdir debian/luastatus-barlib-lemonbar/usr/share/man/man1 ln -s ../man7/luastatus-barlib-lemonbar.7 \ debian/luastatus-barlib-lemonbar/usr/share/man/man1/luastatus-lemonbar-launcher.1 # man page symlink: luastatus-stdout-wrapper(1) -> luastatus-barlib-stdout(7) mkdir debian/luastatus-barlib-stdout/usr/share/man/man1 ln -s ../man7/luastatus-barlib-stdout.7 \ debian/luastatus-barlib-stdout/usr/share/man/man1/luastatus-stdout-wrapper.1 override_dh_auto_test: ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) ./tests/pt.sh $(BUILD_DIR) skip:plugin-dbus skip:plugin-pulse ./tests/torture.sh $(BUILD_DIR) endif %: exec dh $@ ================================================ FILE: examples/dwm/alsa-gauge.lua ================================================ local GAUGE_NCHARS = 10 local function mk_gauge(level, full, empty) local nfull = math.floor(level * GAUGE_NCHARS + 0.5) return full:rep(nfull) .. empty:rep(GAUGE_NCHARS - nfull) end widget = { plugin = 'alsa', cb = function(t) local level = (t.vol.cur - t.vol.min) / (t.vol.max - t.vol.min) if t.mute then return mk_gauge(level, '×', '—') else return mk_gauge(level, '●', '○') end end, } ================================================ FILE: examples/dwm/alsa.lua ================================================ widget = { plugin = 'alsa', cb = function(t) if t.mute then return '[mute]' else local percent = (t.vol.cur - t.vol.min) / (t.vol.max - t.vol.min) * 100 return string.format('[%3d%%]', math.floor(0.5 + percent)) end end, } ================================================ FILE: examples/dwm/backlight.lua ================================================ -- Note that this widget only shows backlight level when it changes. widget = luastatus.require_plugin('backlight-linux').widget{ cb = function(level) if level ~= nil then return string.format('*%3.0f%%', level * 100) end end, } ================================================ FILE: examples/dwm/battery.lua ================================================ widget = luastatus.require_plugin('battery-linux').widget{ period = 2, cb = function(t) local symbol = ({ Charging = '↑', Discharging = '↓', })[t.status] or ' ' local rem_seg if t.rem_time then local h = math.floor(t.rem_time) local m = math.floor(60 * (t.rem_time - h)) rem_seg = string.format('%2dh %02dm', h, m) end return { string.format('%3d%%%s', t.capacity, symbol), rem_seg, } end, } ================================================ FILE: examples/dwm/bluetooth.lua ================================================ -- A widget to display currently connected and paired bluetooth devices. -- To change output format modify reprint_devices function. if not luastatus.execute('command -v bluetoothctl >/dev/null') then error('"bluetoothctl" command, which is required for this widget to work, was not found') end local separator = " " -- Object paths look like /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/somethingsomething local function get_device_mac_address(device_object_path) return device_object_path:gsub("/.*/dev_", ""):gsub("/.*", ""):gsub("_", ":") end -- For reference bluetoothctl devices output looks like that: -- Device XX:XX:XX:XX:XX:XX JBL T450BT -- Device YY:YY:YY:YY:YY:YY Redmi 8 -- -- Function returns mac addresses of all devices. local function get_devices() local devices = {} local handle = io.popen(string.format("bluetoothctl devices")) for line in handle:lines() do local match = string.match(line, "Device ([%x:]+)") if match then table.insert(devices, math) end end handle:close() return devices end -- For reference bluetoothctl info output looks like that: -- Device XX:XX:XX:XX:XX:XX (public) -- Name: JBL T450BT -- Alias: JBL T450BT -- Class: 0xFFFFFFFF -- Icon: audio-card -- Paired: yes -- Trusted: yes -- Blocked: no -- Connected: yes -- LegacyPairing: no -- UUID: Headset (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) -- ... -- UUID: Handsfree (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) -- -- Given this input function returns a following table: -- [alias] string JBL T450BT -- [blocked] boolean false -- [class] string 0x00240404 -- [connected] boolean true -- [icon] string audio -- [legacypairing] boolean false -- [name] string JBL T450BT -- [paired] boolean true -- [trusted] boolean true local function get_device_info(mac_address) if mac_address == nil then mac_address = "" end assert(string.match(mac_address, '^[%x:]*$') ~= nil) local device_info = {} local handle = io.popen(string.format("bluetoothctl info %s", mac_address)) for line in handle:lines() do local key, value = string.match(line, "(%w+): (.*)") -- Filter junk if key ~= "UUID" and key ~= nil and value ~= nil then key = string.lower(key) if key ~= "name" and key ~= "alias" and key ~= "icon" then if value == "yes" then value = true end if value == "no" then value = false end end device_info[key] = value end end handle:close() return device_info end local devices = {} local function reprint_devices() local t = {} for mac_address, device in pairs(devices) do table.insert(t, string.format("%s(%s)", device["name"], mac_address)) end return table.concat(t, separator) end widget = { plugin = "dbus", opts = { greet = true, -- https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/device-api.txt signals = { { sender = "org.bluez", interface = "org.freedesktop.DBus.Properties", signal = "PropertiesChanged", arg0 = "org.bluez.Device1", bus = "system" } } }, cb = function(t) if t.what == "hello" then local mac_addresses = get_devices() for _, mac_address in ipairs(mac_addresses) do local device = get_device_info(mac_address) if device["connected"] and device["paired"] then devices[mac_address] = device end end elseif t.what == "signal" then -- For reference message from dbus looks like that: -- table -- [1] string org.bluez.Device1 -- [2] table -- [2] [1] table -- [2] [1] [1] string SomethingSomething -- [2] [1] [2] boolean false -- [2] [2] table -- [2] [2] [1] string Connected -- [2] [2] [2] boolean true -- [3] table if t.signal == "PropertiesChanged" then for _, message in pairs(t.parameters[2]) do if message[1] == "Connected" or message[1] == "Paired" then local mac_address = get_device_mac_address(t.object_path) if message[2] then local device = get_device_info(mac_address) if device["paired"] then devices[mac_address] = device end else devices[mac_address] = nil end end end end end return reprint_devices() end } ================================================ FILE: examples/dwm/btc-price.lua ================================================ -- Bitcoin price widget. -- Updates on click and every 5 minutes. local custom_sleep_amt = nil local function check_error(t) if t.error then -- Low-level libcurl error return t.error end if t.status < 200 or t.status > 299 then -- Bad HTTP status return string.format('HTTP status %d', t.status) end -- Everything's OK return nil end widget = { plugin = 'web', opts = { planner = function() while true do coroutine.yield({action = 'request', params = { url = 'https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT', timeout = 5, }}) local period = custom_sleep_amt or (5 * 60) custom_sleep_amt = nil coroutine.yield({action = 'sleep', period = period}) end end, make_self_pipe = true, }, cb = function(t) local text local err_msg = check_error(t) if err_msg then print(string.format('WARNING: luastatus: btc-price widget: %s'), err_msg) text = '......' custom_sleep_amt = 5 else local obj = assert(luastatus.plugin.json_decode(t.body)) text = obj.price:match('[^.]+') end return string.format('[$%s]', text) end, event = function(t) if t.button == 1 then luastatus.plugin.wake_up() end end, } ================================================ FILE: examples/dwm/cpu-freq.lua ================================================ local VBLOCKS = {'▂', '▃', '▄', '▅', '▆', '▇', '█'} local function make_chunk(entry) local num = entry.cur - entry.min local denom = entry.max - entry.min local ratio if denom ~= 0 then ratio = num / denom else -- If max_freq == min_freq, set ratio to zero. ratio = 0 end local vblock_idx = math.min( 1 + math.floor(0.5 + ratio * #VBLOCKS), #VBLOCKS) return VBLOCKS[vblock_idx] end local plugin_data = {} local plugin_params = { timer_opts = { period = 2, }, cb = function(t) if t == nil then return nil end local r = {} for _, entry in ipairs(t) do r[#r + 1] = make_chunk(entry) end return table.concat(r) end, event = function(t) if t.button == 1 then plugin_data.please_reload = true end end, } widget = luastatus.require_plugin('cpu-freq-linux').widget(plugin_params, plugin_data) ================================================ FILE: examples/dwm/cpu-temperature.lua ================================================ local plugin_data = {} local plugin_params = { timer_opts = { period = 2, }, cb = function(t) if not t then return nil end local r = {} for _, entry in ipairs(t) do r[#r + 1] = string.format('%.0f°', entry.value) end return r end, event = function(t) if t.button == 1 then plugin_data.please_reload = true end end, } widget = luastatus.require_plugin('cpu-temp-linux').widget(plugin_params, plugin_data) ================================================ FILE: examples/dwm/cpu-usage.lua ================================================ widget = luastatus.require_plugin('cpu-usage-linux').widget{ cb = function(usage) if usage ~= nil then return string.format('[%5.1f%%]', usage * 100) end end, } ================================================ FILE: examples/dwm/disk-io.lua ================================================ widget = luastatus.require_plugin('disk-io-linux').widget{ period = 2, cb = function(t) -- Sort by name for determinism table.sort(t, function(a, b) return a.name < b.name end) local segments = {} for _, entry in ipairs(t) do local R = entry.read_bytes local W = entry.written_bytes if (R >= 0) and (W >= 0) then segments[#segments + 1] = string.format( '%s: %.0fk↓ %.0fk↑', entry.name, R / 1024, W / 1024 ) end end return segments end, } ================================================ FILE: examples/dwm/file-contents.lua ================================================ widget = luastatus.require_plugin('file-contents-linux').widget{ filename = os.getenv('HOME') .. '/status', cb = function(f) -- show the first line of the file return f:read('*line') end, } ================================================ FILE: examples/dwm/fs.lua ================================================ local function sorted_keys(tbl) local keys = {} for k, _ in pairs(tbl) do keys[#keys + 1] = k end table.sort(keys) return keys end widget = { plugin = 'fs', opts = { paths = {'/', '/home'}, }, cb = function(t) -- Sort for determinism local keys = sorted_keys(t) local res = {} for _, k in ipairs(keys) do local v = t[k] table.insert(res, string.format('%s %.0f%%', k, (1 - v.avail / v.total) * 100)) end return res end, } ================================================ FILE: examples/dwm/gmail.lua ================================================ --[[ -- Expects 'credentials.lua' to be present in the current directory; it may contain, e.g., -- return { -- gmail = { -- login = 'john.smith', -- password = 'qwerty' -- } -- } --]] local credentials = require 'credentials' widget = luastatus.require_plugin('imap').widget{ verbose = false, host = 'imap.gmail.com', port = 993, mailbox = 'Inbox', use_ssl = true, timeout = 2 * 60, handshake_timeout = 10, login = credentials.gmail.login, password = credentials.gmail.password, error_sleep_period = 60, cb = function(unseen) if unseen == nil or unseen == 0 then return nil else return string.format('[%d unseen]', unseen) end end, } ================================================ FILE: examples/dwm/ip.lua ================================================ widget = { plugin = 'network-linux', cb = function(t) local r = {} for iface, params in pairs(t) do local iface_clean = iface local addr = params.ipv6 or params.ipv4 if addr then -- strip out "label" from the interface name iface_clean = iface_clean:gsub(':.*', '') -- strip out "zone index" from the address addr = addr:gsub('%%.*', '') if iface_clean ~= 'lo' then r[#r + 1] = string.format('[%s: %s]', iface_clean, addr) end end end return r end, } ================================================ FILE: examples/dwm/loadavg-linux.lua ================================================ local function get_ncpus() local f = assert(io.open('/proc/cpuinfo', 'r')) local n = 0 for line in f:lines() do if line:match('^processor\t') then n = n + 1 end end f:close() return n end local function avg2str(x) assert(x >= 0) if x >= 1000 then return '↑↑↑' end return string.format('%3.0f', x) end widget = { plugin = 'timer', opts = { period = 2, }, cb = function(_) local f = io.open('/proc/loadavg', 'r') local avg1, avg5, avg15 = f:read('*number', '*number', '*number') f:close() assert(avg1 and avg5 and avg15) local ncpus = get_ncpus() return string.format( '[%s%% %s%% %s%%]', avg2str(avg1 / ncpus * 100), avg2str(avg5 / ncpus * 100), avg2str(avg15 / ncpus * 100) ) end, } ================================================ FILE: examples/dwm/media-player-mpris.lua ================================================ local PLAYER = 'clementine' local PLAYBACK_STATUS_ICONS = { Playing = '▶', Paused = '◆', Stopped = '—', } local function fetch_metadata_field(t, key) if t.Metadata then return t.Metadata[key] else return nil end end widget = luastatus.require_plugin('mpris').widget{ player = PLAYER, cb = function(t) if not t.PlaybackStatus then return nil end local title = fetch_metadata_field(t, 'xesam:title') title = title or '' title = luastatus.libwidechar.make_valid_and_printable(title, '?') title = luastatus.libwidechar.truncate_to_width(title, 40) title = title or '' local icon = PLAYBACK_STATUS_ICONS[t.PlaybackStatus] or '?' local result = icon if title ~= '' then result = result .. ' ' .. title end return result end, } ================================================ FILE: examples/dwm/mem-usage.lua ================================================ widget = luastatus.require_plugin('mem-usage-linux').widget{ timer_opts = {period = 2}, cb = function(t) local used_kb = t.total.value - t.avail.value return string.format('[%3.2f GiB]', used_kb / 1024 / 1024) end, } ================================================ FILE: examples/dwm/mpd.lua ================================================ local titlewidth = 40 widget = { plugin = 'mpd', cb = function(t) if t.what == 'update' then local title if t.song.Title then title = t.song.Title if t.song.Artist then title = t.song.Artist .. ': ' .. title end else title = t.song.file or '' end title = luastatus.libwidechar.make_valid(title, '?') if assert(luastatus.libwidechar.width(title)) > titlewidth then title = luastatus.libwidechar.truncate_to_width(title, titlewidth - 1) .. '…' end return string.format('%s %s', ({play = '▶', pause = '‖', stop = '■'})[t.status.state], title ) else -- 'connecting' or 'error' return t.what end end } ================================================ FILE: examples/dwm/network-rate.lua ================================================ local function make_segment(iface, R, S) return string.format('[%s %.0fk↓ %.0fk↑]', iface, R / 1000, S / 1000) end widget = luastatus.require_plugin('network-rate-linux').widget{ iface_except = 'lo', period = 3, in_array_form = true, cb = function(t) local r = {} for _, PQ in ipairs(t) do local iface = PQ[1] local R, S = PQ[2].R, PQ[2].S r[#r + 1] = make_segment(iface, R, S) end return r end, } ================================================ FILE: examples/dwm/pulse-gauge.lua ================================================ local GAUGE_NCHARS = 10 local function mk_gauge(level, full, empty) local nfull = math.floor(level * GAUGE_NCHARS + 0.5) return full:rep(nfull) .. empty:rep(GAUGE_NCHARS - nfull) end widget = { plugin = 'pulse', cb = function(t) local level = t.cur / t.norm if t.mute then return mk_gauge(level, '×', '—') else return mk_gauge(level, '●', '○') end end, } ================================================ FILE: examples/dwm/pulse.lua ================================================ widget = { plugin = 'pulse', cb = function(t) if t.mute then return '[mute]' end local percent = (t.cur / t.norm) * 100 return string.format('[%3d%%]', math.floor(0.5 + percent)) end, } ================================================ FILE: examples/dwm/systemd-unit.lua ================================================ local function make_output(text) return string.format('[Tor: %s]', text) end widget = luastatus.require_plugin('systemd-unit').widget{ unit_name = 'tor.service', cb = function(state) if state == 'active' then return make_output('✓') elseif state == 'reloading' or state == 'activating' then return make_output('•') elseif state == 'inactive' or state == 'deactivating' then return make_output('-') elseif state == 'failed' then return make_output('x') else return make_output('?') end end, } ================================================ FILE: examples/dwm/time-battery-combined.lua ================================================ local function get_bat_seg(t) if not t then return '[--×--]' end if t.status == 'Unknown' or t.status == 'Full' or t.status == 'Not charging' then return nil end local sym = '?' if t.status == 'Discharging' then sym = '↓' elseif t.status == 'Charging' then sym = '↑' end return string.format('[%3d%%%s]', t.capacity, sym) end widget = luastatus.require_plugin('battery-linux').widget{ period = 2, cb = function(t) return { os.date('[%H:%M]'), get_bat_seg(t), } end, } ================================================ FILE: examples/dwm/time-date.lua ================================================ local months = { 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря', } widget = { plugin = 'timer', cb = function() local d = os.date('*t') return { string.format('%d %s', d.day, months[d.month]), string.format('%02d:%02d', d.hour, d.min), } end, } ================================================ FILE: examples/dwm/tor.lua ================================================ -- Trivial but somewhat useful widget showing if the Tor daemon is running. widget = { plugin = 'timer', opts = {period = 5}, cb = function() local f = io.open('/var/run/tor/tor.pid', 'r') if f then f:close() return '[TOR]' end end, } ================================================ FILE: examples/dwm/uptime-linux.lua ================================================ local SUFFIXES_AND_DIVISORS = { {'m', 60}, {'h', 60}, {'d', 24}, {'W', 7}, {'M', 30}, {'Y', 365 / 30}, } local function seconds_to_human_readable_time(sec) local prev_suffix = 's' local found_suffix for _, PQ in ipairs(SUFFIXES_AND_DIVISORS) do local suffix = PQ[1] local divisor = PQ[2] if sec < divisor then found_suffix = prev_suffix break end sec = sec / divisor prev_suffix = suffix end if not found_suffix then found_suffix = prev_suffix end return string.format('%.0f%s', sec, found_suffix) end widget = { plugin = 'timer', opts = { period = 2, }, cb = function(_) local f = io.open('/proc/uptime', 'r') local sec, _ = f:read('*number', '*number') f:close() assert(sec) return string.format('[uptime: %s]', seconds_to_human_readable_time(sec)) end, } ================================================ FILE: examples/dwm/weather-rus.lua ================================================ local LOCATION_NAME = 'Moscow' local TIMEOUT = 10 local INTERVAL = 9 local SLEEP_AFTER_INITIAL = 5 local coordinates = nil local function planner() -- Get coordinates of LOCATION_NAME. while true do -- Form the URL local url = string.format( 'https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1', luastatus.plugin.urlencode(LOCATION_NAME) ) -- Make request coroutine.yield({action = 'request', params = { url = url, timeout = TIMEOUT, }}) -- If successful, break if coordinates ~= nil then break end -- Failure; sleep and retry coroutine.yield({action = 'sleep', period = INTERVAL}) end -- Coordinates fetched, sleep for SLEEP_AFTER_INITIAL. coroutine.yield({action = 'sleep', period = SLEEP_AFTER_INITIAL}) -- Form the weather URL local weather_url = string.format( 'https://api.open-meteo.com/v1/forecast' .. '?latitude=%f' .. '&longitude=%f' .. '¤t_weather=true', coordinates.latitude, coordinates.longitude ) -- Fetch weather, periodically while true do -- Make request coroutine.yield({action = 'request', params = { url = weather_url, timeout = TIMEOUT, }}) -- Sleep and repeat coroutine.yield({action = 'sleep', period = INTERVAL}) end end local function check_error(t) if t.error then -- Low-level libcurl error return t.error end if t.status < 200 or t.status > 299 then -- Bad HTTP status return string.format('HTTP status %d', t.status) end -- Everything's OK return nil end local WEATHER_CODES = { [0] = 'Ясно', [1] = 'Преимущественно ясно', [2] = 'Переменная облачность', [3] = 'Пасмурно', [45] = 'Туман', [48] = 'Осаждение инея и тумана', [51] = 'Изморось, лёгкая', [53] = 'Изморось, умеренная', [55] = 'Изморось, сильная', [56] = 'Ледяная изморось, лёгкая', [57] = 'Ледяная изморось, сильная', [61] = 'Дождь, лёгкий', [63] = 'Дождь, умеренный', [65] = 'Дождь, сильный', [66] = 'Град, лёгкий', [67] = 'Град, сильный', [71] = 'Снегопад, лёгкий', [73] = 'Снегопад, умеренный', [75] = 'Снегопад, сильный', [77] = 'Крупинки снега', [80] = 'Ливень, лёгкий', [81] = 'Ливень, умеренный', [82] = 'Ливень, сильный', [85] = 'Снеговые осадки, лёгкие', [87] = 'Снеговые осадки, сильные', [95] = 'Гроза', [96] = 'Гроза, небольшой град', [99] = 'Гроза, сильный град', } local function fmt_weather(temp, _, _, weather_code) local segment1 = string.format('%.0f°', temp) -- segment2 is either string or nil local segment2 = WEATHER_CODES[weather_code] return {segment1, segment2} end widget = { plugin = 'web', opts = { planner = planner, }, cb = function(t) local err_msg = check_error(t) if err_msg then print(string.format('WARNING: luastatus: weather widget: %s'), err_msg) return '' end if coordinates == nil then local obj = assert(luastatus.plugin.json_decode(t.body)) local result = assert(obj.results[1]) local latitude = assert(result.latitude) local longitude = assert(result.longitude) coordinates = {latitude = latitude, longitude = longitude} return '...' end local obj = assert(luastatus.plugin.json_decode(t.body)) local cw = assert(obj.current_weather) local temp = assert(cw.temperature) local wind_speed = assert(cw.windspeed) -- is_day is an integer, either 0 or 1 local is_day = assert(cw.is_day) ~= 0 local weather_code = assert(cw.weathercode) return fmt_weather(temp, wind_speed, is_day, weather_code) end, } ================================================ FILE: examples/dwm/weather.lua ================================================ local LOCATION_NAME = 'Moscow' local TIMEOUT = 10 local INTERVAL = 9 local SLEEP_AFTER_INITIAL = 5 local coordinates = nil local function planner() -- Get coordinates of LOCATION_NAME. while true do -- Form the URL local url = string.format( 'https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1', luastatus.plugin.urlencode(LOCATION_NAME) ) -- Make request coroutine.yield({action = 'request', params = { url = url, timeout = TIMEOUT, }}) -- If successful, break if coordinates ~= nil then break end -- Failure; sleep and retry coroutine.yield({action = 'sleep', period = INTERVAL}) end -- Coordinates fetched, sleep for SLEEP_AFTER_INITIAL. coroutine.yield({action = 'sleep', period = SLEEP_AFTER_INITIAL}) -- Form the weather URL local weather_url = string.format( 'https://api.open-meteo.com/v1/forecast' .. '?latitude=%f' .. '&longitude=%f' .. '¤t_weather=true', coordinates.latitude, coordinates.longitude ) -- Fetch weather, periodically while true do -- Make request coroutine.yield({action = 'request', params = { url = weather_url, timeout = TIMEOUT, }}) -- Sleep and repeat coroutine.yield({action = 'sleep', period = INTERVAL}) end end local function check_error(t) if t.error then -- Low-level libcurl error return t.error end if t.status < 200 or t.status > 299 then -- Bad HTTP status return string.format('HTTP status %d', t.status) end -- Everything's OK return nil end local WEATHER_CODES = { [0] = 'Clear sky', [1] = 'Mainly clear', [2] = 'Partly cloudy', [3] = 'Overcast', [45] = 'Fog', [48] = 'Depositing rime fog', [51] = 'Drizzle, light', [53] = 'Drizzle, moderate', [55] = 'Drizzle, dense', [56] = 'Freezing drizzle, light', [57] = 'Freezing drizzle, dense', [61] = 'Rain, slight', [63] = 'Rain, moderate', [65] = 'Rain, heavy', [66] = 'Freezing rain, light', [67] = 'Freezing rain, heavy', [71] = 'Snow fall, slight', [73] = 'Snow fall, moderate', [75] = 'Snow fall, heavy', [77] = 'Snow grains', [80] = 'Rain shower, slight', [81] = 'Rain shower, moderate', [82] = 'Rain shower, violent', [85] = 'Snow showers, slight', [87] = 'Snow showers, heavy', [95] = 'Thunderstorm', [96] = 'Thunderstorm, slight hail', [99] = 'Thunderstorm, heavy hail', } local function fmt_weather(temp, _, _, weather_code) local segment1 = string.format('%.0f°', temp) -- segment2 is either string or nil local segment2 = WEATHER_CODES[weather_code] return {segment1, segment2} end widget = { plugin = 'web', opts = { planner = planner, }, cb = function(t) local err_msg = check_error(t) if err_msg then print(string.format('WARNING: luastatus: weather widget: %s'), err_msg) return '' end if coordinates == nil then local obj = assert(luastatus.plugin.json_decode(t.body)) local result = assert(obj.results[1]) local latitude = assert(result.latitude) local longitude = assert(result.longitude) coordinates = {latitude = latitude, longitude = longitude} return '...' end local obj = assert(luastatus.plugin.json_decode(t.body)) local cw = assert(obj.current_weather) local temp = assert(cw.temperature) local wind_speed = assert(cw.windspeed) -- is_day is an integer, either 0 or 1 local is_day = assert(cw.is_day) ~= 0 local weather_code = assert(cw.weathercode) return fmt_weather(temp, wind_speed, is_day, weather_code) end, } ================================================ FILE: examples/dwm/wireless.lua ================================================ local MIN_DBM, MAX_DBM = -90, -20 local NGAUGE = 5 local function round(x) return math.floor(x + 0.5) end local function make_wifi_gauge(dbm) if dbm < MIN_DBM then dbm = MIN_DBM end if dbm > MAX_DBM then dbm = MAX_DBM end local nbright = round(NGAUGE * (1 - 0.7 * (MAX_DBM - dbm) / (MAX_DBM - MIN_DBM))) return ('●'):rep(nbright) .. ('○'):rep(NGAUGE - nbright) end local function sorted_keys(tbl) local keys = {} for k, _ in pairs(tbl) do keys[#keys + 1] = k end table.sort(keys) return keys end widget = { plugin = 'network-linux', opts = { wireless = true, timeout = 10, }, cb = function(t) if not t then return nil end -- Sort for determinism local ifaces = sorted_keys(t) local r = {} for _, iface in ipairs(ifaces) do local params = t[iface] if params.wireless then if params.wireless.ssid then r[#r + 1] = params.wireless.ssid end if params.wireless.signal_dbm then r[#r + 1] = make_wifi_gauge(params.wireless.signal_dbm) end elseif iface ~= 'lo' and (params.ipv4 or params.ipv6) then r[#r + 1] = string.format('[%s]', iface) end end return r end, } ================================================ FILE: examples/dwm/xkb.lua ================================================ widget = { plugin = 'xkb', cb = function(t) if t.name then local base_layout = t.name:match('[^(]+') if base_layout == 'gb' or base_layout == 'us' then return '[En]' elseif base_layout == 'ru' then return '[Ru]' else return '[' .. base_layout:sub(1, 1):upper() .. base_layout:sub(2) .. ']' end else return '[? ID ' .. t.id .. ']' end end, } ================================================ FILE: examples/i3/alsa-gauge.lua ================================================ local GAUGE_NCHARS = 10 local function mk_gauge(level, full, empty) local nfull = math.floor(level * GAUGE_NCHARS + 0.5) return full:rep(nfull) .. empty:rep(GAUGE_NCHARS - nfull) end widget = { plugin = 'alsa', cb = function(t) local level = (t.vol.cur - t.vol.min) / (t.vol.max - t.vol.min) if t.mute then return {full_text=mk_gauge(level, '×', '—'), color='#e03838'} else return {full_text=mk_gauge(level, '●', '○'), color='#718ba6'} end end, } ================================================ FILE: examples/i3/alsa-interactive-gauge.lua ================================================ -- So, it looks like this: -- -- ••• | ██████████████ | [ 70%] | ••• -- -- ^ gauge block ^ text block -- -- The gauge block is initially hidden; click on the text block to toggle its visibility. -- Click anywhere at the gauge block to change the volume. -- Click with the right mouse button on either block to toggle mute state. local CARD = 'default' local CHANNEL = 'Master' local HBLOCKS = {' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'} local GAUGE_NCHARS = 20 -- i3bar adds the separator width to "relative_x" and "width" properties of a click event. -- -- So let's do some multivariable equations (we assume a monospace font is used). -- -- Let: -- * `x' be the separator width in pixels (unknown); -- * `y' be the width of one character in pixels (unknown); -- * `U' be the number of characters in the text block (known); -- * `V' be the number of characters in the gauge block (known); -- * `A' be the width, in pixels, of the text block (known when clicked); -- * `B' be the width, in pixels, on the gauge block (known when clicked). -- -- We have: -- { x + U*y = A; -- { x + V*y = B. -- So, -- y = (A - B) / (U - V); -- x = A - U*y. local last_t = nil local text_block_nchars, text_block_width = nil, nil local gauge = false local function round(x) return math.floor(x + 0.5) end local function mk_gauge(level) local rel_level = level * GAUGE_NCHARS local nfull = math.floor(rel_level) local filled = HBLOCKS[#HBLOCKS]:rep(nfull) if nfull == GAUGE_NCHARS then return filled end local mid_idx = round((rel_level - nfull) * (#HBLOCKS - 1)) return filled .. HBLOCKS[1 + mid_idx] .. HBLOCKS[1]:rep(GAUGE_NCHARS - nfull - 1) end widget = { plugin = 'alsa', opts = { card = CARD, channel = CHANNEL, make_self_pipe = true, }, cb = function(t) last_t = t local level = (t.vol.cur - t.vol.min) / (t.vol.max - t.vol.min) local r = {} if t.mute then r[2] = {full_text='[mute]', color='#e03838'} else r[2] = {full_text=string.format('[%3d%%]', round(level * 100)), color='#718ba6'} end text_block_nchars = #r[2].full_text -- please note this does not work with Unicode. if gauge then local fg, bg = '#dcdcdc', '#444444' if t.mute then fg, bg = '#e03838', '#4a1414' end r[1] = {full_text=mk_gauge(level), color=fg, background=bg, instance='gauge'} end return r end, event = function(t) if t.button == 1 then -- left mouse button if t.instance == 'gauge' then local char_width = round((t.width - text_block_width) / (GAUGE_NCHARS - text_block_nchars)) local sep_width = text_block_width - text_block_nchars * char_width local x = t.relative_x - sep_width if x < 0 then return end local rawvol = round( last_t.vol.min + x / (t.width - sep_width) * (last_t.vol.max - last_t.vol.min)) assert(luastatus.execute( string.format('amixer -D "%s" set "%s" %s >/dev/null', CARD, CHANNEL, rawvol))) else gauge = not gauge text_block_width = t.width luastatus.plugin.wake_up() end elseif t.button == 3 then -- right mouse button assert(luastatus.execute(string.format( 'amixer -D "%s" set "%s" toggle >/dev/null', CARD, CHANNEL))) end end, } ================================================ FILE: examples/i3/alsa.lua ================================================ widget = { plugin = 'alsa', cb = function(t) if t.mute then return {full_text = '[mute]', color = '#e03838'} else local percent = (t.vol.cur - t.vol.min) / (t.vol.max - t.vol.min) * 100 return {full_text = string.format('[%3d%%]', math.floor(0.5 + percent)), color = '#718ba6'} end end, } ================================================ FILE: examples/i3/backlight.lua ================================================ -- Note that this widget only shows backlight level when it changes. widget = luastatus.require_plugin('backlight-linux').widget{ cb = function(level) if level ~= nil then return {full_text = string.format('*%3.0f%%', level * 100)} end end, } ================================================ FILE: examples/i3/battery.lua ================================================ widget = luastatus.require_plugin('battery-linux').widget{ period = 2, cb = function(t) local symbol = ({ Charging = '↑', Discharging = '↓', })[t.status] or ' ' local rem_seg if t.rem_time then local h = math.floor(t.rem_time) local m = math.floor(60 * (t.rem_time - h)) rem_seg = {full_text = string.format('%2dh %02dm', h, m), color = '#595959'} end return { {full_text = string.format('%3d%%%s', t.capacity, symbol)}, rem_seg, } end, } ================================================ FILE: examples/i3/bluetooth.lua ================================================ -- A widget to display currently connected and paired bluetooth devices. -- To change output format modify reprint_devices function. if not luastatus.execute('command -v bluetoothctl >/dev/null') then error('"bluetoothctl" command, which is required for this widget to work, was not found') end local separator = " " -- Object paths look like /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/somethingsomething local function get_device_mac_address(device_object_path) return device_object_path:gsub("/.*/dev_", ""):gsub("/.*", ""):gsub("_", ":") end -- For reference bluetoothctl devices output looks like that: -- Device XX:XX:XX:XX:XX:XX JBL T450BT -- Device YY:YY:YY:YY:YY:YY Redmi 8 -- -- Function returns mac addresses of all devices. local function get_devices() local devices = {} local handle = io.popen(string.format("bluetoothctl devices")) for line in handle:lines() do local match = string.match(line, "Device ([%x:]+)") if match then table.insert(devices, match) end end handle:close() return devices end -- For reference bluetoothctl info output looks like that: -- Device XX:XX:XX:XX:XX:XX (public) -- Name: JBL T450BT -- Alias: JBL T450BT -- Class: 0xFFFFFFFF -- Icon: audio-card -- Paired: yes -- Trusted: yes -- Blocked: no -- Connected: yes -- LegacyPairing: no -- UUID: Headset (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) -- ... -- UUID: Handsfree (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) -- -- Given this input function returns a following table: -- [alias] string JBL T450BT -- [blocked] boolean false -- [class] string 0x00240404 -- [connected] boolean true -- [icon] string audio -- [legacypairing] boolean false -- [name] string JBL T450BT -- [paired] boolean true -- [trusted] boolean true local function get_device_info(mac_address) if mac_address == nil then mac_address = "" end assert(string.match(mac_address, '^[%x:]*$') ~= nil) local device_info = {} local handle = io.popen(string.format("bluetoothctl info %s", mac_address)) for line in handle:lines() do local key, value = string.match(line, "(%w+): (.*)") -- Filter junk if key ~= "UUID" and key ~= nil and value ~= nil then key = string.lower(key) if key ~= "name" and key ~= "alias" and key ~= "icon" then if value == "yes" then value = true end if value == "no" then value = false end end device_info[key] = value end end handle:close() return device_info end local devices = {} local function reprint_devices() local t = {} for mac_address, device in pairs(devices) do table.insert(t, string.format("%s(%s)", device["name"], mac_address)) end return table.concat(t, separator) end widget = { plugin = "dbus", opts = { greet = true, -- https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/device-api.txt signals = { { sender = "org.bluez", interface = "org.freedesktop.DBus.Properties", signal = "PropertiesChanged", arg0 = "org.bluez.Device1", bus = "system" } } }, cb = function(t) if t.what == "hello" then local mac_addresses = get_devices() for _, mac_address in ipairs(mac_addresses) do local device = get_device_info(mac_address) if device["connected"] and device["paired"] then devices[mac_address] = device end end elseif t.what == "signal" then -- For reference message from dbus looks like that: -- table -- [1] string org.bluez.Device1 -- [2] table -- [2] [1] table -- [2] [1] [1] string SomethingSomething -- [2] [1] [2] boolean false -- [2] [2] table -- [2] [2] [1] string Connected -- [2] [2] [2] boolean true -- [3] table if t.signal == "PropertiesChanged" then for _, message in pairs(t.parameters[2]) do if message[1] == "Connected" or message[1] == "Paired" then local mac_address = get_device_mac_address(t.object_path) if message[2] then local device = get_device_info(mac_address) if device["paired"] then devices[mac_address] = device end else devices[mac_address] = nil end end end end end return {full_text = reprint_devices()} end } ================================================ FILE: examples/i3/btc-price.lua ================================================ -- Bitcoin price widget. -- Updates on click and every 5 minutes. local custom_sleep_amt = nil local function check_error(t) if t.error then -- Low-level libcurl error return t.error end if t.status < 200 or t.status > 299 then -- Bad HTTP status return string.format('HTTP status %d', t.status) end -- Everything's OK return nil end widget = { plugin = 'web', opts = { planner = function() while true do coroutine.yield({action = 'request', params = { url = 'https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT', timeout = 5, }}) local period = custom_sleep_amt or (5 * 60) custom_sleep_amt = nil coroutine.yield({action = 'sleep', period = period}) end end, make_self_pipe = true, }, cb = function(t) local text local err_msg = check_error(t) if err_msg then print(string.format('WARNING: luastatus: btc-price widget: %s'), err_msg) text = '......' custom_sleep_amt = 5 else local obj = assert(luastatus.plugin.json_decode(t.body)) text = obj.price:match('[^.]+') end return { full_text = string.format( '[$%s]', luastatus.barlib.pango_escape(text) ), color = '#586A4B', markup = 'pango', } end, event = function(t) if t.button == 1 then luastatus.plugin.wake_up() end end, } ================================================ FILE: examples/i3/cpu-freq.lua ================================================ local VBLOCKS = {'▂', '▃', '▄', '▅', '▆', '▇', '█'} local function make_color(ratio) local red = math.floor(0.5 + 255 * ratio) return string.format('#%02x%02x00', red, 255 - red) end local function make_chunk(entry) local num = entry.cur - entry.min local denom = entry.max - entry.min local ratio if denom ~= 0 then ratio = num / denom else -- If max_freq == min_freq, set ratio to zero. ratio = 0 end local vblock_idx = math.min( 1 + math.floor(0.5 + ratio * #VBLOCKS), #VBLOCKS) return string.format("%s", make_color(ratio), VBLOCKS[vblock_idx]) end local plugin_data = {} local plugin_params = { timer_opts = { period = 2, }, cb = function(t) if t == nil then return nil end local r = {} for _, entry in ipairs(t) do r[#r + 1] = make_chunk(entry) end return {full_text = table.concat(r), markup = 'pango'} end, event = function(t) if t.button == 1 then plugin_data.please_reload = true end end, } widget = luastatus.require_plugin('cpu-freq-linux').widget(plugin_params, plugin_data) ================================================ FILE: examples/i3/cpu-temperature.lua ================================================ local COOL_TEMP = 50 local HEAT_TEMP = 75 local function getcolor(temp) local t = (temp - COOL_TEMP) / (HEAT_TEMP - COOL_TEMP) if t < 0 then t = 0 end if t > 1 then t = 1 end local red = math.floor(t * 255 + 0.5) return string.format('#%02x%02x00', red, 255 - red) end local plugin_data = {} local plugin_params = { timer_opts = { period = 2, }, cb = function(t) if not t then return nil end local r = {} for _, entry in ipairs(t) do local temp = entry.value r[#r + 1] = {full_text = string.format('%.0f°', temp), color = getcolor(temp)} end return r end, event = function(t) if t.button == 1 then plugin_data.please_reload = true end end, } widget = luastatus.require_plugin('cpu-temp-linux').widget(plugin_params, plugin_data) ================================================ FILE: examples/i3/cpu-usage.lua ================================================ widget = luastatus.require_plugin('cpu-usage-linux').widget{ cb = function(usage) if usage ~= nil then return {full_text = string.format('[%5.1f%%]', usage * 100)} end end, } ================================================ FILE: examples/i3/disk-io.lua ================================================ widget = luastatus.require_plugin('disk-io-linux').widget{ period = 2, cb = function(t) -- Sort by name for determinism table.sort(t, function(a, b) return a.name < b.name end) local segments = {} for _, entry in ipairs(t) do local R = entry.read_bytes local W = entry.written_bytes if (R >= 0) and (W >= 0) then segments[#segments + 1] = { full_text = string.format( '%s: %.0fk↓ %.0fk↑', entry.name, R / 1024, W / 1024 ), markup = 'pango', } end end return segments end, } ================================================ FILE: examples/i3/file-contents.lua ================================================ widget = luastatus.require_plugin('file-contents-linux').widget{ filename = os.getenv('HOME') .. '/status', cb = function(f) -- show the first line of the file return {full_text = f:read('*line')} end, } ================================================ FILE: examples/i3/fs.lua ================================================ local function sorted_keys(tbl) local keys = {} for k, _ in pairs(tbl) do keys[#keys + 1] = k end table.sort(keys) return keys end widget = { plugin = 'fs', opts = { paths = {'/', '/home'}, }, cb = function(t) -- Sort for determinism local keys = sorted_keys(t) local res = {} for _, k in ipairs(keys) do local v = t[k] table.insert(res, { full_text = string.format('%s %.0f%%', k, (1 - v.avail / v.total) * 100), instance = k, }) end return res end, } ================================================ FILE: examples/i3/gmail.lua ================================================ --[[ -- Expects 'credentials.lua' to be present in the current directory; it may contain, e.g., -- return { -- gmail = { -- login = 'john.smith', -- password = 'qwerty' -- } -- } --]] local credentials = require 'credentials' widget = luastatus.require_plugin('imap').widget{ verbose = false, host = 'imap.gmail.com', port = 993, mailbox = 'Inbox', use_ssl = true, timeout = 2 * 60, handshake_timeout = 10, login = credentials.gmail.login, password = credentials.gmail.password, error_sleep_period = 60, cb = function(unseen) if unseen == nil then return nil elseif unseen == 0 then return {full_text = '[-]', color = '#595959'} else return {full_text = string.format('[%d unseen]', unseen)} end end, event = [[ local t = ... if t.button == 1 then os.execute('xdg-open https://gmail.com &') end ]], } ================================================ FILE: examples/i3/ip.lua ================================================ widget = { plugin = 'network-linux', cb = function(t) local r = {} for iface, params in pairs(t) do local iface_clean = iface local addr = params.ipv6 or params.ipv4 if addr then -- strip out "label" from the interface name iface_clean = iface_clean:gsub(':.*', '') -- strip out "zone index" from the address addr = addr:gsub('%%.*', '') if iface_clean ~= 'lo' then r[#r + 1] = { full_text = string.format('[%s: %s]', iface_clean, addr), color = '#709080', } end end end return r end, } ================================================ FILE: examples/i3/loadavg-linux.lua ================================================ local function get_ncpus() local f = assert(io.open('/proc/cpuinfo', 'r')) local n = 0 for line in f:lines() do if line:match('^processor\t') then n = n + 1 end end f:close() return n end local function avg2str(x) assert(x >= 0) if x >= 1000 then return '↑↑↑' end return string.format('%3.0f', x) end widget = { plugin = 'timer', opts = { period = 2, }, cb = function(_) local f = io.open('/proc/loadavg', 'r') local avg1, avg5, avg15 = f:read('*number', '*number', '*number') f:close() assert(avg1 and avg5 and avg15) local ncpus = get_ncpus() return {full_text = string.format( '[%s%% %s%% %s%%]', avg2str(avg1 / ncpus * 100), avg2str(avg5 / ncpus * 100), avg2str(avg15 / ncpus * 100) )} end, } ================================================ FILE: examples/i3/media-player-mpris.lua ================================================ local PLAYER = 'clementine' local PLAYBACK_STATUS_ICONS = { Playing = '▶', Paused = '◆', Stopped = '—', } local COLOR_BRIGHT = '#60b177' local COLOR_DIM = '#9fb15d' local function fetch_metadata_field(t, key) if t.Metadata then return t.Metadata[key] else return nil end end local function do_call_method(method_name) local is_ok, err_msg = luastatus.plugin.call_method_str({ bus = 'session', dest = 'org.mpris.MediaPlayer2.' .. PLAYER, object_path = '/org/mpris/MediaPlayer2', interface = 'org.mpris.MediaPlayer2.Player', method = method_name, -- no arg_str }) if not is_ok then print(string.format( 'WARNING: luastatus: mpris widget: cannot call method "%s": %s', method_name, err_msg )) end end widget = luastatus.require_plugin('mpris').widget{ player = PLAYER, cb = function(t) if not t.PlaybackStatus then return nil end local title = fetch_metadata_field(t, 'xesam:title') title = title or '' title = luastatus.libwidechar.make_valid_and_printable(title, '?') title = luastatus.libwidechar.truncate_to_width(title, 40) title = title or '' local icon = PLAYBACK_STATUS_ICONS[t.PlaybackStatus] or '?' local segments = { {full_text = '←', color = COLOR_DIM, instance = 'prev'}, {full_text = icon, color = COLOR_BRIGHT, instance = 'status'}, {full_text = '→', color = COLOR_DIM, instance = 'next'}, } if title ~= '' then table.insert(segments, { full_text = title, color = COLOR_BRIGHT, }) end return segments end, event = function(t) if t.button ~= 1 then return end if t.instance == 'status' then do_call_method('PlayPause') elseif t.instance == 'prev' then do_call_method('Previous') elseif t.instance == 'next' then do_call_method('Next') end end, } ================================================ FILE: examples/i3/mem-usage.lua ================================================ widget = luastatus.require_plugin('mem-usage-linux').widget{ timer_opts = {period = 2}, cb = function(t) local used_kb = t.total.value - t.avail.value return {full_text = string.format('[%3.2f GiB]', used_kb / 1024 / 1024), color = '#af8ec3'} end, } ================================================ FILE: examples/i3/mpd-progressbar.lua ================================================ local text, time, total, is_playing = nil, nil, nil, false local timeout = 2 local titlewidth = 40 local function W_split(str, boundary) local head, _ = luastatus.libwidechar.truncate_to_width(str, boundary) assert(head) local tail = str:sub(1 + #head) return head, tail end widget = { plugin = 'mpd', opts = { timeout = timeout, }, cb = function(t) if t.what == 'update' then -- build title local title if t.song.Title then title = t.song.Title if t.song.Artist then title = t.song.Artist .. ': ' .. title end else title = t.song.file or '' end title = luastatus.libwidechar.make_valid(title, '?') if assert(luastatus.libwidechar.width(title)) > titlewidth then title = luastatus.libwidechar.truncate_to_width(title, titlewidth - 1) .. '…' end -- build text text = string.format('%s %s', ({play = '▶', pause = '‖', stop = '■'})[t.status.state], title ) -- update other globals if t.status.time then time, total = t.status.time:match('(%d+):(%d+)') time, total = tonumber(time), tonumber(total) else time, total = nil, nil end is_playing = t.status.state == 'play' elseif t.what == 'timeout' then if is_playing then time = math.min(time + timeout, total) end else -- 'connecting' or 'error' return {full_text = t.what} end -- calc progress local width = assert(luastatus.libwidechar.width(text)) -- 'time' and 'total' can be nil here, we check for 'total' only local ulpos = (total and total ~= 0) and math.floor(width / total * time + 0.5) or 0 local head, tail = W_split(text, ulpos) return {full_text = '' .. luastatus.barlib.pango_escape(head) .. '' .. luastatus.barlib.pango_escape(tail), markup = 'pango'} end } ================================================ FILE: examples/i3/network-rate.lua ================================================ local function make_segment(iface, R, S) return { full_text = string.format('[%s %.0fk↓ %.0fk↑]', iface, R / 1000, S / 1000 ), markup = 'pango', } end widget = luastatus.require_plugin('network-rate-linux').widget{ iface_except = 'lo', period = 3, in_array_form = true, cb = function(t) local r = {} for _, PQ in ipairs(t) do local iface = PQ[1] local R, S = PQ[2].R, PQ[2].S r[#r + 1] = make_segment(iface, R, S) end return r end, } ================================================ FILE: examples/i3/pulse-gauge.lua ================================================ local GAUGE_NCHARS = 10 local function mk_gauge(level, full, empty) local nfull = math.floor(level * GAUGE_NCHARS + 0.5) return full:rep(nfull) .. empty:rep(GAUGE_NCHARS - nfull) end widget = { plugin = 'pulse', cb = function(t) local level = t.cur / t.norm if t.mute then return {full_text=mk_gauge(level, '×', '—'), color='#e03838'} else return {full_text=mk_gauge(level, '●', '○'), color='#718ba6'} end end, } ================================================ FILE: examples/i3/pulse-interactive-gauge.lua ================================================ -- So, it looks like this: -- -- ••• | ██████████████ | [ 70%] | ••• -- -- ^ gauge block ^ text block -- -- The gauge block is initially hidden; click on the text block to toggle its visibility. -- Click anywhere at the gauge block to change the volume. -- Click with the right mouse button on either block to toggle mute state. local SINK = '@DEFAULT_SINK@' local HBLOCKS = {' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'} local GAUGE_NCHARS = 20 -- i3bar adds the separator width to "relative_x" and "width" properties of a click event. -- -- So let's do some multivariable equations (we assume a monospace font is used). -- -- Let: -- * `x' be the separator width in pixels (unknown); -- * `y' be the width of one character in pixels (unknown); -- * `U' be the number of characters in the text block (known); -- * `V' be the number of characters in the gauge block (known); -- * `A' be the width, in pixels, of the text block (known when clicked); -- * `B' be the width, in pixels, on the gauge block (known when clicked). -- -- We have: -- { x + U*y = A; -- { x + V*y = B. -- So, -- y = (A - B) / (U - V); -- x = A - U*y. local last_t = nil local text_block_nchars, text_block_width = nil, nil local gauge = false local function round(x) return math.floor(x + 0.5) end local function mk_gauge(level) local rel_level = level * GAUGE_NCHARS local nfull = math.floor(rel_level) local filled = HBLOCKS[#HBLOCKS]:rep(nfull) if nfull == GAUGE_NCHARS then return filled end local mid_idx = round((rel_level - nfull) * (#HBLOCKS - 1)) return filled .. HBLOCKS[1 + mid_idx] .. HBLOCKS[1]:rep(GAUGE_NCHARS - nfull - 1) end widget = { plugin = 'pulse', opts = { sink = SINK, make_self_pipe = true, }, cb = function(t) if t == nil then t = last_t else last_t = t end local level = t.cur / t.norm local r = {} if t.mute then r[2] = {full_text='[mute]', color='#e03838'} else r[2] = {full_text=string.format('[%3d%%]', round(level * 100)), color='#718ba6'} end text_block_nchars = #r[2].full_text -- please note this does not work with Unicode. if gauge then local fg, bg = '#dcdcdc', '#444444' if t.mute then fg, bg = '#e03838', '#4a1414' end r[1] = {full_text=mk_gauge(level), color=fg, background=bg, instance='gauge'} end return r end, event = function(t) if t.button == 1 then -- left mouse button if t.instance == 'gauge' then local char_width = round((t.width - text_block_width) / (GAUGE_NCHARS - text_block_nchars)) local sep_width = text_block_width - text_block_nchars * char_width local x = t.relative_x - sep_width if x < 0 then return end local rawvol = round(x / (t.width - sep_width) * last_t.norm) os.execute(string.format( 'pactl set-sink-volume "%s" %s', SINK, rawvol)) else gauge = not gauge text_block_width = t.width luastatus.plugin.wake_up() end elseif t.button == 3 then -- right mouse button os.execute(string.format( 'pactl set-sink-mute "%s" toggle', SINK)) end end, } ================================================ FILE: examples/i3/pulse.lua ================================================ widget = { plugin = 'pulse', cb = function(t) if t.mute then return {full_text = '[mute]', color = '#e03838'} end local percent = (t.cur / t.norm) * 100 return {full_text = string.format('[%3d%%]', math.floor(0.5 + percent)), color = '#718ba6'} end, } ================================================ FILE: examples/i3/systemd-unit.lua ================================================ local COLORS = { pink = '#ec93d3', green = '#60b48a', yellow = '#dfaf8f', red = '#dca3a3', dim = '#909090', } local function make_output(text, color_name) local color = assert(COLORS[color_name]) local body = string.format('[Tor: %s]', color, text) return {full_text = body, markup = 'pango'} end widget = luastatus.require_plugin('systemd-unit').widget{ unit_name = 'tor.service', cb = function(state) if state == 'active' then return make_output('✓', 'green') elseif state == 'reloading' or state == 'activating' then return make_output('•', 'yellow') elseif state == 'inactive' or state == 'deactivating' then return make_output('-', 'dim') elseif state == 'failed' then return make_output('x', 'red') else return make_output('?', 'pink') end end, } ================================================ FILE: examples/i3/time-battery-combined.lua ================================================ local function get_bat_seg(t) if not t then return {full_text = '[--×--]'} end if t.status == 'Unknown' or t.status == 'Full' or t.status == 'Not charging' then return nil end local sym, color = '?', '#dcdcdc' if t.status == 'Discharging' then sym = '↓' color = '#dca3a3' elseif t.status == 'Charging' then sym = '↑' color = '#60b48a' end return {full_text = string.format('[%3d%%%s]', t.capacity, sym), color = color} end widget = luastatus.require_plugin('battery-linux').widget{ period = 2, cb = function(t) return { {full_text = os.date('[%H:%M]'), color = '#dc8cc3'}, get_bat_seg(t), } end, } ================================================ FILE: examples/i3/time-date.lua ================================================ local months = {'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'} widget = { plugin = 'timer', cb = function() local d = os.date('*t') return { {full_text = string.format('%d %s', d.day, months[d.month])}, {full_text = string.format('%02d:%02d', d.hour, d.min)}, } end, } ================================================ FILE: examples/i3/tor.lua ================================================ -- Trivial but somewhat useful widget showing if the Tor daemon is running. widget = { plugin = 'timer', opts = {period = 5}, cb = function() local f = io.open('/var/run/tor/tor.pid', 'r') if f then f:close() return {full_text = '[TOR]'} else return nil end end, } ================================================ FILE: examples/i3/update-on-click.lua ================================================ widget = { plugin = 'timer', opts = { make_self_pipe = true, period = 2.0, }, cb = function(t) if t == 'self_pipe' then return {full_text = 'Thanks!'} else return {full_text = 'Click me'} end end, event = function(_) luastatus.plugin.wake_up() end, } ================================================ FILE: examples/i3/uptime-linux.lua ================================================ local SUFFIXES_AND_DIVISORS = { {'m', 60}, {'h', 60}, {'d', 24}, {'W', 7}, {'M', 30}, {'Y', 365 / 30}, } local function seconds_to_human_readable_time(sec) local prev_suffix = 's' local found_suffix for _, PQ in ipairs(SUFFIXES_AND_DIVISORS) do local suffix = PQ[1] local divisor = PQ[2] if sec < divisor then found_suffix = prev_suffix break end sec = sec / divisor prev_suffix = suffix end if not found_suffix then found_suffix = prev_suffix end return string.format('%.0f%s', sec, found_suffix) end widget = { plugin = 'timer', opts = { period = 2, }, cb = function(_) local f = io.open('/proc/uptime', 'r') local sec, _ = f:read('*number', '*number') f:close() assert(sec) return {full_text = string.format( '[uptime: %s]', seconds_to_human_readable_time(sec) )} end, } ================================================ FILE: examples/i3/weather-rus.lua ================================================ local LOCATION_NAME = 'Moscow' local TIMEOUT = 10 local INTERVAL = 9 local SLEEP_AFTER_INITIAL = 5 local coordinates = nil local function planner() -- Get coordinates of LOCATION_NAME. while true do -- Form the URL local url = string.format( 'https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1', luastatus.plugin.urlencode(LOCATION_NAME) ) -- Make request coroutine.yield({action = 'request', params = { url = url, timeout = TIMEOUT, }}) -- If successful, break if coordinates ~= nil then break end -- Failure; sleep and retry coroutine.yield({action = 'sleep', period = INTERVAL}) end -- Coordinates fetched, sleep for SLEEP_AFTER_INITIAL. coroutine.yield({action = 'sleep', period = SLEEP_AFTER_INITIAL}) -- Form the weather URL local weather_url = string.format( 'https://api.open-meteo.com/v1/forecast' .. '?latitude=%f' .. '&longitude=%f' .. '¤t_weather=true', coordinates.latitude, coordinates.longitude ) -- Fetch weather, periodically while true do -- Make request coroutine.yield({action = 'request', params = { url = weather_url, timeout = TIMEOUT, }}) -- Sleep and repeat coroutine.yield({action = 'sleep', period = INTERVAL}) end end local function check_error(t) if t.error then -- Low-level libcurl error return t.error end if t.status < 200 or t.status > 299 then -- Bad HTTP status return string.format('HTTP status %d', t.status) end -- Everything's OK return nil end local WEATHER_CODES = { [0] = 'Ясно', [1] = 'Преимущественно ясно', [2] = 'Переменная облачность', [3] = 'Пасмурно', [45] = 'Туман', [48] = 'Осаждение инея и тумана', [51] = 'Изморось, лёгкая', [53] = 'Изморось, умеренная', [55] = 'Изморось, сильная', [56] = 'Ледяная изморось, лёгкая', [57] = 'Ледяная изморось, сильная', [61] = 'Дождь, лёгкий', [63] = 'Дождь, умеренный', [65] = 'Дождь, сильный', [66] = 'Град, лёгкий', [67] = 'Град, сильный', [71] = 'Снегопад, лёгкий', [73] = 'Снегопад, умеренный', [75] = 'Снегопад, сильный', [77] = 'Крупинки снега', [80] = 'Ливень, лёгкий', [81] = 'Ливень, умеренный', [82] = 'Ливень, сильный', [85] = 'Снеговые осадки, лёгкие', [87] = 'Снеговые осадки, сильные', [95] = 'Гроза', [96] = 'Гроза, небольшой град', [99] = 'Гроза, сильный град', } local function fmt_weather(temp, _, _, weather_code) local segment1 = {full_text = string.format('%.0f°', temp)} local segment2 = nil local weather = WEATHER_CODES[weather_code] if weather then segment2 = {full_text = weather} end return {segment1, segment2} end widget = { plugin = 'web', opts = { planner = planner, }, cb = function(t) local err_msg = check_error(t) if err_msg then print(string.format('WARNING: luastatus: weather widget: %s'), err_msg) return {full_text = '...', color = '#e03838'} end if coordinates == nil then local obj = assert(luastatus.plugin.json_decode(t.body)) local result = assert(obj.results[1]) local latitude = assert(result.latitude) local longitude = assert(result.longitude) coordinates = {latitude = latitude, longitude = longitude} return {full_text = '...', color = '#709080'} end local obj = assert(luastatus.plugin.json_decode(t.body)) local cw = assert(obj.current_weather) local temp = assert(cw.temperature) local wind_speed = assert(cw.windspeed) -- is_day is an integer, either 0 or 1 local is_day = assert(cw.is_day) ~= 0 local weather_code = assert(cw.weathercode) return fmt_weather(temp, wind_speed, is_day, weather_code) end, } ================================================ FILE: examples/i3/weather.lua ================================================ local LOCATION_NAME = 'Moscow' local TIMEOUT = 10 local INTERVAL = 9 local SLEEP_AFTER_INITIAL = 5 local coordinates = nil local function planner() -- Get coordinates of LOCATION_NAME. while true do -- Form the URL local url = string.format( 'https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1', luastatus.plugin.urlencode(LOCATION_NAME) ) -- Make request coroutine.yield({action = 'request', params = { url = url, timeout = TIMEOUT, }}) -- If successful, break if coordinates ~= nil then break end -- Failure; sleep and retry coroutine.yield({action = 'sleep', period = INTERVAL}) end -- Coordinates fetched, sleep for SLEEP_AFTER_INITIAL. coroutine.yield({action = 'sleep', period = SLEEP_AFTER_INITIAL}) -- Form the weather URL local weather_url = string.format( 'https://api.open-meteo.com/v1/forecast' .. '?latitude=%f' .. '&longitude=%f' .. '¤t_weather=true', coordinates.latitude, coordinates.longitude ) -- Fetch weather, periodically while true do -- Make request coroutine.yield({action = 'request', params = { url = weather_url, timeout = TIMEOUT, }}) -- Sleep and repeat coroutine.yield({action = 'sleep', period = INTERVAL}) end end local function check_error(t) if t.error then -- Low-level libcurl error return t.error end if t.status < 200 or t.status > 299 then -- Bad HTTP status return string.format('HTTP status %d', t.status) end -- Everything's OK return nil end local WEATHER_CODES = { [0] = 'Clear sky', [1] = 'Mainly clear', [2] = 'Partly cloudy', [3] = 'Overcast', [45] = 'Fog', [48] = 'Depositing rime fog', [51] = 'Drizzle, light', [53] = 'Drizzle, moderate', [55] = 'Drizzle, dense', [56] = 'Freezing drizzle, light', [57] = 'Freezing drizzle, dense', [61] = 'Rain, slight', [63] = 'Rain, moderate', [65] = 'Rain, heavy', [66] = 'Freezing rain, light', [67] = 'Freezing rain, heavy', [71] = 'Snow fall, slight', [73] = 'Snow fall, moderate', [75] = 'Snow fall, heavy', [77] = 'Snow grains', [80] = 'Rain shower, slight', [81] = 'Rain shower, moderate', [82] = 'Rain shower, violent', [85] = 'Snow showers, slight', [87] = 'Snow showers, heavy', [95] = 'Thunderstorm', [96] = 'Thunderstorm, slight hail', [99] = 'Thunderstorm, heavy hail', } local function fmt_weather(temp, _, _, weather_code) local segment1 = {full_text = string.format('%.0f°', temp)} local segment2 = nil local weather = WEATHER_CODES[weather_code] if weather then segment2 = {full_text = weather} end return {segment1, segment2} end widget = { plugin = 'web', opts = { planner = planner, }, cb = function(t) local err_msg = check_error(t) if err_msg then print(string.format('WARNING: luastatus: weather widget: %s'), err_msg) return {full_text = '...', color = '#e03838'} end if coordinates == nil then local obj = assert(luastatus.plugin.json_decode(t.body)) local result = assert(obj.results[1]) local latitude = assert(result.latitude) local longitude = assert(result.longitude) coordinates = {latitude = latitude, longitude = longitude} return {full_text = '...', color = '#709080'} end local obj = assert(luastatus.plugin.json_decode(t.body)) local cw = assert(obj.current_weather) local temp = assert(cw.temperature) local wind_speed = assert(cw.windspeed) -- is_day is an integer, either 0 or 1 local is_day = assert(cw.is_day) ~= 0 local weather_code = assert(cw.weathercode) return fmt_weather(temp, wind_speed, is_day, weather_code) end, } ================================================ FILE: examples/i3/wireless.lua ================================================ local ORIGIN = '•' -- '●' '○' '◌' '◍' '⬤' local RAY = '}' -- '›' '〉' '❭' '❯' '❱' '⟩' '⟫' '》' local MIN_DBM, MAX_DBM = -90, -20 local NGAUGE = 5 local COLOR_DIM = '#709080' local function round(x) return math.floor(x + 0.5) end local function make_wifi_gauge(dbm) if dbm < MIN_DBM then dbm = MIN_DBM end if dbm > MAX_DBM then dbm = MAX_DBM end local nbright = round(NGAUGE * (1 - 0.7 * (MAX_DBM - dbm) / (MAX_DBM - MIN_DBM))) return { full_text = string.format( '%s%s%s', ORIGIN, RAY:rep(nbright), COLOR_DIM, RAY:rep(NGAUGE - nbright) ), markup = 'pango', } end local function sorted_keys(tbl) local keys = {} for k, _ in pairs(tbl) do keys[#keys + 1] = k end table.sort(keys) return keys end widget = { plugin = 'network-linux', opts = { wireless = true, timeout = 10, }, cb = function(t) if not t then return nil end -- Sort for determinism local ifaces = sorted_keys(t) local r = {} for _, iface in ipairs(ifaces) do local params = t[iface] if params.wireless then if params.wireless.ssid then r[#r + 1] = { full_text = params.wireless.ssid, color = COLOR_DIM, } end if params.wireless.signal_dbm then r[#r + 1] = make_wifi_gauge(params.wireless.signal_dbm) end elseif iface ~= 'lo' and (params.ipv4 or params.ipv6) then r[#r + 1] = { full_text = string.format('[%s]', iface), color = COLOR_DIM, } end end return r end, } ================================================ FILE: examples/i3/xkb.lua ================================================ widget = { plugin = 'xkb', cb = function(t) if t.name then local base_layout = t.name:match('[^(]+') if base_layout == 'gb' or base_layout == 'us' then return {full_text = '[En]', color = '#9c9c9c'} elseif base_layout == 'ru' then return {full_text = '[Ru]', color = '#eab93d'} else return {full_text = '[' .. base_layout:sub(1, 1):upper() .. base_layout:sub(2) .. ']'} end else return {full_text = '[? ID ' .. t.id .. ']'} end end, } ================================================ FILE: examples/lemonbar/alsa-gauge.lua ================================================ local GAUGE_NCHARS = 10 local function mk_gauge(level, full, empty) local nfull = math.floor(level * GAUGE_NCHARS + 0.5) return full:rep(nfull) .. empty:rep(GAUGE_NCHARS - nfull) end widget = { plugin = 'alsa', cb = function(t) local level = (t.vol.cur - t.vol.min) / (t.vol.max - t.vol.min) if t.mute then return "%{F#e03838}" .. mk_gauge(level, '×', '—') .. "%{F-}" else return "%{F#718ba6}" .. mk_gauge(level, '●', '○') .. "%{F-}" end end, } ================================================ FILE: examples/lemonbar/alsa.lua ================================================ widget = { plugin = 'alsa', cb = function(t) if t.mute then return "%{F#e03838}" .. '[mute]' .. "%{F-}" else local percent = (t.vol.cur - t.vol.min) / (t.vol.max - t.vol.min) * 100 local body = string.format('[%3d%%]', math.floor(0.5 + percent)) return "%{F#718ba6}" .. luastatus.barlib.escape(body) .. "%{F-}" end end, } ================================================ FILE: examples/lemonbar/backlight.lua ================================================ -- Note that this widget only shows backlight level when it changes. widget = luastatus.require_plugin('backlight-linux').widget{ cb = function(level) if level ~= nil then local body = string.format('*%3.0f%%', level * 100) return luastatus.barlib.escape(body) end end, } ================================================ FILE: examples/lemonbar/battery.lua ================================================ widget = luastatus.require_plugin('battery-linux').widget{ period = 2, cb = function(t) local symbol = ({ Charging = '↑', Discharging = '↓', })[t.status] or ' ' local rem_seg if t.rem_time then local h = math.floor(t.rem_time) local m = math.floor(60 * (t.rem_time - h)) rem_seg = "%{F#595959}" .. string.format('%2dh %02dm', h, m) .. "%{F-}" end return { luastatus.barlib.escape(string.format('%3d%%%s', t.capacity, symbol)), rem_seg, } end, } ================================================ FILE: examples/lemonbar/bluetooth.lua ================================================ -- A widget to display currently connected and paired bluetooth devices. -- To change output format modify reprint_devices function. if not luastatus.execute('command -v bluetoothctl >/dev/null') then error('"bluetoothctl" command, which is required for this widget to work, was not found') end local separator = " " -- Object paths look like /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/somethingsomething local function get_device_mac_address(device_object_path) return device_object_path:gsub("/.*/dev_", ""):gsub("/.*", ""):gsub("_", ":") end -- For reference bluetoothctl devices output looks like that: -- Device XX:XX:XX:XX:XX:XX JBL T450BT -- Device YY:YY:YY:YY:YY:YY Redmi 8 -- -- Function returns mac addresses of all devices. local function get_devices() local devices = {} local handle = io.popen(string.format("bluetoothctl devices")) for line in handle:lines() do local match = string.match(line, "Device ([%x:]+)") if match then table.insert(devices, match) end end handle:close() return devices end -- For reference bluetoothctl info output looks like that: -- Device XX:XX:XX:XX:XX:XX (public) -- Name: JBL T450BT -- Alias: JBL T450BT -- Class: 0xFFFFFFFF -- Icon: audio-card -- Paired: yes -- Trusted: yes -- Blocked: no -- Connected: yes -- LegacyPairing: no -- UUID: Headset (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) -- ... -- UUID: Handsfree (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) -- -- Given this input function returns a following table: -- [alias] string JBL T450BT -- [blocked] boolean false -- [class] string 0x00240404 -- [connected] boolean true -- [icon] string audio -- [legacypairing] boolean false -- [name] string JBL T450BT -- [paired] boolean true -- [trusted] boolean true local function get_device_info(mac_address) if mac_address == nil then mac_address = "" end assert(string.match(mac_address, '^[%x:]*$') ~= nil) local device_info = {} local handle = io.popen(string.format("bluetoothctl info %s", mac_address)) for line in handle:lines() do local key, value = string.match(line, "(%w+): (.*)") -- Filter junk if key ~= "UUID" and key ~= nil and value ~= nil then key = string.lower(key) if key ~= "name" and key ~= "alias" and key ~= "icon" then if value == "yes" then value = true end if value == "no" then value = false end end device_info[key] = value end end handle:close() return device_info end local devices = {} local function reprint_devices() local t = {} for mac_address, device in pairs(devices) do table.insert(t, string.format("%s(%s)", device["name"], mac_address)) end return table.concat(t, separator) end widget = { plugin = "dbus", opts = { greet = true, -- https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/device-api.txt signals = { { sender = "org.bluez", interface = "org.freedesktop.DBus.Properties", signal = "PropertiesChanged", arg0 = "org.bluez.Device1", bus = "system" } } }, cb = function(t) if t.what == "hello" then local mac_addresses = get_devices() for _, mac_address in ipairs(mac_addresses) do local device = get_device_info(mac_address) if device["connected"] and device["paired"] then devices[mac_address] = device end end elseif t.what == "signal" then -- For reference message from dbus looks like that: -- table -- [1] string org.bluez.Device1 -- [2] table -- [2] [1] table -- [2] [1] [1] string SomethingSomething -- [2] [1] [2] boolean false -- [2] [2] table -- [2] [2] [1] string Connected -- [2] [2] [2] boolean true -- [3] table if t.signal == "PropertiesChanged" then for _, message in pairs(t.parameters[2]) do if message[1] == "Connected" or message[1] == "Paired" then local mac_address = get_device_mac_address(t.object_path) if message[2] then local device = get_device_info(mac_address) if device["paired"] then devices[mac_address] = device end else devices[mac_address] = nil end end end end end return luastatus.barlib.escape(reprint_devices()) end } ================================================ FILE: examples/lemonbar/btc-price.lua ================================================ -- Bitcoin price widget. -- Updates on click and every 5 minutes. local custom_sleep_amt = nil local function check_error(t) if t.error then -- Low-level libcurl error return t.error end if t.status < 200 or t.status > 299 then -- Bad HTTP status return string.format('HTTP status %d', t.status) end -- Everything's OK return nil end local function stylize(styles, fmt, ...) local pos_args = {...} local res, _ = fmt:gsub('([@%%])([0-9])', function(sigil, digit) local i = tonumber(digit) if sigil == '@' then if i == 0 then return '%{F-}' end return styles[i] else assert(i ~= 0) return pos_args[i] end end) return res end widget = { plugin = 'web', opts = { planner = function() while true do coroutine.yield({action = 'request', params = { url = 'https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT', timeout = 5, }}) local period = custom_sleep_amt or (5 * 60) custom_sleep_amt = nil coroutine.yield({action = 'sleep', period = period}) end end, make_self_pipe = true, }, cb = function(t) local text local err_msg = check_error(t) if err_msg then print(string.format('WARNING: luastatus: btc-price widget: %s'), err_msg) text = '......' custom_sleep_amt = 5 else local obj = assert(luastatus.plugin.json_decode(t.body)) text = obj.price:match('[^.]+') end return stylize( {'%{F#586A4B}', '%{F#C0863F}'}, '@1[@2$@1%1]@0', luastatus.barlib.escape(text) ) end, event = function(t) if t.button == 1 then luastatus.plugin.wake_up() end end, } ================================================ FILE: examples/lemonbar/cpu-freq.lua ================================================ local VBLOCKS = {'▂', '▃', '▄', '▅', '▆', '▇', '█'} local function make_color(ratio) local red = math.floor(0.5 + 255 * ratio) return string.format('#%02x%02x00', red, 255 - red) end local function make_chunk(entry) local num = entry.cur - entry.min local denom = entry.max - entry.min local ratio if denom ~= 0 then ratio = num / denom else -- If max_freq == min_freq, set ratio to zero. ratio = 0 end local vblock_idx = math.min( 1 + math.floor(0.5 + ratio * #VBLOCKS), #VBLOCKS) return '%{F' .. make_color(ratio) .. '}' .. VBLOCKS[vblock_idx] end local plugin_data = {} local plugin_params = { timer_opts = { period = 2, }, cb = function(t) if t == nil then return nil end local r = {} for _, entry in ipairs(t) do r[#r + 1] = make_chunk(entry) end return table.concat(r) .. '%{F-}' end, } widget = luastatus.require_plugin('cpu-freq-linux').widget(plugin_params, plugin_data) ================================================ FILE: examples/lemonbar/cpu-temperature.lua ================================================ local COOL_TEMP = 50 local HEAT_TEMP = 75 local function getcolor(temp) local t = (temp - COOL_TEMP) / (HEAT_TEMP - COOL_TEMP) if t < 0 then t = 0 end if t > 1 then t = 1 end local red = math.floor(t * 255 + 0.5) return string.format('#%02x%02x00', red, 255 - red) end local plugin_data = {} local plugin_params = { timer_opts = { period = 2, }, cb = function(t) if not t then return nil end local r = {} for _, entry in ipairs(t) do local temp = entry.value local body = string.format('%.0f°', temp) r[#r + 1] = '%{F' .. getcolor(temp) .. '}' .. body .. '%{F-}' end return r end, } widget = luastatus.require_plugin('cpu-temp-linux').widget(plugin_params, plugin_data) ================================================ FILE: examples/lemonbar/cpu-usage.lua ================================================ widget = luastatus.require_plugin('cpu-usage-linux').widget{ cb = function(usage) if usage ~= nil then local body = string.format('[%5.1f%%]', usage * 100) return luastatus.barlib.escape(body) end end, } ================================================ FILE: examples/lemonbar/disk-io.lua ================================================ local function stylize(styles, fmt, ...) local pos_args = {...} local res, _ = fmt:gsub('([@%%])([0-9])', function(sigil, digit) local i = tonumber(digit) if sigil == '@' then if i == 0 then return '%{F-}' end return styles[i] else assert(i ~= 0) return pos_args[i] end end) return res end widget = luastatus.require_plugin('disk-io-linux').widget{ period = 2, cb = function(t) -- Sort by name for determinism table.sort(t, function(a, b) return a.name < b.name end) local segments = {} for _, entry in ipairs(t) do local R = entry.read_bytes local W = entry.written_bytes if (R >= 0) and (W >= 0) then segments[#segments + 1] = stylize( {'%{F#DCA3A3}', '%{F#72D5A3}'}, '%1: @1%2k↓ @2%3k↑@0', luastatus.barlib.escape(entry.name), string.format('%.0f', R / 1024), string.format('%.0f', W / 1024) ) end end return segments end, } ================================================ FILE: examples/lemonbar/file-contents.lua ================================================ widget = luastatus.require_plugin('file-contents-linux').widget{ filename = os.getenv('HOME') .. '/status', cb = function(f) -- show the first line of the file return luastatus.barlib.escape(f:read('*line')) end, } ================================================ FILE: examples/lemonbar/fs.lua ================================================ local function sorted_keys(tbl) local keys = {} for k, _ in pairs(tbl) do keys[#keys + 1] = k end table.sort(keys) return keys end widget = { plugin = 'fs', opts = { paths = {'/', '/home'}, }, cb = function(t) -- Sort for determinism local keys = sorted_keys(t) local res = {} for _, k in ipairs(keys) do local v = t[k] local body = string.format('%s %.0f%%', k, (1 - v.avail / v.total) * 100) table.insert(res, luastatus.barlib.escape(body)) end return res end, } ================================================ FILE: examples/lemonbar/gmail.lua ================================================ --[[ -- Expects 'credentials.lua' to be present in the current directory; it may contain, e.g., -- return { -- gmail = { -- login = 'john.smith', -- password = 'qwerty' -- } -- } --]] local credentials = require 'credentials' widget = luastatus.require_plugin('imap').widget{ verbose = false, host = 'imap.gmail.com', port = 993, mailbox = 'Inbox', use_ssl = true, timeout = 2 * 60, handshake_timeout = 10, login = credentials.gmail.login, password = credentials.gmail.password, error_sleep_period = 60, cb = function(unseen) if unseen == nil then return nil elseif unseen == 0 then return "%{F#595959}" .. '[-]' .. "%{F-}" else return string.format('[%d unseen]', unseen) end end, } ================================================ FILE: examples/lemonbar/ip.lua ================================================ widget = { plugin = 'network-linux', cb = function(t) local r = {} for iface, params in pairs(t) do local iface_clean = iface local addr = params.ipv6 or params.ipv4 if addr then -- strip out "label" from the interface name iface_clean = iface_clean:gsub(':.*', '') -- strip out "zone index" from the address addr = addr:gsub('%%.*', '') if iface_clean ~= 'lo' then local body = string.format('[%s: %s]', iface_clean, addr) r[#r + 1] = "%{F#709080}" .. body .. "%{F-}" end end end return r end, } ================================================ FILE: examples/lemonbar/loadavg-linux.lua ================================================ local function get_ncpus() local f = assert(io.open('/proc/cpuinfo', 'r')) local n = 0 for line in f:lines() do if line:match('^processor\t') then n = n + 1 end end f:close() return n end local function avg2str(x) assert(x >= 0) if x >= 1000 then return '↑↑↑' end return string.format('%3.0f', x) end widget = { plugin = 'timer', opts = { period = 2, }, cb = function(_) local f = io.open('/proc/loadavg', 'r') local avg1, avg5, avg15 = f:read('*number', '*number', '*number') f:close() assert(avg1 and avg5 and avg15) local ncpus = get_ncpus() return luastatus.barlib.escape(string.format( '[%s%% %s%% %s%%]', avg2str(avg1 / ncpus * 100), avg2str(avg5 / ncpus * 100), avg2str(avg15 / ncpus * 100) )) end, } ================================================ FILE: examples/lemonbar/media-player-mpris.lua ================================================ local PLAYER = 'clementine' local PLAYBACK_STATUS_ICONS = { Playing = '▶', Paused = '◆', Stopped = '—', } local COLOR_BRIGHT = '#60b177' local COLOR_DIM = '#9fb15d' local function fetch_metadata_field(t, key) if t.Metadata then return t.Metadata[key] else return nil end end local function do_call_method(method_name) local is_ok, err_msg = luastatus.plugin.call_method_str({ bus = 'session', dest = 'org.mpris.MediaPlayer2.' .. PLAYER, object_path = '/org/mpris/MediaPlayer2', interface = 'org.mpris.MediaPlayer2.Player', method = method_name, -- no arg_str }) if not is_ok then print(string.format( 'WARNING: luastatus: mpris widget: cannot call method "%s": %s', method_name, err_msg )) end end local function make_segment(text, color, event_text) local color_begin = '%{F' .. color .. '}' local color_end = '%{F-}' local res = color_begin .. luastatus.barlib.escape(text) .. color_end if event_text then local a_begin = '%{A:' .. event_text .. ':}' local a_end = '%{A}' res = a_begin .. res .. a_end end return res end widget = luastatus.require_plugin('mpris').widget{ player = PLAYER, cb = function(t) if not t.PlaybackStatus then return nil end local title = fetch_metadata_field(t, 'xesam:title') title = title or '' title = luastatus.libwidechar.make_valid_and_printable(title, '?') title = luastatus.libwidechar.truncate_to_width(title, 40) title = title or '' local icon = PLAYBACK_STATUS_ICONS[t.PlaybackStatus] or '?' local chunks = { make_segment('←', COLOR_DIM, 'prev'), make_segment(icon, COLOR_BRIGHT, 'status'), make_segment('→', COLOR_DIM, 'next'), } if title ~= '' then table.insert(chunks, make_segment(title, COLOR_BRIGHT, nil)) end return table.concat(chunks, ' ') end, event = function(t) if t == 'status' then do_call_method('PlayPause') elseif t == 'prev' then do_call_method('Previous') elseif t == 'next' then do_call_method('Next') end end, } ================================================ FILE: examples/lemonbar/mem-usage.lua ================================================ widget = luastatus.require_plugin('mem-usage-linux').widget{ timer_opts = {period = 2}, cb = function(t) local used_kb = t.total.value - t.avail.value local body = string.format('[%3.2f GiB]', used_kb / 1024 / 1024) return "%{F#af8ec3}" .. body .. "%{F-}" end, } ================================================ FILE: examples/lemonbar/mpd-progressbar.lua ================================================ local text, time, total, is_playing = nil, nil, nil, false local timeout = 2 local titlewidth = 40 local function W_split(str, boundary) local head, _ = luastatus.libwidechar.truncate_to_width(str, boundary) assert(head) local tail = str:sub(1 + #head) return head, tail end widget = { plugin = 'mpd', opts = { timeout = timeout, }, cb = function(t) if t.what == 'update' then -- build title local title if t.song.Title then title = t.song.Title if t.song.Artist then title = t.song.Artist .. ': ' .. title end else title = t.song.file or '' end title = luastatus.libwidechar.make_valid(title, '?') if assert(luastatus.libwidechar.width(title)) > titlewidth then title = luastatus.libwidechar.truncate_to_width(title, titlewidth - 1) .. '…' end -- build text text = string.format('%s %s', ({play = '▶', pause = '‖', stop = '■'})[t.status.state], title ) -- update other globals if t.status.time then time, total = t.status.time:match('(%d+):(%d+)') time, total = tonumber(time), tonumber(total) else time, total = nil, nil end is_playing = t.status.state == 'play' elseif t.what == 'timeout' then if is_playing then time = math.min(time + timeout, total) end else -- 'connecting' or 'error' return {full_text = t.what} end -- calc progress local width = assert(luastatus.libwidechar.width(text)) -- 'time' and 'total' can be nil here, we check for 'total' only local ulpos = (total and total ~= 0) and math.floor(width / total * time + 0.5) or 0 local head, tail = W_split(text, ulpos) return '%{u+}' .. luastatus.barlib.escape(head) .. '%{u-}' .. luastatus.barlib.escape(tail) end } ================================================ FILE: examples/lemonbar/network-rate.lua ================================================ local function stylize(styles, fmt, ...) local pos_args = {...} local res, _ = fmt:gsub('([@%%])([0-9])', function(sigil, digit) local i = tonumber(digit) if sigil == '@' then if i == 0 then return '%{F-}' end return styles[i] else assert(i ~= 0) return pos_args[i] end end) return res end local function make_segment(iface, R, S) return stylize( {'%{F#DCA3A3}', '%{F#72D5A3}'}, '[%1 @1%2k @2%3k@0]', luastatus.barlib.escape(iface), string.format('%.0f↓', R / 1000), string.format('%.0f↑', S / 1000) ) end widget = luastatus.require_plugin('network-rate-linux').widget{ iface_except = 'lo', period = 3, in_array_form = true, cb = function(t) local r = {} for _, PQ in ipairs(t) do local iface = PQ[1] local R, S = PQ[2].R, PQ[2].S r[#r + 1] = make_segment(iface, R, S) end return r end, } ================================================ FILE: examples/lemonbar/pulse-gauge.lua ================================================ local GAUGE_NCHARS = 10 local function mk_gauge(level, full, empty) local nfull = math.floor(level * GAUGE_NCHARS + 0.5) return full:rep(nfull) .. empty:rep(GAUGE_NCHARS - nfull) end widget = { plugin = 'pulse', cb = function(t) local level = t.cur / t.norm if t.mute then return "%{F#e03838}" .. mk_gauge(level, '×', '—') .. "%{F-}" else return "%{F#718ba6}" .. mk_gauge(level, '●', '○') .. "%{F-}" end end, } ================================================ FILE: examples/lemonbar/pulse.lua ================================================ widget = { plugin = 'pulse', cb = function(t) if t.mute then return "%{F#e03838}" .. '[mute]' .. "%{F-}" end local percent = (t.cur / t.norm) * 100 local body = string.format('[%3d%%]', math.floor(0.5 + percent)) return "%{F#718ba6}" .. luastatus.barlib.escape(body) .. "%{F-}" end, } ================================================ FILE: examples/lemonbar/systemd-unit.lua ================================================ local COLORS = { pink = '#ec93d3', green = '#60b48a', yellow = '#dfaf8f', red = '#dca3a3', dim = '#909090', } local function make_output(text, color_name) local color_begin = '%{F' .. assert(COLORS[color_name]) .. '}' local color_end = '%{F-}' return string.format('[Tor: %s%s%s]', color_begin, text, color_end) end widget = luastatus.require_plugin('systemd-unit').widget{ unit_name = 'tor.service', cb = function(state) if state == 'active' then return make_output('✓', 'green') elseif state == 'reloading' or state == 'activating' then return make_output('•', 'yellow') elseif state == 'inactive' or state == 'deactivating' then return make_output('-', 'dim') elseif state == 'failed' then return make_output('x', 'red') else return make_output('?', 'pink') end end, } ================================================ FILE: examples/lemonbar/time-battery-combined.lua ================================================ local function get_bat_seg(t) if not t then return '[--×--]' end if t.status == 'Unknown' or t.status == 'Full' or t.status == 'Not charging' then return nil end local sym, color = '?', '#dcdcdc' if t.status == 'Discharging' then sym = '↓' color = '#dca3a3' elseif t.status == 'Charging' then sym = '↑' color = '#60b48a' end local body = string.format('[%3d%%%s]', t.capacity, sym) return '%{F' .. color .. '}' .. luastatus.barlib.escape(body) .. '%{F-}' end widget = luastatus.require_plugin('battery-linux').widget{ period = 2, cb = function(t) return { "%{F#dc8cc3}" .. os.date('[%H:%M]') .. "%{F-}", get_bat_seg(t), } end, } ================================================ FILE: examples/lemonbar/time-date.lua ================================================ local months = { 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря', } widget = { plugin = 'timer', cb = function() local d = os.date('*t') return { string.format('%d %s', d.day, months[d.month]), string.format('%02d:%02d', d.hour, d.min), } end, } ================================================ FILE: examples/lemonbar/tor.lua ================================================ -- Trivial but somewhat useful widget showing if the Tor daemon is running. widget = { plugin = 'timer', opts = {period = 5}, cb = function() local f = io.open('/var/run/tor/tor.pid', 'r') if f then f:close() return '[TOR]' else return nil end end, } ================================================ FILE: examples/lemonbar/uptime-linux.lua ================================================ local SUFFIXES_AND_DIVISORS = { {'m', 60}, {'h', 60}, {'d', 24}, {'W', 7}, {'M', 30}, {'Y', 365 / 30}, } local function seconds_to_human_readable_time(sec) local prev_suffix = 's' local found_suffix for _, PQ in ipairs(SUFFIXES_AND_DIVISORS) do local suffix = PQ[1] local divisor = PQ[2] if sec < divisor then found_suffix = prev_suffix break end sec = sec / divisor prev_suffix = suffix end if not found_suffix then found_suffix = prev_suffix end return string.format('%.0f%s', sec, found_suffix) end widget = { plugin = 'timer', opts = { period = 2, }, cb = function(_) local f = io.open('/proc/uptime', 'r') local sec, _ = f:read('*number', '*number') f:close() assert(sec) return string.format( '[uptime: %s]', seconds_to_human_readable_time(sec) ) end, } ================================================ FILE: examples/lemonbar/weather-rus.lua ================================================ local LOCATION_NAME = 'Moscow' local TIMEOUT = 10 local INTERVAL = 9 local SLEEP_AFTER_INITIAL = 5 local coordinates = nil local function planner() -- Get coordinates of LOCATION_NAME. while true do -- Form the URL local url = string.format( 'https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1', luastatus.plugin.urlencode(LOCATION_NAME) ) -- Make request coroutine.yield({action = 'request', params = { url = url, timeout = TIMEOUT, }}) -- If successful, break if coordinates ~= nil then break end -- Failure; sleep and retry coroutine.yield({action = 'sleep', period = INTERVAL}) end -- Coordinates fetched, sleep for SLEEP_AFTER_INITIAL. coroutine.yield({action = 'sleep', period = SLEEP_AFTER_INITIAL}) -- Form the weather URL local weather_url = string.format( 'https://api.open-meteo.com/v1/forecast' .. '?latitude=%f' .. '&longitude=%f' .. '¤t_weather=true', coordinates.latitude, coordinates.longitude ) -- Fetch weather, periodically while true do -- Make request coroutine.yield({action = 'request', params = { url = weather_url, timeout = TIMEOUT, }}) -- Sleep and repeat coroutine.yield({action = 'sleep', period = INTERVAL}) end end local function check_error(t) if t.error then -- Low-level libcurl error return t.error end if t.status < 200 or t.status > 299 then -- Bad HTTP status return string.format('HTTP status %d', t.status) end -- Everything's OK return nil end local WEATHER_CODES = { [0] = 'Ясно', [1] = 'Преимущественно ясно', [2] = 'Переменная облачность', [3] = 'Пасмурно', [45] = 'Туман', [48] = 'Осаждение инея и тумана', [51] = 'Изморось, лёгкая', [53] = 'Изморось, умеренная', [55] = 'Изморось, сильная', [56] = 'Ледяная изморось, лёгкая', [57] = 'Ледяная изморось, сильная', [61] = 'Дождь, лёгкий', [63] = 'Дождь, умеренный', [65] = 'Дождь, сильный', [66] = 'Град, лёгкий', [67] = 'Град, сильный', [71] = 'Снегопад, лёгкий', [73] = 'Снегопад, умеренный', [75] = 'Снегопад, сильный', [77] = 'Крупинки снега', [80] = 'Ливень, лёгкий', [81] = 'Ливень, умеренный', [82] = 'Ливень, сильный', [85] = 'Снеговые осадки, лёгкие', [87] = 'Снеговые осадки, сильные', [95] = 'Гроза', [96] = 'Гроза, небольшой град', [99] = 'Гроза, сильный град', } local function fmt_weather(temp, _, _, weather_code) local segment1 = string.format('%.0f°', temp) -- segment2 is either string or nil local segment2 = WEATHER_CODES[weather_code] return {segment1, segment2} end widget = { plugin = 'web', opts = { planner = planner, }, cb = function(t) local err_msg = check_error(t) if err_msg then print(string.format('WARNING: luastatus: weather widget: %s'), err_msg) return '%{F#e03838}...%{F-}' end if coordinates == nil then local obj = assert(luastatus.plugin.json_decode(t.body)) local result = assert(obj.results[1]) local latitude = assert(result.latitude) local longitude = assert(result.longitude) coordinates = {latitude = latitude, longitude = longitude} return '%{F#709080}...%{F-}' end local obj = assert(luastatus.plugin.json_decode(t.body)) local cw = assert(obj.current_weather) local temp = assert(cw.temperature) local wind_speed = assert(cw.windspeed) -- is_day is an integer, either 0 or 1 local is_day = assert(cw.is_day) ~= 0 local weather_code = assert(cw.weathercode) return fmt_weather(temp, wind_speed, is_day, weather_code) end, } ================================================ FILE: examples/lemonbar/weather.lua ================================================ local LOCATION_NAME = 'Moscow' local TIMEOUT = 10 local INTERVAL = 9 local SLEEP_AFTER_INITIAL = 5 local coordinates = nil local function planner() -- Get coordinates of LOCATION_NAME. while true do -- Form the URL local url = string.format( 'https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1', luastatus.plugin.urlencode(LOCATION_NAME) ) -- Make request coroutine.yield({action = 'request', params = { url = url, timeout = TIMEOUT, }}) -- If successful, break if coordinates ~= nil then break end -- Failure; sleep and retry coroutine.yield({action = 'sleep', period = INTERVAL}) end -- Coordinates fetched, sleep for SLEEP_AFTER_INITIAL. coroutine.yield({action = 'sleep', period = SLEEP_AFTER_INITIAL}) -- Form the weather URL local weather_url = string.format( 'https://api.open-meteo.com/v1/forecast' .. '?latitude=%f' .. '&longitude=%f' .. '¤t_weather=true', coordinates.latitude, coordinates.longitude ) -- Fetch weather, periodically while true do -- Make request coroutine.yield({action = 'request', params = { url = weather_url, timeout = TIMEOUT, }}) -- Sleep and repeat coroutine.yield({action = 'sleep', period = INTERVAL}) end end local function check_error(t) if t.error then -- Low-level libcurl error return t.error end if t.status < 200 or t.status > 299 then -- Bad HTTP status return string.format('HTTP status %d', t.status) end -- Everything's OK return nil end local WEATHER_CODES = { [0] = 'Clear sky', [1] = 'Mainly clear', [2] = 'Partly cloudy', [3] = 'Overcast', [45] = 'Fog', [48] = 'Depositing rime fog', [51] = 'Drizzle, light', [53] = 'Drizzle, moderate', [55] = 'Drizzle, dense', [56] = 'Freezing drizzle, light', [57] = 'Freezing drizzle, dense', [61] = 'Rain, slight', [63] = 'Rain, moderate', [65] = 'Rain, heavy', [66] = 'Freezing rain, light', [67] = 'Freezing rain, heavy', [71] = 'Snow fall, slight', [73] = 'Snow fall, moderate', [75] = 'Snow fall, heavy', [77] = 'Snow grains', [80] = 'Rain shower, slight', [81] = 'Rain shower, moderate', [82] = 'Rain shower, violent', [85] = 'Snow showers, slight', [87] = 'Snow showers, heavy', [95] = 'Thunderstorm', [96] = 'Thunderstorm, slight hail', [99] = 'Thunderstorm, heavy hail', } local function fmt_weather(temp, _, _, weather_code) local segment1 = string.format('%.0f°', temp) -- segment2 is either string or nil local segment2 = WEATHER_CODES[weather_code] return {segment1, segment2} end widget = { plugin = 'web', opts = { planner = planner, }, cb = function(t) local err_msg = check_error(t) if err_msg then print(string.format('WARNING: luastatus: weather widget: %s'), err_msg) return '%{F#e03838}...%{F-}' end if coordinates == nil then local obj = assert(luastatus.plugin.json_decode(t.body)) local result = assert(obj.results[1]) local latitude = assert(result.latitude) local longitude = assert(result.longitude) coordinates = {latitude = latitude, longitude = longitude} return '%{F#709080}...%{F-}' end local obj = assert(luastatus.plugin.json_decode(t.body)) local cw = assert(obj.current_weather) local temp = assert(cw.temperature) local wind_speed = assert(cw.windspeed) -- is_day is an integer, either 0 or 1 local is_day = assert(cw.is_day) ~= 0 local weather_code = assert(cw.weathercode) return fmt_weather(temp, wind_speed, is_day, weather_code) end, } ================================================ FILE: examples/lemonbar/wireless.lua ================================================ local ORIGIN = '•' -- '●' '○' '◌' '◍' '⬤' local RAY = '}' -- '›' '〉' '❭' '❯' '❱' '⟩' '⟫' '》' local MIN_DBM, MAX_DBM = -90, -20 local NGAUGE = 5 local COLOR_DIM = '#709080' local function round(x) return math.floor(x + 0.5) end local function colorize_as_dim(s) s = luastatus.barlib.escape(s) return '%{F' .. COLOR_DIM .. '}' .. s .. '%{F-}' end local function make_wifi_gauge(dbm) if dbm < MIN_DBM then dbm = MIN_DBM end if dbm > MAX_DBM then dbm = MAX_DBM end local nbright = round(NGAUGE * (1 - 0.7 * (MAX_DBM - dbm) / (MAX_DBM - MIN_DBM))) return ORIGIN .. RAY:rep(nbright) .. colorize_as_dim(RAY:rep(NGAUGE - nbright)) end local function sorted_keys(tbl) local keys = {} for k, _ in pairs(tbl) do keys[#keys + 1] = k end table.sort(keys) return keys end widget = { plugin = 'network-linux', opts = { wireless = true, timeout = 10, }, cb = function(t) if not t then return nil end -- Sort for determinism local ifaces = sorted_keys(t) local r = {} for _, iface in ipairs(ifaces) do local params = t[iface] if params.wireless then if params.wireless.ssid then r[#r + 1] = colorize_as_dim(params.wireless.ssid) end if params.wireless.signal_dbm then r[#r + 1] = make_wifi_gauge(params.wireless.signal_dbm) end elseif iface ~= 'lo' and (params.ipv4 or params.ipv6) then r[#r + 1] = colorize_as_dim(string.format('[%s]', iface)) end end return r end, } ================================================ FILE: examples/lemonbar/xkb.lua ================================================ widget = { plugin = 'xkb', cb = function(t) if t.name then local base_layout = t.name:match('[^(]+') if base_layout == 'gb' or base_layout == 'us' then return "%{F#9c9c9c}" .. '[En]' .. "%{F-}" elseif base_layout == 'ru' then return "%{F#eab93d}" .. '[Ru]' .. "%{F-}" else local body = string.format('[%s]', base_layout) return luastatus.barlib.escape(body) end else return '[? ID ' .. t.id .. ']' end end, } ================================================ FILE: fuzz_utils/do_fuzz_everything.sh ================================================ #!/usr/bin/env bash set -e shopt -s nullglob if [[ -z "$XXX_AFL_DIR" ]]; then echo >&2 "You must set the 'XXX_AFL_DIR' environment variable." exit 1 fi case "$XXX_AFL_IS_PP" in 0) ;; 1) ;; *) echo >&2 "You must set 'XXX_AFL_IS_PP' environment variable to either 0 or 1." echo >&2 "Set it to 0 if AFL is the original Google's version (not AFL++)." echo >&2 "Set it to 1 if AFL is actually AFL++." exit 1 ;; esac cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" cd .. fuzz_dirs=( plugins/*/fuzz* barlibs/*/fuzz* ) if (( XXX_AFL_IS_PP )); then afl_cc="$XXX_AFL_DIR"/afl-cc else afl_cc="$XXX_AFL_DIR"/afl-gcc fi for d in "${fuzz_dirs[@]}"; do [[ -d "$d" ]] || continue "$d"/clear.sh CC="$afl_cc" "$d"/build.sh if [[ -x "$d"/fuzz_all.sh ]]; then "$d"/fuzz_all.sh else "$d"/fuzz.sh fi done ================================================ FILE: fuzz_utils/find_interesting.sh ================================================ #!/usr/bin/env bash set -e shopt -s nullglob cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" cd .. is_dir_empty() { local -a files files=( "$1"/* "$1"/.* ) (( "${#files[@]}" == 2 )) } say() { printf '%s\n' "$*" >&2 } fuzz_dirs=( plugins/*/fuzz* barlibs/*/fuzz* ) results=() for d1 in "${fuzz_dirs[@]}"; do [[ -d "$d1" ]] || continue for d2 in "$d1"/findgins*; do [[ -d "$d2" ]] || continue for d3 in "$d2"/crashes "$d2"/hangs; do [[ -d "$d3" ]] || continue if ! is_dir_empty "$d3"; then results+=("$d3") fi done done done if (( ${#results[@]} != 0 )); then say 'The following "interesting" directories are non-empty:' for res in "${results[@]}"; do say " * $res" done exit 1 fi say 'Everything is OK.' exit 0 ================================================ FILE: fuzz_utils/fuzz_utils.h ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef luastatus_fuzz_utils_h_ #define luastatus_fuzz_utils_h_ #include #include #include #include #include // At least for fuzzing, please use a reasonably modern GNU C-compatible compiler. #define FUZZ_UTILS_INHEADER \ static inline __attribute__((unused)) #define FUZZ_UTILS_ATTR_WARN_UNUSED \ __attribute__((warn_unused_result)) #define FUZZ_UTILS_OOM() \ do { \ fputs("fuzz_utils: out of memory.\n", stderr); \ abort(); \ } while (0) typedef struct { char *data; size_t size; size_t capacity; } FuzzInput; FUZZ_UTILS_INHEADER FuzzInput fuzz_input_new_prealloc(size_t prealloc) { if (!prealloc) { return (FuzzInput) {0}; } char *data = malloc(prealloc); if (!data) { FUZZ_UTILS_OOM(); } return (FuzzInput) { .data = data, .size = 0, .capacity = prealloc, }; } FUZZ_UTILS_INHEADER void fuzz_input__ensure1(FuzzInput *x) { if (x->size != x->capacity) { return; } if (x->capacity) { if (x->capacity > SIZE_MAX / 2) { goto oom; } x->capacity *= 2; } else { x->capacity = 1; } if (!(x->data = realloc(x->data, x->capacity))) { goto oom; } return; oom: FUZZ_UTILS_OOM(); } FUZZ_UTILS_INHEADER FUZZ_UTILS_ATTR_WARN_UNUSED int fuzz_input_read(int fd, FuzzInput *dst) { for (;;) { fuzz_input__ensure1(dst); ssize_t r = read(fd, dst->data + dst->size, dst->capacity - dst->size); if (r < 0) { return -1; } else if (r == 0) { return 0; } dst->size += r; } } FUZZ_UTILS_INHEADER void fuzz_input_free(FuzzInput x) { free(x.data); } FUZZ_UTILS_INHEADER void fuzz_utils_used(const char *ptr, size_t len) { // This is a compiler barrier: there's a contract between the compiler and // the user that the compiler doesn't try to guess what inline assembly does, // even if its body is empty. // // So after this, the compiler conservately thinks the span is used, because // the inline assembly might have dereferenced it and done something that has // side effects (e.g. made a system call "write(1, ptr, len)"). __asm__ __volatile__ ( "" : : "r"(ptr), "r"(len) : "memory" ); } #endif ================================================ FILE: fuzz_utils/gen_testcases/.gitignore ================================================ __pycache__ ================================================ FILE: fuzz_utils/gen_testcases/__init__.py ================================================ # Copyright (C) 2025 luastatus developers # # This file is part of luastatus. # # luastatus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # luastatus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with luastatus. If not, see . ================================================ FILE: fuzz_utils/gen_testcases/gen_testcases.py ================================================ #!/usr/bin/env python3 # Copyright (C) 2025 luastatus developers # # This file is part of luastatus. # # luastatus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # luastatus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with luastatus. If not, see . import argparse import sys import random import typing import mod_generator import mod_mutator import mod_mutators LENGTH_DEFAULT = 16 NUM_FILES_DEFAULT = 20 class ChunkSpec: def __init__(self, weight: int, chunk: bytes, is_homogeneous: bool) -> None: assert weight > 0 assert chunk self.weight = weight self.chunk = chunk self.is_homogeneous = is_homogeneous def make_chunk_spec_checked(weight: int, chunk: bytes, is_homogeneous: bool) -> ChunkSpec: if weight <= 0: raise ValueError('weight must be positive') if not chunk: raise ValueError('chunk is empty') return ChunkSpec(weight=weight, chunk=chunk, is_homogeneous=is_homogeneous) def into_h_w_and_rest(s: str) -> typing.Tuple[bool, int, str]: is_homogeneous = False if s.startswith('h:'): is_homogeneous = True s = s[2:] weight_s, rest = s.split(':', maxsplit=1) weight = int(weight_s) return (is_homogeneous, weight, rest) def parse_arg_ab(s: str) -> ChunkSpec: is_homogeneous, weight, content = into_h_w_and_rest(s) return make_chunk_spec_checked( weight=weight, chunk=content.encode(), is_homogeneous=is_homogeneous) def parse_arg_ab_range(s: str) -> ChunkSpec: is_homogeneous, weight, content = into_h_w_and_rest(s) first_s, last_s = content.split('-') first, last = int(first_s), int(last_s) chunk = bytes(list(range(first, last + 1))) return make_chunk_spec_checked( weight=weight, chunk=chunk, is_homogeneous=is_homogeneous) def parse_arg_length(s: str) -> typing.Tuple[int, int]: if '-' in s: n_min_s, n_max_s = s.split('-') n_min, n_max = int(n_min_s), int(n_max_s) else: n = int(s) n_min, n_max = n, n if (n_min > n_max) or (n_min < 0) or (n_max < 0): raise ValueError('invalid length specifier') return (n_min, n_max) def parse_arg_num_files(s: str) -> int: n = int(s) if n < 3: raise ValueError('--num-files value must be at least 3') return n def parse_arg_prefix(s: str) -> bytes: return s.encode() def parse_arg_substrings(s: str) -> typing.List[bytes]: separator = s[0] parts = s[1:].split(separator) return [part.encode() for part in parts] def parse_arg_extra_testcase(s: str) -> typing.Tuple[str, bytes]: file_suffix, content_s = s.split(':', maxsplit=1) return (file_suffix, content_s.encode()) def main() -> None: ap = argparse.ArgumentParser() ap.add_argument('dest_dir') def _add_list_arg(spelling: str, **kwargs: typing.Any) -> None: ap.add_argument(spelling, action='append', default=[], **kwargs) _add_list_arg('--a', type=parse_arg_ab, metavar='') _add_list_arg('--a-range', type=parse_arg_ab_range, metavar='') _add_list_arg('--b', type=parse_arg_ab, metavar='') _add_list_arg('--b-range', type=parse_arg_ab_range, metavar='') ap.add_argument( '--a-is-important', action='store_true') ap.add_argument( '--length', default=(LENGTH_DEFAULT, LENGTH_DEFAULT), type=parse_arg_length, metavar='|-') ap.add_argument( '--num-files', default=NUM_FILES_DEFAULT, type=parse_arg_num_files, metavar='') ap.add_argument( '--mut-prefix', type=parse_arg_prefix, required=False) _add_list_arg('--extra-testcase', type=parse_arg_extra_testcase) ap.add_argument( '--mut-substrings', type=parse_arg_substrings, required=False) ap.add_argument( '--file-prefix', default='') ap.add_argument( '--random-seed', type=int, required=False, metavar='') args = ap.parse_args() if args.random_seed is not None: random.seed(args.random_seed, version=2) g = mod_generator.Generator() def _add_choices(chunk_specs: typing.List[ChunkSpec], key: str) -> None: for chunk_spec in chunk_specs: sheaf = mod_generator.Sheaf( chunk_spec.chunk, is_homogeneous=chunk_spec.is_homogeneous) g.add_sheaf_for_key( key=key, sheaf=sheaf, weight=chunk_spec.weight) _add_choices(args.a, key='A') _add_choices(args.a_range, key='A') _add_choices(args.b, key='B') _add_choices(args.b_range, key='B') if not (g.has_choices_for_key('A') and g.has_choices_for_key('B')): if not g.has_choices_for_key('A'): print('Ether "--a" or "--a-range" must be specified.', file=sys.stderr) else: print('Ether "--b" or "--b-range" must be specified.', file=sys.stderr) ap.print_help(sys.stderr) sys.exit(2) mutators: typing.List[mod_mutator.Mutator] = [] if args.mut_substrings is not None: mutators.append(mod_mutators.SubstringMutator(args.mut_substrings)) if args.mut_prefix is not None: mutators.append(mod_mutators.PrefixMutator(args.mut_prefix)) max_variants = max((m.get_number_of_variants() for m in mutators), default=-1) if max_variants > args.num_files: print( 'Specified mutators have too many variants (%d) for --num-files=%d' % ( max_variants, args.num_files ), file=sys.stderr) sys.exit(2) def _gen_filename(suffix: str) -> str: return f'{args.dest_dir}/{args.file_prefix}testcase_{suffix}' length_min, length_max = args.length if args.a_is_important: whole_length = g.get_whole_length_for_key('A') if length_min < whole_length: print( 'Minimal length (%d) is less than number of whole bytes in A (%d)' % ( length_min, whole_length ), file=sys.stderr) sys.exit(2) for i in range(args.num_files): with open(_gen_filename(f'{i:03d}'), 'wb') as f: length = random.randint(length_min, length_max) include_all_of_A = (i == 0) and args.a_is_important content = mod_mutator.generate_with_mutators( g=g, mutators=mutators, length=length, i=i, n=args.num_files, include_all_of_A=include_all_of_A) f.write(content) for file_suffix, content in args.extra_testcase: with open(_gen_filename(file_suffix), 'wb') as f: f.write(content) if __name__ == '__main__': main() ================================================ FILE: fuzz_utils/gen_testcases/mod_generator.py ================================================ # Copyright (C) 2025 luastatus developers # # This file is part of luastatus. # # luastatus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # luastatus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with luastatus. If not, see . import random import typing def _clamp(x: int, min_value: int, max_value: int) -> int: assert min_value <= max_value if x < min_value: return min_value if x > max_value: return max_value return x class Sheaf: def __init__(self, b: bytes, is_homogeneous: bool) -> None: assert b self.b = b self.is_homogeneous = is_homogeneous def get_length(self) -> int: return len(self.b) def random_byte(self) -> bytes: i = random.randint(0, len(self.b) - 1) return self.b[i:i + 1] def all_bytes(self) -> typing.Iterator[bytes]: for i in range(len(self.b)): yield self.b[i:i + 1] class WeightedSheafList: def __init__(self) -> None: self.sheaves: typing.List[Sheaf] = [] self.weights: typing.List[int] = [] def add_sheaf(self, sheaf: Sheaf, weight: int) -> None: assert weight > 0 self.sheaves.append(sheaf) self.weights.append(weight) def is_nonempty(self) -> bool: return bool(self.sheaves) def random_sheaf(self) -> Sheaf: assert self.is_nonempty() res, = random.choices(self.sheaves, weights=self.weights) return res def get_whole_length(self) -> int: res = 0 for sheaf in self.sheaves: if sheaf.is_homogeneous: res += 1 else: res += sheaf.get_length() return res def make_whole_iterator(self) -> typing.Iterator[bytes]: for sheaf in self.sheaves: if sheaf.is_homogeneous: yield sheaf.random_byte() else: for byte in sheaf.all_bytes(): yield byte while True: yield self.random_sheaf().random_byte() class Generator: def __init__(self) -> None: self.wsls = {'A': WeightedSheafList(), 'B': WeightedSheafList()} def add_sheaf_for_key(self, key: str, sheaf: Sheaf, weight: int) -> None: self.wsls[key].add_sheaf(sheaf, weight=weight) def has_choices_for_key(self, key: str) -> bool: return self.wsls[key].is_nonempty() def get_whole_length_for_key(self, key: str) -> int: return self.wsls[key].get_whole_length() def random_bytes(self, length: int, i: int, n: int, include_all_of_A: bool) -> bytes: assert length >= 0 assert n > 0 assert 0 <= i < n ratio = i / (n - 1) num_B = _clamp(round(ratio * length), min_value=0, max_value=length) num_A = length - num_B formula = list('A' * num_A + 'B' * num_B) random.shuffle(formula) A_iter = None if include_all_of_A: assert self.wsls['A'].get_whole_length() <= length A_iter = self.wsls['A'].make_whole_iterator() chunks = [] for letter in formula: if letter == 'A' and A_iter is not None: chunks.append(next(A_iter)) else: wsl = self.wsls[letter] sheaf = wsl.random_sheaf() chunks.append(sheaf.random_byte()) return b''.join(chunks) ================================================ FILE: fuzz_utils/gen_testcases/mod_mutator.py ================================================ # Copyright (C) 2025 luastatus developers # # This file is part of luastatus. # # luastatus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # luastatus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with luastatus. If not, see . import typing import mod_generator class Mutator: def mutate(self, b: bytes, variant: int) -> bytes: raise NotImplementedError() def get_number_of_variants(self) -> int: raise NotImplementedError() def generate_with_mutators( g: mod_generator.Generator, mutators: typing.List[Mutator], length: int, i: int, n: int, include_all_of_A: bool ) -> bytes: assert n > 0 assert 0 <= i < n res = g.random_bytes(length=length, i=i, n=n, include_all_of_A=include_all_of_A) for m in mutators: variant = i % m.get_number_of_variants() res = m.mutate(res, variant=variant) return res ================================================ FILE: fuzz_utils/gen_testcases/mod_mutators.py ================================================ # Copyright (C) 2025 luastatus developers # # This file is part of luastatus. # # luastatus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # luastatus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with luastatus. If not, see . import random import typing import mod_mutator class PrefixMutator(mod_mutator.Mutator): def __init__(self, prefix: bytes) -> None: self.prefix = prefix def mutate(self, b: bytes, variant: int) -> bytes: return self.prefix[:variant] + b def get_number_of_variants(self) -> int: return len(self.prefix) + 1 class SubstringMutator(mod_mutator.Mutator): def __init__(self, substr_variants: typing.List[bytes]) -> None: self.substr_variants = substr_variants def mutate(self, b: bytes, variant: int) -> bytes: pos = random.randint(0, len(b)) substr = self.substr_variants[variant] return b[:pos] + substr + b[pos:] def get_number_of_variants(self) -> int: return len(self.substr_variants) ================================================ FILE: generate-man.sh ================================================ #!/bin/sh if [ "$#" -ne 2 ]; then printf '%s\n' >&2 "USAGE: $0 " exit 2 fi has() { command -v "$1" >/dev/null } if has rst2man; then RST2MAN=rst2man elif has rst2man.py; then RST2MAN=rst2man.py else echo >&2 "ERROR: neither 'rst2man' nor 'rst2man.py' were found." exit 1 fi sed 's/^\.\. :X-man-page-only: \?//' -- "$1" | $RST2MAN > "$2" ================================================ FILE: include/barlib_data.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef luastatus_include_barlib_data_h_ #define luastatus_include_barlib_data_h_ #include #include #include "common.h" #ifndef LUASTATUS_BARLIB_SAYF_ATTRIBUTE # ifdef __GNUC__ # define LUASTATUS_BARLIB_SAYF_ATTRIBUTE __attribute__((format(printf, 3, 4))) # else # define LUASTATUS_BARLIB_SAYF_ATTRIBUTE /*nothing*/ # endif #endif typedef LUASTATUS_BARLIB_SAYF_ATTRIBUTE void LuastatusBarlibSayf_v1( void *userdata, int level, const char *fmt, ...); typedef struct { // This barlib's private data. void *priv; // A black-box user data. void *userdata; // Should be used for logging. LuastatusBarlibSayf_v1 *sayf; // See DOCS/design/map_get.md. void ** (*map_get)(void *userdata, const char *key); } LuastatusBarlibData_v1; typedef struct { lua_State *(*call_begin) (void *userdata, size_t widget_idx); void (*call_end) (void *userdata, size_t widget_idx); void (*call_cancel)(void *userdata, size_t widget_idx); } LuastatusBarlibEWFuncs_v1; typedef struct { // This function should initialize a barlib by assigning something to /bd->priv/. // You would typically do: // // typedef struct { // ... // } Priv; // ... // static // int // init(LuastatusBarlibData *bd, const char *const *opts, size_t nwidgets) // { // Priv *p = bd->priv = LS_XNEW(Priv, 1); // ... // // /opts/ are options passed to luastatus with /-B/ switches, with a sentinel /NULL/ entry. // // /nwidgets/ is the number of widgets. It stays unchanged during the entire life cycle of a // barlib. // // It should return: // // /LUASTATUS_ERR/ on failure; // // /LUASTATUS_OK/ on success. // int (*init)(LuastatusBarlibData_v1 *bd, const char *const *opts, size_t nwidgets); // This function should register Lua functions provided by the barlib into the table on the top // of /L/'s stack. // // These functions should be thread-safe! // // It is guaranteed that /L/ stack has at least 15 free stack slots. // // May be /NULL/. // void (*register_funcs)(LuastatusBarlibData_v1 *bd, lua_State *L); // This function should update the widget with index /widget_idx/. The data is located on the // top of /L/'s stack; its format defined by the barlib itself. // // This function may push elements onto /L/'s stack (to iterate over tables), but should not // modify elements below the initial top, or interact with /L/ in any other way. // // It is guaranteed that /L/'s stack has at least 15 free slots. // // It must return: // // /LUASTATUS_OK/ on success. // In this case, /L/'s stack must not contain any extra elements pushed onto it; // // /LUASTATUS_NONFATAL_ERR/ on a non-fatal error, e.g., if the format of the data on top of // /L/'s stack is invalid. // In this case, /L/'s stack may contain extra elements pushed onto it; // // /LUASTATUS_ERR/ on a fatal error, e.g. if the connection to the display has been lost. // In this case, /L/'s stack may contain extra elements pushed onto it. // int (*set)(LuastatusBarlibData_v1 *bd, lua_State *L, size_t widget_idx); // This function should update the widget with index /widget_idx/ and set its content to // something that indicates an error. // // It must return: // // /LUASTATUS_OK/ on success; // // /LUASTATUS_ERR/ on a fatal error, e.g. if the connection to the display has been lost. // int (*set_error)(LuastatusBarlibData_v1 *bd, size_t widget_idx); // This function should run barlib's event watcher. Once the barlib wants to report an event // occurred on widget with index /i/, it should call // /lua_State *L = funcs.call_begin(i, bd->userdata)/, // thus obtaining a /lua_State */ object /L/. Then, it must either: // 1. make the following call, with exactly one additional value pushed onto /L/'s stack: // /funcs.call_end(i, bd->userdata)/, // or // 2. make the following call, with any number of additional values push onto /L/'s stack: // /funcs.call_cancel(i, bd->userdata)/. // // Both /funcs.call_end/ and /funcs.call_cancel/ invalidate /L/ as a pointer. // // It is guaranteed that each /L/ object returned from /funcs.call_begin/ has at least 15 free // stack slots. // // It must return: // /LUASTATUS_NONFATAL_ERR/ if, in some reason other than a fatal error, no events will be // reported anymore; // // /LUASTATUS_ERR/ on a fatal error, e.g. if the connection to the display has been lost. // // May be /NULL/ (and should be /NULL/ if events are not supported). // int (*event_watcher)(LuastatusBarlibData_v1 *bd, LuastatusBarlibEWFuncs_v1 funcs); // This function must destroy a previously successfully initialized barlib. void (*destroy)(LuastatusBarlibData_v1 *bd); } LuastatusBarlibIface_v1; #endif ================================================ FILE: include/barlib_data_v1.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef luastatus_include_barlib_data_v1_h_ #define luastatus_include_barlib_data_v1_h_ #include "barlib_data.h" #define LuastatusBarlibIface LuastatusBarlibIface_v1 #define LuastatusBarlibSayf LuastatusBarlibSayf_v1 #define LuastatusBarlibData LuastatusBarlibData_v1 #define LuastatusBarlibEWFuncs LuastatusBarlibEWFuncs_v1 #endif ================================================ FILE: include/barlib_v1.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef luastatus_include_barlib_v1_h_ #define luastatus_include_barlib_v1_h_ #include #include "barlib_data_v1.h" #include "common.h" const int LUASTATUS_BARLIB_LUA_VERSION_NUM = LUA_VERSION_NUM; extern LuastatusBarlibIface_v1 luastatus_barlib_iface_v1; #endif ================================================ FILE: include/common.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef luastatus_include_common_h_ #define luastatus_include_common_h_ enum { LUASTATUS_OK, LUASTATUS_ERR, LUASTATUS_NONFATAL_ERR, }; enum { LUASTATUS_LOG_FATAL, LUASTATUS_LOG_ERR, LUASTATUS_LOG_WARN, LUASTATUS_LOG_INFO, LUASTATUS_LOG_VERBOSE, LUASTATUS_LOG_DEBUG, LUASTATUS_LOG_TRACE, LUASTATUS_LOG_LAST, }; #endif ================================================ FILE: include/plugin_data.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef luastatus_include_plugin_data_h_ #define luastatus_include_plugin_data_h_ #include #include #include "common.h" #ifndef LUASTATUS_PLUGIN_SAYF_ATTRIBUTE # ifdef __GNUC__ # define LUASTATUS_PLUGIN_SAYF_ATTRIBUTE __attribute__((format(printf, 3, 4))) # else # define LUASTATUS_PLUGIN_SAYF_ATTRIBUTE /*nothing*/ # endif #endif typedef LUASTATUS_PLUGIN_SAYF_ATTRIBUTE void LuastatusPluginSayf_v1( void *userdata, int level, const char *fmt, ...); typedef struct { // This widget's private data. void *priv; // A black-box user data. void *userdata; // Should be used for logging. LuastatusPluginSayf_v1 *sayf; // See DOCS/design/map_get.md. void ** (*map_get)(void *userdata, const char *key); } LuastatusPluginData_v1; typedef struct { lua_State *(*call_begin) (void *userdata); void (*call_end) (void *userdata); void (*call_cancel)(void *userdata); } LuastatusPluginRunFuncs_v1; typedef struct { // This function should initialize a widget by assigning something to /pd->priv/. // You would typically do: // // typedef struct { // ... // } Priv; // ... // static // int // init(LuastatusPluginData *pd, lua_State *L) { // Priv *p = pd->priv = LS_XNEW(Priv, 1); // ... // // The options table is on the top of /L/'s stack. // // This function may push elements onto /L/'s stack to iterate over tables, but should not // modify elements below the initial top, or interact with /L/ in any other way. // // It is guaranteed that /L/'s stack has at least 15 free slots. // // It should return: // // /LUASTATUS_OK/ on success. // In this case, /L/'s stack should not contain any extra elements pushed onto it; // // /LUASTATUS_ERR/ on failure. // In this case, /L/'s stack may contain extra elements pushed onto it. // int (*init)(LuastatusPluginData_v1 *pd, lua_State *L); // This function should register Lua functions provided by the plugin into table on top of /L/'s // stack. // // These functions, in current implementation, are not required to be thread-safe with respect // to the widget instance, that is, /register_funcs/ in only called once per plugin instance, // and while the widget is calling a function registered by this plugin instance, no other Lua // state may do so. // // This may change in the future. // // Note that the above does not mean they are allowed to be genereally thread-unsafe. // // It is guaranteed that /L/'s stack has at least 15 free slots. // // May be /NULL/. void (*register_funcs)(LuastatusPluginData_v1 *pd, lua_State *L); // This function should run a widget. Once the plugin wants to update the widget, it should call // /lua_State *L = funcs.call_begin(pd->userdata)/, // thus obtaining a /lua_State */ object /L/. Then it must either: // 1. make the following call, with exactly one additional value pushed onto /L/'s stack: // /funcs.call_end(bd->userdata)/, // or // 2. make the following call, with any number of additional values push onto /L/'s stack: // /funcs.call_cancel(bd->userdata)/. // // Both /funcs.call_end/ and /funcs.call_cancel/ invalidate /L/ as a pointer. // // It is guaranteed that each /L/ object returned from /funcs.call_begin/ has at least 15 free // stack slots. // // It should only return on an unrecoverable failure. void (*run)(LuastatusPluginData_v1 *pd, LuastatusPluginRunFuncs_v1 funcs); // This function should destroy a previously successfully initialized widget. void (*destroy)(LuastatusPluginData_v1 *pd); } LuastatusPluginIface_v1; #endif ================================================ FILE: include/plugin_data_v1.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef luastatus_include_plugin_data_v1_h_ #define luastatus_include_plugin_data_v1_h_ #include "plugin_data.h" #define LuastatusPluginIface LuastatusPluginIface_v1 #define LuastatusPluginSayf LuastatusPluginSayf_v1 #define LuastatusPluginData LuastatusPluginData_v1 #define LuastatusPluginRunFuncs LuastatusPluginRunFuncs_v1 #endif ================================================ FILE: include/plugin_v1.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef luastatus_include_plugin_v1_h_ #define luastatus_include_plugin_v1_h_ #include #include "plugin_data_v1.h" #include "common.h" const int LUASTATUS_PLUGIN_LUA_VERSION_NUM = LUA_VERSION_NUM; extern LuastatusPluginIface_v1 luastatus_plugin_iface_v1; #endif ================================================ FILE: include/sayf_macros.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef luastatus_include_sayf_macros_ #define luastatus_include_sayf_macros_ #define LS_FATALF(Data_, ...) Data_->sayf(Data_->userdata, LUASTATUS_LOG_FATAL, __VA_ARGS__) #define LS_ERRF(Data_, ...) Data_->sayf(Data_->userdata, LUASTATUS_LOG_ERR, __VA_ARGS__) #define LS_WARNF(Data_, ...) Data_->sayf(Data_->userdata, LUASTATUS_LOG_WARN, __VA_ARGS__) #define LS_INFOF(Data_, ...) Data_->sayf(Data_->userdata, LUASTATUS_LOG_INFO, __VA_ARGS__) #define LS_VERBOSEF(Data_, ...) Data_->sayf(Data_->userdata, LUASTATUS_LOG_VERBOSE, __VA_ARGS__) #define LS_DEBUGF(Data_, ...) Data_->sayf(Data_->userdata, LUASTATUS_LOG_DEBUG, __VA_ARGS__) #define LS_TRACEF(Data_, ...) Data_->sayf(Data_->userdata, LUASTATUS_LOG_TRACE, __VA_ARGS__) #endif ================================================ FILE: libhackyfix/CMakeLists.txt ================================================ file (GLOB sources "*.c") add_library (hackyfix OBJECT ${sources}) target_compile_definitions (hackyfix PUBLIC -D_POSIX_C_SOURCE=200809L) # link against dl target_link_libraries (hackyfix PUBLIC ${CMAKE_DL_LIBS}) set_target_properties (ls PROPERTIES POSITION_INDEPENDENT_CODE ON) ================================================ FILE: libhackyfix/README.txt ================================================ Lua uses non-thread-safe functions all over the place and does another things that are very problematic in multi-threaded environments. Examples of things that are very problematic in multi-threaded environments: * Calling "fflush(NULL)" before calling popen() in its implementation of io.popen() function. The reasons why Lua does this are unknown to us. fflush(NULL) traverses all open FILE* objects, including read-only ones, acquires the lock on each one, flushes it, and then releases the lock. The problem is that "fflush(NULL)" hangs if, at the time of the call, at least one FILE* object is waiting on an input (e.g. blocked in fgetc(), getline() or similar) or is blocked on writing (e.g. when writing to a pipe). It only returns once all blocking stdio functions that are in-flight return, because all stdio functions (except unlocked_* ones) also lock the file. This makes using stdio in any of the threads very risky: Lua's io.popen() in another thread now may block for indefinite time. Note that Lua's stdlib also uses stdio all over the place. Examples of usage of non-thread-safe (according to POSIX-2008) functions: * exit(): only used in implementation of os.exit(). - we replace this Lua function in luastatus. * setlocale(): only used in implementation of os.setlocale(). - we replace this Lua function in luastatus. * system(): only used in implementation of os.execute(). - we replace this Lua function in luastatus. * dlerror(): used internally! * getenv(): used internally! And also in implementation of os.getenv(). * strerror(): used internally! * localeconv(): used internally! So this library replaces the following functions with thread-safe counterparts and/or different semantics to work around things that are very problematic in multi-threaded environments: * fflush(); * strerror(); * getenv(); * localeconv(); * dlerror() (unless compiled with glibc, where it *is* thread-safe). ================================================ FILE: libhackyfix/fatal.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "fatal.h" #include #include #include #include static void full_write(int fd, const char *buf, size_t nbuf) { size_t nwritten = 0; while (nwritten < nbuf) { ssize_t w = write(fd, buf + nwritten, nbuf - nwritten); if (w < 0) { break; } nwritten += w; } } void libhackyfix_fatal(const char *msg) { full_write(2, msg, strlen(msg)); abort(); } ================================================ FILE: libhackyfix/fatal.h ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef libhackyfix_fatal_h_ #define libhackyfix_fatal_h_ void libhackyfix_fatal(const char *msg); #endif ================================================ FILE: libhackyfix/get_rtld_next_handle.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef _GNU_SOURCE # define _GNU_SOURCE #endif #include "get_rtld_next_handle.h" #include void *libhackyfix_get_rtld_next_handle(void) { return RTLD_NEXT; } ================================================ FILE: libhackyfix/get_rtld_next_handle.h ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef libhackyfix_get_rtld_next_handle_h_ #define libhackyfix_get_rtld_next_handle_h_ void *libhackyfix_get_rtld_next_handle(void); #endif ================================================ FILE: libhackyfix/libhackyfix.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include "get_rtld_next_handle.h" #include "fatal.h" // dlerror() is not thread-safe at least on NetBSD and musl. // Let's assume other *BSDs and/or libcs have non-thread-safe dlerror() as well. #if defined(__GLIBC__) # define REPLACE_DLERROR 0 #else # define REPLACE_DLERROR 1 #endif static int (*orig_fflush)(FILE *f); static struct lconv *(*orig_localeconv)(void); static struct lconv my_lconv; static char *xstrdup_or_null(const char *s) { if (!s) { return NULL; } size_t ns = strlen(s); char *res = malloc(ns + 1); if (!res) { libhackyfix_fatal("FATAL: libhackyfix: malloc() failed\n"); } memcpy(res, s, ns + 1); return res; } static void query_and_copy_lconv(void) { struct lconv *cur = orig_localeconv(); my_lconv = *cur; #define copy(field) (my_lconv.field = xstrdup_or_null(cur->field)) copy(decimal_point); copy(thousands_sep); copy(grouping); copy(int_curr_symbol); copy(currency_symbol); copy(mon_decimal_point); copy(mon_thousands_sep); copy(mon_grouping); copy(positive_sign); copy(negative_sign); #undef copy } __attribute__((constructor)) static void initialize(void) { void *rtld_next_handle = libhackyfix_get_rtld_next_handle(); *(void **) &orig_fflush = dlsym(rtld_next_handle, "fflush"); if (!orig_fflush) { libhackyfix_fatal("FATAL: libhackyfix: dlsym(..., \"fflush\") failed\n"); } *(void **) &orig_localeconv = dlsym(rtld_next_handle, "localeconv"); if (!orig_localeconv) { libhackyfix_fatal("FATAL: libhackyfix: dlsym(..., \"localeconv\") failed\n"); } query_and_copy_lconv(); } __attribute__((destructor)) static void deinitialize(void) { free(my_lconv.decimal_point); free(my_lconv.thousands_sep); free(my_lconv.grouping); free(my_lconv.int_curr_symbol); free(my_lconv.currency_symbol); free(my_lconv.mon_decimal_point); free(my_lconv.mon_thousands_sep); free(my_lconv.mon_grouping); free(my_lconv.positive_sign); free(my_lconv.negative_sign); } // ================================================== // Replacement for /fflush/ // ================================================== int fflush(FILE *f) { if (!f) { return 0; } return orig_fflush(f); } // ================================================== // Replacement for /strerror/ // ================================================== #if _POSIX_C_SOURCE < 200112L || defined(_GNU_SOURCE) # error "Unsupported feature test macros." #endif static __thread char errbuf[512]; char *strerror(int errnum) { // We introduce an /int/ variable in order to get a compilation warning if /strerror_r()/ is // still GNU-specific and returns a pointer to char. int r = strerror_r(errnum, errbuf, sizeof(errbuf)); return r == 0 ? errbuf : (char *) "unknown error or truncated error message"; } // ================================================== // Replacement for /getenv/ // ================================================== extern char **environ; char *getenv(const char *name) { if (!environ) { return NULL; } if ((strchr(name, '='))) { return NULL; } size_t nname = strlen(name); for (char **s = environ; *s; ++s) { const char *entry = *s; if (strncmp(entry, name, nname) == 0 && entry[nname] == '=') { return (char *) (entry + nname + 1); } } return NULL; } // ================================================== // Replacement for /localeconv/ // ================================================== struct lconv *localeconv(void) { return &my_lconv; } #if REPLACE_DLERROR // ================================================== // Replacement for /dlerror/ // ================================================== char *dlerror(void) { return (char *) "(no error message for you because on your system dlerror() isn't thread-safe)"; } #endif ================================================ FILE: libls/CMakeLists.txt ================================================ file (GLOB sources "*.c") add_library (ls OBJECT ${sources}) include (CheckSymbolExists) set (CMAKE_REQUIRED_DEFINITIONS "-D_GNU_SOURCE") check_symbol_exists (pipe2 "fcntl.h;unistd.h" LS_HAVE_GNU_PIPE2) check_symbol_exists (SOCK_CLOEXEC "sys/socket.h" LS_HAVE_GNU_SOCK_CLOEXEC) configure_file ("ls_probes.in.h" "ls_probes.generated.h") target_compile_definitions (ls PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (ls LUA) target_include_directories (ls PUBLIC "${PROJECT_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}") find_library (MATH_LIBRARY m) if (MATH_LIBRARY) target_link_libraries (ls PUBLIC ${MATH_LIBRARY}) endif () set_target_properties (ls PROPERTIES POSITION_INDEPENDENT_CODE ON) ================================================ FILE: libls/ls_algo.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_algo_h_ #define ls_algo_h_ #define LS_ARRAY_SIZE(X_) (sizeof(X_) / sizeof((X_)[0])) #define LS_SWAP(Type_, X_, Y_) \ do { \ Type_ ls_swap_tmp_ = (X_); \ (X_) = (Y_); \ (Y_) = ls_swap_tmp_; \ } while (0) #endif ================================================ FILE: libls/ls_alloc_utils.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "ls_alloc_utils.h" #include #include #include #include "ls_panic.h" static inline bool mul_zu(size_t *dst, size_t a, size_t b) { if (b && a > SIZE_MAX / b) { return false; } *dst = a * b; return true; } void *ls_xmalloc(size_t nelems, size_t elemsz) { size_t total_bytes; if (!mul_zu(&total_bytes, nelems, elemsz)) { goto oom; } void *r = malloc(total_bytes); if (!r && total_bytes) { goto oom; } return r; oom: ls_oom(); } void *ls_xcalloc(size_t nelems, size_t elemsz) { void *r = calloc(nelems, elemsz); if (nelems && elemsz && !r) { ls_oom(); } return r; } void *ls_xrealloc(void *p, size_t nelems, size_t elemsz) { size_t total_bytes; if (!mul_zu(&total_bytes, nelems, elemsz)) { goto oom; } if (!total_bytes) { free(p); return NULL; } void *r = realloc(p, total_bytes); if (!r) { goto oom; } return r; oom: ls_oom(); } void *ls_x2realloc(void *p, size_t *pnelems, size_t elemsz) { LS_ASSERT(elemsz != 0); if (*pnelems) { if (!mul_zu(pnelems, *pnelems, 2)) { ls_oom(); } } else { *pnelems = 1; } return ls_xrealloc(p, *pnelems, elemsz); } void *ls_xmemdup(const void *p, size_t n) { void *r = malloc(n); if (n) { if (!r) { ls_oom(); } memcpy(r, p, n); } return r; } ================================================ FILE: libls/ls_alloc_utils.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_alloc_utils_h_ #define ls_alloc_utils_h_ #include #include #include "ls_panic.h" #include "ls_compdep.h" #define LS_XNEW(Type_, NElems_) \ ((Type_ *) ls_xmalloc(NElems_, sizeof(Type_))) #define LS_XNEW0(Type_, NElems_) \ ((Type_ *) ls_xcalloc(NElems_, sizeof(Type_))) #define LS_M_X2REALLOC(Ptr_, NElemsPtr_) \ ((__typeof__(Ptr_)) ls_x2realloc((Ptr_), (NElemsPtr_), sizeof((Ptr_)[0]))) #define LS_M_XMEMDUP(Ptr_, N_) \ ((__typeof__(Ptr_)) ls_xmemdup((Ptr_), ((size_t) (N_)) * sizeof((Ptr_)[0]))) #define LS_M_XREALLOC(Ptr_, N_) \ ((__typeof__(Ptr_)) ls_xrealloc((Ptr_), (N_), sizeof((Ptr_)[0]))) // Out-of-memory handler; should be called when an allocation fails. // // Should not return. #define ls_oom() LS_PANIC("out of memory") // The behavior is same as calling // /malloc(nelems * elemsz)/, // except when the multiplication overflows, or the allocation fails. In these cases, this function // panics. LS_ATTR_WARN_UNUSED void *ls_xmalloc(size_t nelems, size_t elemsz); // The behavior is same as calling // /calloc(nelems, elemsz)/, // except when the allocation fails. In that case, this function panics. LS_ATTR_WARN_UNUSED void *ls_xcalloc(size_t nelems, size_t elemsz); // The behavior is same as calling // /realloc(p, nelems * elemsz)/, // except when the multiplication overflows, or the reallocation fails. In these cases, this // function panics. // // Zero /nelems/ and/or /elemsz/ are supported (even though C23 declared /realloc/ to zero size // undefined behavior): if the total size requested is zero, this function /free/s the pointer // and returns NULL. LS_ATTR_WARN_UNUSED void *ls_xrealloc(void *p, size_t nelems, size_t elemsz); // The behavior is same as calling // /realloc(p, (*pnelems = F(*pnelems)) * elemsz)/, // where F(n) = max(1, 2 * n), // except when a multiplication overflows, or the reallocation fails. In these cases, this function // panics. // // Zero /elemsz/ is NOT supported: it isn't clear what the semantics of this function should be in // this case: technically you can store any number of zero-sized elements in an allocation of any // size, but /*pnelems/ is limited to /SIZE_MAX/ (should we alter /*pnelems/ at all? If yes, what // should we do in case of an overflow?). Also, C99 says there can't be any zero-sized types: empty // structs or unions, and arrays of size 0, are prohibited. LS_ATTR_WARN_UNUSED void *ls_x2realloc(void *p, size_t *pnelems, size_t elemsz); // Duplicates (as if with /malloc/) /n/ bytes of memory at address /p/. Panics on failure. LS_ATTR_WARN_UNUSED void *ls_xmemdup(const void *p, size_t n); // The behavior is same as calling // /strdup(s)/, // except when the allocation fails. In that case, this function panics. LS_INHEADER LS_ATTR_WARN_UNUSED char *ls_xstrdup(const char *s) { LS_ASSERT(s != NULL); return ls_xmemdup(s, strlen(s) + 1); } #endif ================================================ FILE: libls/ls_compdep.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_compdep_h_ #define ls_compdep_h_ // ------------------------------------------------------------ // GCC version (set by clang to some very old version) // ------------------------------------------------------------ #define LS_GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) // ------------------------------------------------------------ // Clang stuff // ------------------------------------------------------------ #if defined(__has_attribute) # define LS_CLANG_HAS_ATTRIBUTE(X_) __has_attribute(X_) #else # define LS_CLANG_HAS_ATTRIBUTE(X_) 0 #endif #if defined(__has_builtin) # define LS_CLANG_HAS_BUILTIN(X_) __has_builtin(X_) #else # define LS_CLANG_HAS_BUILTIN(X_) 0 #endif // ------------------------------------------------------------ // Attributes // ------------------------------------------------------------ #if __GNUC__ >= 2 # define LS_ATTR_UNUSED __attribute__((unused)) # define LS_ATTR_PRINTF(N_, M_) __attribute__((format(printf, N_, M_))) # define LS_ATTR_NORETURN __attribute__((noreturn)) #else # define LS_ATTR_UNUSED /*nothing*/ # define LS_ATTR_PRINTF(N_, M_) /*nothing*/ # define LS_ATTR_NORETURN /*nothing*/ #endif #if LS_GCC_VERSION >= 30406 || LS_CLANG_HAS_ATTRIBUTE(warn_unused_result) # define LS_ATTR_WARN_UNUSED __attribute__((warn_unused_result)) #else # define LS_ATTR_WARN_UNUSED /*nothing*/ #endif // ------------------------------------------------------------ // LS_INHEADER // ------------------------------------------------------------ #define LS_INHEADER static inline LS_ATTR_UNUSED #endif ================================================ FILE: libls/ls_cstring_utils.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #if _POSIX_C_SOURCE < 200112L || defined(_GNU_SOURCE) # error "Unsupported feature test macros; either tune them or change the code." #endif #include "ls_cstring_utils.h" #include #include const char *ls_strerror_r(int errnum, char *buf, size_t nbuf) { // luastatus-specific "fake" errno values switch (errnum) { case -EINVAL: return "Not a FIFO"; } // We introduce an /int/ variable in order to get a compilation warning if /strerror_r()/ is // still GNU-specific and returns a pointer to char. int r = strerror_r(errnum, buf, nbuf); return r == 0 ? buf : "unknown error or truncated error message"; } ================================================ FILE: libls/ls_cstring_utils.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_cstring_utils_h_ #define ls_cstring_utils_h_ #include #include #include "ls_compdep.h" #include "ls_panic.h" // If zero-terminated string /str/ starts with zero-terminated string /prefix/, returns // /str + strlen(prefix)/; otherwise, returns /NULL/. LS_INHEADER const char *ls_strfollow(const char *str, const char *prefix) { LS_ASSERT(str != NULL); LS_ASSERT(prefix != NULL); size_t nprefix = strlen(prefix); return strncmp(str, prefix, nprefix) == 0 ? str + nprefix : NULL; } // Behaves like the GNU-specific /strerror_r/: either fills /buf/ and returns it, or returns a // pointer to a static string. const char *ls_strerror_r(int errnum, char *buf, size_t nbuf); #endif ================================================ FILE: libls/ls_evloop_lfuncs.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_evloop_lfuncs_h_ #define ls_evloop_lfuncs_h_ #include #include #include #include #include #include #include #include "ls_compdep.h" #include "ls_panic.h" #include "ls_osdep.h" #include "ls_io_utils.h" #include "ls_time_utils.h" // Some plugins provide a "push_timeout"/"push_period" function that allows a widget to specify the // next timeout for an otherwise constant timeout-based plugin's event loop. // // The pushed timeout value has to be guarded with a lock as the "push_timeout"/"push_period" // function may be called from the /event/ function of a widget (that is, asynchronously from the // plugin's viewpoint). // // /LS_PushedTimeout/ is a structure containing the pushed timeout value (or an absence of such, as // indicated by the value being negative), and a lock. // // // This structure must reside at a constant address throughout its whole life; this is required for // the Lua closure created with /ls_pushed_timeout_push_luafunc()/. // typedef struct { double value; pthread_mutex_t lock; } LS_PushedTimeout; // Initializes /p/ with an absence of pushed timeout value and a newly-created lock. LS_INHEADER void ls_pushed_timeout_init(LS_PushedTimeout *p) { p->value = -1; LS_PTH_CHECK(pthread_mutex_init(&p->lock, NULL)); } // Does the following actions atomically: // * checks if /p/ has a pushed timeout value; // * if it does, clears it and returns the timeout value that /p/ has previously had; // * if it does not, returns /alt/. LS_INHEADER LS_TimeDelta ls_pushed_timeout_fetch(LS_PushedTimeout *p, LS_TimeDelta alt) { LS_TimeDelta r; LS_PTH_CHECK(pthread_mutex_lock(&p->lock)); if (p->value < 0) { r = alt; } else { r = ls_double_to_TD(p->value, LS_TD_FOREVER); p->value = -1; } LS_PTH_CHECK(pthread_mutex_unlock(&p->lock)); return r; } LS_INHEADER int ls_pushed_timeout_lfunc(lua_State *L) { double arg = luaL_checknumber(L, 1); if (!isgreaterequal(arg, 0.0)) { return luaL_argerror(L, 1, "invalid timeout"); } LS_PushedTimeout *p = lua_touserdata(L, lua_upvalueindex(1)); LS_PTH_CHECK(pthread_mutex_lock(&p->lock)); p->value = arg; LS_PTH_CHECK(pthread_mutex_unlock(&p->lock)); return 0; } // Creates a "push_timeout" function (a "C closure" with /p/'s address, in Lua terminology) on /L/'s // stack. // // The resulting Lua function takes one numeric argument (the timeout in seconds) and does not // return anything. It may throw an error if an invalid argument is provided. Once called, it will // (atomically) alter /p/'s timeout value. // // The caller must ensure that the /L/'s stack has at least 2 free slots. LS_INHEADER void ls_pushed_timeout_push_luafunc(LS_PushedTimeout *p, lua_State *L) { lua_pushlightuserdata(L, p); lua_pushcclosure(L, ls_pushed_timeout_lfunc, 1); } // Destroys /p/. LS_INHEADER void ls_pushed_timeout_destroy(LS_PushedTimeout *p) { LS_PTH_CHECK(pthread_mutex_destroy(&p->lock)); } // Some plugins provide the so-called self-pipe facility; that is, the ability to "wake up" the // widget's event loop (and force a call to the widget's /cb()/ function) from within the widget's // /event()/ function via a special "wake_up" Lua function. // // Such a pipe consists of an array, /int fds[2]/, of two file descriptors; the /fds[0]/ file // descriptor is to be read from, and /fds[1]/ is to be written to. // // // This array must reside at a constant address throughout its whole life; this is required for // the Lua closure created with /ls_self_pipe_push_luafunc()/. // // Creates a new self-pipe. // // On success, /0/ is returned. // // On failure, /-1/ is returned, /fds[0]/ and /fds[1]/ are set to -1, and /errno/ is set. LS_INHEADER int ls_self_pipe_open(int fds[2]) { if (ls_cloexec_pipe(fds) < 0) { fds[0] = -1; fds[1] = -1; return -1; } ls_make_nonblock(fds[0]); ls_make_nonblock(fds[1]); return 0; } LS_INHEADER int ls_self_pipe_lfunc(lua_State *L) { int *fds = lua_touserdata(L, lua_upvalueindex(1)); int fd = fds[1]; if (fd < 0) { return luaL_error(L, "self-pipe has not been opened"); } ssize_t unused = write(fd, "", 1); // write '\0' (void) unused; return 0; } // Creates a "wake_up" function (a "C closure" with /s/'s address, in Lua terminology) on /L/'s // stack. // // The resulting Lua function takes no arguments and does not return anything. Once called, it will // write a single byte to /fds[1]/ (in a thread-safe manner), or throw an error if the self-pipe // has not been opened. // // The caller must ensure that the /L/'s stack has at least 2 free slots. LS_INHEADER void ls_self_pipe_push_luafunc(int fds[2], lua_State *L) { lua_pushlightuserdata(L, fds); lua_pushcclosure(L, ls_self_pipe_lfunc, 1); } #endif ================================================ FILE: libls/ls_fifo_device.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_fifo_device_h_ #define ls_fifo_device_h_ #include #include "ls_compdep.h" #include "ls_time_utils.h" #include "ls_io_utils.h" // A "FIFO device" is a device that emits an event whenever somebody does // touch(1) (that is, opens for writing and then closes) on a FIFO file with // a specified path. // // If specified path is NULL, no events are ever reported. // // The suggested usage is as follows: // // LS_FifoDevice dev = ls_fifo_device_new(); // for (;;) { // // ... // if (ls_fifo_device_open(&dev, fifo_path) < 0) { // // ... handle error ... // } // if (ls_fifo_device_wait(&dev, timeout)) { /// // ... somebody touched the FIFO, do something ... // } // // ... // } // ls_fifo_device_close(&dev); typedef struct { int fd; } LS_FifoDevice; LS_INHEADER LS_FifoDevice ls_fifo_device_new(void) { return (LS_FifoDevice) {-1}; } LS_INHEADER int ls_fifo_device_open(LS_FifoDevice *dev, const char *path) { if (dev->fd >= 0) { return 0; } if (!path) { return 0; } dev->fd = ls_open_fifo(path); if (dev->fd < 0) { return -1; } return 0; } LS_INHEADER int ls_fifo_device_wait(LS_FifoDevice *dev, LS_TimeDelta tmo) { int r = ls_wait_input_on_fd(dev->fd, tmo); if (r > 0) { close(dev->fd); dev->fd = -1; } return r; } LS_INHEADER void ls_fifo_device_close(LS_FifoDevice *dev) { ls_close(dev->fd); } LS_INHEADER void ls_fifo_device_reset(LS_FifoDevice *dev) { ls_fifo_device_close(dev); *dev = ls_fifo_device_new(); } #endif ================================================ FILE: libls/ls_freemem.h ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_freemem_h_ #define ls_freemem_h_ #include #include "ls_alloc_utils.h" #include "ls_compdep.h" #include "ls_panic.h" // This function is called whenever a dynamic array is cleared. // We want to "preserve" at most 1 Kb of the previous capacity. LS_INHEADER LS_ATTR_WARN_UNUSED void *ls_freemem(void *data, size_t *psize, size_t *pcapacity, size_t elemsz) { LS_ASSERT(elemsz != 0); size_t new_capacity = 1024 / elemsz; if (*pcapacity > new_capacity) { data = ls_xrealloc(data, new_capacity, elemsz); *pcapacity = new_capacity; } *psize = 0; return data; } #define LS_M_FREEMEM(Data_, SizePtr_, CapacityPtr_) \ ((__typeof__(Data_)) ls_freemem( \ (Data_), \ (SizePtr_), \ (CapacityPtr_), \ sizeof(*(Data_)))) #endif ================================================ FILE: libls/ls_getenv_r.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "ls_getenv_r.h" #include #include #include "ls_panic.h" extern char **environ; const char *ls_getenv_r(const char *name) { LS_ASSERT(name != NULL); if (!environ) { return NULL; } if ((strchr(name, '='))) { return NULL; } size_t nname = strlen(name); for (char **s = environ; *s; ++s) { const char *entry = *s; if (strncmp(entry, name, nname) == 0 && entry[nname] == '=') { return entry + nname + 1; } } return NULL; } ================================================ FILE: libls/ls_getenv_r.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_getenv_r_h_ #define ls_getenv_r_h_ // Thread-safe /getenv/. const char *ls_getenv_r(const char *name); #endif ================================================ FILE: libls/ls_io_utils.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "ls_io_utils.h" #include #include #include #include #include #include #include "ls_time_utils.h" #include "ls_panic.h" static int poll_forever(struct pollfd *fds, size_t nfds) { int r; while ((r = poll(fds, nfds, -1)) < 0 && errno == EINTR) { // do nothing } return r; } int ls_poll(struct pollfd *fds, size_t nfds, LS_TimeDelta tmo) { if (nfds != (nfds_t) nfds) { errno = EOVERFLOW; return -1; } int tmo_ms = ls_TD_to_poll_ms_tmo(tmo); if (tmo_ms < 0) { return poll_forever(fds, nfds); } sigset_t allsigs; LS_ASSERT(sigfillset(&allsigs) == 0); sigset_t origmask; LS_PTH_CHECK(pthread_sigmask(SIG_SETMASK, &allsigs, &origmask)); int r = poll(fds, nfds, tmo_ms); int saved_errno = errno; LS_PTH_CHECK(pthread_sigmask(SIG_SETMASK, &origmask, NULL)); errno = saved_errno; return r; } int ls_open_fifo(const char *fifo) { LS_ASSERT(fifo != NULL); int saved_errno; int fd = open(fifo, O_RDONLY | O_CLOEXEC | O_NONBLOCK); if (fd < 0) { goto error; } struct stat sb; if (fstat(fd, &sb) < 0) { goto error; } if (!S_ISFIFO(sb.st_mode)) { errno = -EINVAL; goto error; } return fd; error: saved_errno = errno; ls_close(fd); errno = saved_errno; return -1; } ================================================ FILE: libls/ls_io_utils.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_io_utils_h_ #define ls_io_utils_h_ #include #include #include #include #include #include "ls_compdep.h" #include "ls_time_utils.h" #if EAGAIN == EWOULDBLOCK # define LS_IS_EAGAIN(E_) ((E_) == EAGAIN) #else # define LS_IS_EAGAIN(E_) ((E_) == EAGAIN || (E_) == EWOULDBLOCK) #endif // Makes a file descriptor close-on-exec. // On success, /fd/ is returned. // On failure, /-1/ is returned and /errno/ is set. LS_INHEADER int ls_make_cloexec(int fd) { int flags = fcntl(fd, F_GETFD); if (flags < 0) { return -1; } flags |= FD_CLOEXEC; if (fcntl(fd, F_SETFD, flags) < 0) { return -1; } return fd; } // Makes a file descriptor non-blocking. // On success, /fd/ is returned. // On failure, /-1/ is returned and /errno/ is set. LS_INHEADER int ls_make_nonblock(int fd) { int flags = fcntl(fd, F_GETFL); if (flags < 0) { return -1; } flags |= O_NONBLOCK; if (fcntl(fd, F_SETFL, flags) < 0) { return -1; } return fd; } // Close if non-negative: valgrind doesn't like /close()/ on negative FDs. LS_INHEADER int ls_close(int fd) { if (fd < 0) { return 0; } return close(fd); } // Poll restarting on /EINTR/ errors. int ls_poll(struct pollfd *fds, size_t nfds, LS_TimeDelta tmo); // Poll a single fd for /POLLIN/ events. LS_INHEADER int ls_wait_input_on_fd(int fd, LS_TimeDelta tmo) { struct pollfd pfd = {.fd = fd, .events = POLLIN}; return ls_poll(&pfd, 1, tmo); } // Open a FIFO for reading, and check that this is really a FIFO. int ls_open_fifo(const char *fifo); #endif ================================================ FILE: libls/ls_lua_compat.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_lua_compat_h_ #define ls_lua_compat_h_ #include #include #include #include #include #include #include "ls_compdep.h" #if LUA_VERSION_NUM >= 504 # define ls_lua_pushfail(L_) luaL_pushfail(L_) #else # define ls_lua_pushfail(L_) lua_pushnil(L_) #endif LS_INHEADER bool ls_lua_is_lua51(lua_State *L) { #if LUA_VERSION_NUM >= 502 (void) L; return false; #else // LuaJIT, when compiled with -DLUAJIT_ENABLE_LUA52COMPAT, still defines // LUA_VERSION_NUM to 501, but syntax parser, library functions and some // aspect of language work as if it was Lua 5.2. // L: ? lua_getglobal(L, "rawlen"); // L: ? rawlen bool ret = lua_isnil(L, -1); lua_pop(L, 1); // L: ? return ret; #endif } LS_INHEADER size_t ls_lua_array_len(lua_State *L, int pos) { #if LUA_VERSION_NUM <= 501 return lua_objlen(L, pos); #else return lua_rawlen(L, pos); #endif } #ifdef LUA_MAXINTEGER # define LS_LUA_MAXI \ (LUA_MAXINTEGER > (SIZE_MAX - 1) ? (SIZE_MAX - 1) : LUA_MAXINTEGER) #else # define LS_LUA_MAXI INT_MAX #endif LS_INHEADER int ls_lua_num_prealloc(size_t n) { return n < (size_t) INT_MAX ? (int) n : INT_MAX; } #endif ================================================ FILE: libls/ls_osdep.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #define _GNU_SOURCE #include "ls_osdep.h" #include #include #include #include "ls_probes.generated.h" int ls_cloexec_pipe(int pipefd[2]) { #if LS_HAVE_GNU_PIPE2 return pipe2(pipefd, O_CLOEXEC); #else if (pipe(pipefd) < 0) { return -1; } ls_make_cloexec(pipefd[0]); ls_make_cloexec(pipefd[1]); return 0; #endif } int ls_cloexec_socket(int domain, int type, int protocol) { #if LS_HAVE_GNU_SOCK_CLOEXEC return socket(domain, type | SOCK_CLOEXEC, protocol); #else int fd = socket(domain, type, protocol); if (fd < 0) { return -1; } ls_make_cloexec(fd); return fd; #endif } ================================================ FILE: libls/ls_osdep.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_osdep_h_ #define ls_osdep_h_ // We can't define /_GNU_SOURCE/ here, nor can we include "probes.generated.h", as // it may be located outside of the source tree: /libls/ is built with /libls/'s // binary directory included, unlike everything that includes its headers. // So no in-header functions. // The behavior is same as calling /pipe(pipefd)/, except that both file descriptors are made // close-on-exec. If the latter fails, the pipe is destroyed, /-1/ is returned and /errno/ is set. int ls_cloexec_pipe(int pipefd[2]); // The behavior is same as calling /socket(domain, type, protocol)/, except that the file // descriptor is make close-on-exec. If the latter fails, the pipe is destroyed, /-1/ is reurned and // /errno/ is set. int ls_cloexec_socket(int domain, int type, int protocol); #endif ================================================ FILE: libls/ls_panic.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "ls_panic.h" #include #include #include "ls_cstring_utils.h" void ls__do_panic_impl__( const char *macro_name, const char *expr, const char *msg, const char *func, const char *file, int line) { fprintf( stderr, "%s(%s) failed in %s at %s:%d: %s\n", macro_name, expr, func, file, line, msg); abort(); } void ls__do_panic_with_errnum_impl__( const char *macro_name, const char *expr, const char *msg, int errnum, const char *func, const char *file, int line) { char buf[512]; fprintf( stderr, "%s(%s) failed in %s at %s:%d: %s: %s\n", macro_name, expr, func, file, line, msg, ls_strerror_r(errnum, buf, sizeof(buf)) ); abort(); } ================================================ FILE: libls/ls_panic.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_panic_h_ #define ls_panic_h_ #include "ls_compdep.h" LS_ATTR_NORETURN void ls__do_panic_impl__( const char *macro_name, const char *expr, const char *msg, const char *func, const char *file, int line); LS_ATTR_NORETURN void ls__do_panic_with_errnum_impl__( const char *macro_name, const char *expr, const char *msg, int errnum, const char *func, const char *file, int line); #define LS__DO_PANIC__(MacroName_, Expr_, Msg_) \ ls__do_panic_impl__( \ MacroName_, \ Expr_, \ Msg_, \ __func__, __FILE__, __LINE__) #define LS__DO_PANIC_WITH_ERRNUM__(MacroName_, Expr_, Msg_, Errnum_) \ ls__do_panic_with_errnum_impl__( \ MacroName_, \ Expr_, \ Msg_, \ Errnum_, \ __func__, __FILE__, __LINE__) // Logs /Msg_/ and aborts. #define LS_PANIC(Msg_) \ LS__DO_PANIC__("LS_PANIC", "", Msg_) // Logs /Msg_/ with errno value /Errnum_/ and aborts. #define LS_PANIC_WITH_ERRNUM(Msg_, Errnum_) \ LS__DO_PANIC_WITH_ERRNUM__("LS_PANIC_WITH_ERRNUM", "", Msg_, Errnum_) // Asserts that a /pthread_*/ call was successful. #define LS_PTH_CHECK(Expr_) \ do { \ int ls_pth_rc_ = (Expr_); \ if (ls_pth_rc_ != 0) { \ LS__DO_PANIC_WITH_ERRNUM__("LS_PTH_CHECK", #Expr_, "check failed", ls_pth_rc_); \ } \ } while (0) // Asserts that /Expr_/ is true. #define LS_ASSERT(Expr_) \ do { \ if (!(Expr_)) { \ LS__DO_PANIC__("LS_ASSERT", #Expr_, "assertion failed"); \ } \ } while (0) // Just use it to tell the compiler some code is unreachable. #define LS_MUST_BE_UNREACHABLE() LS_PANIC("LS_MUST_BE_UNREACHABLE()") #endif ================================================ FILE: libls/ls_parse_int.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "ls_parse_int.h" #include #include int ls_strtou_b(const char *s, size_t ns, const char **endptr) { int ret = 0; size_t i = 0; for (; i != ns; ++i) { int digit = ((int) s[i]) - '0'; if (digit < 0 || digit > 9) { break; } if (ret > INT_MAX / 10) { ret = -ERANGE; break; } ret *= 10; if (ret > INT_MAX - digit) { ret = -ERANGE; break; } ret += digit; } if (endptr) { *endptr = s + i; } return ret; } int ls_full_strtou_b(const char *s, size_t ns) { if (!ns) { return -EINVAL; } const char *endptr; int r = ls_strtou_b(s, ns, &endptr); if (r < 0) { return r; } if (endptr != s + ns) { return -EINVAL; } return r; } ================================================ FILE: libls/ls_parse_int.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_parse_int_h_ #define ls_parse_int_h_ #include #include #include "ls_compdep.h" #include "ls_panic.h" // Parses (locale-independently) a decimal unsigned integer, inspecting no more than first /ns/ // characters of /s/. Once this limit is reached, or a non-digit character is found, this function // stops, writes the current position to /*endptr/, unless it is /NULL/; and returns what has been // parsed insofar (if nothing, /0/ is returned). // // If an overflow happens, /-ERANGE/ is returned. int ls_strtou_b(const char *s, size_t ns, const char **endptr); // Parses (locale-independently) a decimal unsigned integer using first /ns/ characters of /s/. // // If a non-digit character is found among them, or if /ns/ is /0/, /-EINVAL/ is returned. // If an overflow happens, /-ERANGE/ is returned. int ls_full_strtou_b(const char *s, size_t ns); // Parses (locale-independently) a decimal unsigned integer from a zero-terminated string /s/. // // If a non-digit character is found in /s/, or if /s/ is empty, /-EINVAL/ is returned. // If an overflow happens, /-ERANGE/ is returned. LS_INHEADER int ls_full_strtou(const char *s) { LS_ASSERT(s != NULL); return ls_full_strtou_b(s, strlen(s)); } #endif ================================================ FILE: libls/ls_probes.in.h ================================================ #ifndef ls_probes_h_ #define ls_probes_h_ #cmakedefine01 LS_HAVE_GNU_PIPE2 #cmakedefine01 LS_HAVE_GNU_SOCK_CLOEXEC #endif ================================================ FILE: libls/ls_strarr.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_strarr_h_ #define ls_strarr_h_ #include #include #include "ls_string.h" #include "ls_alloc_utils.h" #include "ls_compdep.h" #include "ls_freemem.h" #include "ls_panic.h" // An array of constant strings on a single buffer. Panics on allocation failure. typedef struct { size_t *data; size_t size; size_t capacity; } LS_StringArray_Offsets; typedef struct { LS_String buf; LS_StringArray_Offsets offsets; } LS_StringArray; LS_INHEADER LS_StringArray ls_strarr_new(void) { return (LS_StringArray) { .buf = ls_string_new(), .offsets = {NULL, 0, 0}, }; } LS_INHEADER LS_StringArray ls_strarr_new_reserve(size_t totlen, size_t nelems) { size_t *offsets_buf = LS_XNEW(size_t, nelems); return (LS_StringArray) { .buf = ls_string_new_reserve(totlen), .offsets = {offsets_buf, 0, nelems}, }; } LS_INHEADER void ls_strarr_append(LS_StringArray *sa, const char *buf, size_t nbuf) { LS_StringArray_Offsets *o = &sa->offsets; if (o->size == o->capacity) { o->data = LS_M_X2REALLOC(o->data, &o->capacity); } o->data[o->size++] = sa->buf.size; ls_string_append_b(&sa->buf, buf, nbuf); } LS_INHEADER void ls_strarr_append_s(LS_StringArray *sa, const char *s) { LS_ASSERT(s != NULL); ls_strarr_append(sa, s, strlen(s) + 1); } LS_INHEADER size_t ls_strarr_size(LS_StringArray sa) { return sa.offsets.size; } LS_INHEADER const char *ls_strarr_at(LS_StringArray sa, size_t i, size_t *n) { LS_StringArray_Offsets o = sa.offsets; LS_ASSERT(i < o.size); size_t begin = o.data[i]; size_t end = (i + 1 == o.size) ? sa.buf.size : o.data[i + 1]; if (n) { *n = end - begin; } return sa.buf.data + begin; } LS_INHEADER void ls_strarr_clear(LS_StringArray *sa) { ls_string_clear(&sa->buf); LS_StringArray_Offsets *o = &sa->offsets; o->data = LS_M_FREEMEM(o->data, &o->size, &o->capacity); } LS_INHEADER void ls_strarr_destroy(LS_StringArray sa) { ls_string_free(sa.buf); free(sa.offsets.data); } #endif ================================================ FILE: libls/ls_string.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "ls_string.h" #include #include #include #include "ls_panic.h" void ls_string_append_vf(LS_String *x, const char *fmt, va_list vl) { va_list vl2; va_copy(vl2, vl); size_t navail = x->capacity - x->size; int r = vsnprintf(x->data + x->size, navail, fmt, vl); if (r < 0) { goto fail; } if (((size_t) r) >= navail) { ls_string_ensure_avail(x, ((size_t) r) + 1); if (vsnprintf(x->data + x->size, ((size_t) r) + 1, fmt, vl2) < 0) { goto fail; } } x->size += r; va_end(vl2); return; fail: LS_PANIC_WITH_ERRNUM("ls_string_append_vf: vsnprintf() failed", errno); } ================================================ FILE: libls/ls_string.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_string_h_ #define ls_string_h_ #include #include #include #include #include "ls_compdep.h" #include "ls_alloc_utils.h" #include "ls_algo.h" #include "ls_freemem.h" #include "ls_panic.h" typedef struct { char *data; size_t size; size_t capacity; } LS_String; LS_INHEADER LS_String ls_string_new(void) { return (LS_String) {NULL, 0, 0}; } LS_INHEADER LS_String ls_string_new_reserve(size_t n) { char *buf = LS_XNEW(char, n); return (LS_String) {buf, 0, n}; } LS_INHEADER void ls_string_reserve(LS_String *x, size_t n) { if (x->capacity < n) { x->capacity = n; x->data = LS_M_XREALLOC(x->data, n); } } LS_INHEADER void ls_string_ensure_avail(LS_String *x, size_t n) { while (x->capacity - x->size < n) { x->data = LS_M_X2REALLOC(x->data, &x->capacity); } } LS_INHEADER void ls_string_clear(LS_String *x) { x->data = LS_M_FREEMEM(x->data, &x->size, &x->capacity); } LS_INHEADER void ls_string_assign_b(LS_String *x, const char *buf, size_t nbuf) { ls_string_reserve(x, nbuf); // see DOCS/c_notes/empty-ranges-and-c-stdlib.md if (nbuf) { memcpy(x->data, buf, nbuf); } x->size = nbuf; } LS_INHEADER void ls_string_assign_s(LS_String *x, const char *s) { LS_ASSERT(s != NULL); ls_string_assign_b(x, s, strlen(s)); } LS_INHEADER void ls_string_append_b(LS_String *x, const char *buf, size_t nbuf) { ls_string_ensure_avail(x, nbuf); // see DOCS/c_notes/empty-ranges-and-c-stdlib.md if (nbuf) { memcpy(x->data + x->size, buf, nbuf); } x->size += nbuf; } LS_INHEADER void ls_string_append_s(LS_String *x, const char *s) { LS_ASSERT(s != NULL); ls_string_append_b(x, s, strlen(s)); } LS_INHEADER void ls_string_append_c(LS_String *x, char c) { if (x->size == x->capacity) { x->data = LS_M_X2REALLOC(x->data, &x->capacity); } x->data[x->size++] = c; } // Appends a formatted string to /x/. LS_ATTR_PRINTF(2, 0) void ls_string_append_vf(LS_String *x, const char *fmt, va_list vl); // Appends a formatted string to /x/. LS_INHEADER LS_ATTR_PRINTF(2, 3) void ls_string_append_f(LS_String *x, const char *fmt, ...) { va_list vl; va_start(vl, fmt); ls_string_append_vf(x, fmt, vl); va_end(vl); } LS_ATTR_PRINTF(2, 0) LS_INHEADER void ls_string_assign_vf(LS_String *x, const char *fmt, va_list vl) { ls_string_clear(x); ls_string_append_vf(x, fmt, vl); } LS_INHEADER LS_ATTR_PRINTF(2, 3) void ls_string_assign_f(LS_String *s, const char *fmt, ...) { va_list vl; va_start(vl, fmt); ls_string_assign_vf(s, fmt, vl); va_end(vl); } LS_INHEADER LS_String ls_string_new_from_s(const char *s) { LS_String x = ls_string_new(); ls_string_assign_s(&x, s); return x; } LS_INHEADER LS_String ls_string_new_from_b(const char *buf, size_t nbuf) { LS_String x = ls_string_new(); ls_string_assign_b(&x, buf, nbuf); return x; } LS_INHEADER LS_ATTR_PRINTF(1, 0) LS_String ls_string_new_from_vf(const char *fmt, va_list vl) { LS_String x = ls_string_new(); ls_string_append_vf(&x, fmt, vl); return x; } LS_INHEADER LS_ATTR_PRINTF(1, 2) LS_String ls_string_new_from_f(const char *fmt, ...) { va_list vl; va_start(vl, fmt); LS_String x = ls_string_new_from_vf(fmt, vl); va_end(vl); return x; } LS_INHEADER bool ls_string_eq_b(LS_String x, const char *buf, size_t nbuf) { // We have to check that the size is not zero before calling /memcmp/: // see DOCS/c_notes/empty-ranges-and-c-stdlib.md return x.size == nbuf && (nbuf == 0 || memcmp(x.data, buf, nbuf) == 0); } LS_INHEADER bool ls_string_eq(LS_String x, LS_String y) { return ls_string_eq_b(x, y.data, y.size); } // Swaps two string efficiently (in O(1) time). LS_INHEADER void ls_string_swap(LS_String *x, LS_String *y) { LS_SWAP(LS_String, *x, *y); } LS_INHEADER void ls_string_free(LS_String x) { free(x.data); } #endif ================================================ FILE: libls/ls_time_utils.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_time_utils_h_ #define ls_time_utils_h_ #include #include #include #include #include #include #include #include "ls_compdep.h" #include "ls_panic.h" // Time deltas >= LS_TMO_MAX (in seconds) are treated as "forever". #define LS_TMO_MAX 2147483647.0 // These are wrapped into structs so that it's hard to do the wrong thing. // A time stamp. Can be either valid or "bad". typedef struct { double ts; } LS_TimeStamp; // A time delta. Can be either valid or "forever". typedef struct { double delta; } LS_TimeDelta; #define LS_TD_FOREVER ((LS_TimeDelta) {LS_TMO_MAX}) #define LS_TD_ZERO ((LS_TimeDelta) {0.0}) #define LS_TS_BAD ((LS_TimeStamp) {-1.0}) LS_INHEADER bool ls_double_is_good_time_delta(double x) { return isgreaterequal(x, 0.0); } LS_INHEADER bool ls_TD_is_forever(LS_TimeDelta TD) { return TD.delta >= LS_TMO_MAX; } LS_INHEADER bool ls_TS_is_bad(LS_TimeStamp TS) { return TS.ts < 0; } LS_INHEADER bool ls_double_to_TD_checked(double delta, LS_TimeDelta *out) { if (!ls_double_is_good_time_delta(delta)) { return false; } if (delta > LS_TMO_MAX) { delta = LS_TMO_MAX; } *out = (LS_TimeDelta) {delta}; return true; } LS_INHEADER LS_TimeDelta ls_double_to_TD(double delta, LS_TimeDelta if_bad) { LS_TimeDelta res; if (ls_double_to_TD_checked(delta, &res)) { return res; } return if_bad; } LS_INHEADER LS_TimeDelta ls_double_to_TD_or_die(double delta) { LS_TimeDelta res; if (!ls_double_to_TD_checked(delta, &res)) { LS_PANIC("ls_double_to_TD_or_die() failed"); } return res; } LS_INHEADER double ls_timespec_to_raw_double(struct timespec ts) { double s = ts.tv_sec; double ns = ts.tv_nsec; return s + ns / 1e9; } LS_INHEADER LS_TimeStamp ls_timespec_to_TS(struct timespec ts) { return (LS_TimeStamp) {ls_timespec_to_raw_double(ts)}; } LS_INHEADER LS_TimeDelta ls_timespec_to_TD(struct timespec ts) { return (LS_TimeDelta) {ls_timespec_to_raw_double(ts)}; } LS_INHEADER struct timespec ls_now_timespec(void) { struct timespec ts; int rc; #if (!defined(_POSIX_MONOTONIC_CLOCK)) || (_POSIX_MONOTONIC_CLOCK < 0) // CLOCK_MONOTONIC is not supported at compile-time. rc = clock_gettime(CLOCK_REALTIME, &ts); #elif _POSIX_MONOTONIC_CLOCK > 0 // CLOCK_MONOTONIC is supported both at compile-time and at run-time. rc = clock_gettime(CLOCK_MONOTONIC, &ts); #else // CLOCK_MONOTONIC is supported at compile-time, but might or might not // be supported at run-time. rc = clock_gettime(CLOCK_MONOTONIC, &ts); if (rc < 0) { rc = clock_gettime(CLOCK_REALTIME, &ts); } #endif if (rc < 0) { LS_PANIC("clock_gettime() failed"); } return ts; } LS_INHEADER LS_TimeStamp ls_now(void) { return ls_timespec_to_TS(ls_now_timespec()); } LS_INHEADER struct timespec ls_TD_to_timespec(LS_TimeDelta TD) { LS_ASSERT(!ls_TD_is_forever(TD)); double delta = TD.delta; return (struct timespec) { .tv_sec = delta, .tv_nsec = (delta - (time_t) delta) * 1e9, }; } LS_INHEADER struct timeval ls_TD_to_timeval(LS_TimeDelta TD) { LS_ASSERT(!ls_TD_is_forever(TD)); double delta = TD.delta; return (struct timeval) { .tv_sec = delta, .tv_usec = (delta - (time_t) delta) * 1e6, }; } LS_INHEADER int ls_TD_to_poll_ms_tmo(LS_TimeDelta TD) { if (ls_TD_is_forever(TD)) { return -1; } double ms = TD.delta * 1000; return ms > INT_MAX ? INT_MAX : ms; } LS_INHEADER LS_TimeDelta ls_TS_minus_TS_nonneg(LS_TimeStamp a, LS_TimeStamp b) { LS_ASSERT(!ls_TS_is_bad(a)); LS_ASSERT(!ls_TS_is_bad(b)); double res = a.ts - b.ts; return (LS_TimeDelta) {res < 0 ? 0 : res}; } LS_INHEADER LS_TimeStamp ls_TS_plus_TD(LS_TimeStamp a, LS_TimeDelta b) { LS_ASSERT(!ls_TS_is_bad(a)); LS_ASSERT(!ls_TD_is_forever(b)); return (LS_TimeStamp) {a.ts + b.delta}; } LS_INHEADER bool ls_TD_less(LS_TimeDelta a, LS_TimeDelta b) { if (ls_TD_is_forever(a) && ls_TD_is_forever(b)) { return false; } return a.delta < b.delta; } LS_INHEADER void ls_sleep(LS_TimeDelta TD) { if (ls_TD_is_forever(TD)) { for (;;) { pause(); } } struct timespec ts = ls_TD_to_timespec(TD); struct timespec rem; while (nanosleep(&ts, &rem) < 0 && errno == EINTR) { ts = rem; } } LS_INHEADER void ls_sleep_simple(double seconds) { ls_sleep(ls_double_to_TD_or_die(seconds)); } #endif ================================================ FILE: libls/ls_tls_ebuf.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "ls_tls_ebuf.h" static __thread char ebuf[LS_TLS_EBUF_N]; char *ls_tls_ebuf(void) { return ebuf; } ================================================ FILE: libls/ls_tls_ebuf.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_tls_ebuf_h_ #define ls_tls_ebuf_h_ #include "ls_compdep.h" #include "ls_cstring_utils.h" // Thread-local storage buffer. Used mostly as a buffer for /ls_strerror_r/. enum { LS_TLS_EBUF_N = 512 }; char *ls_tls_ebuf(void); LS_INHEADER const char *ls_tls_strerror(int errnum) { return ls_strerror_r(errnum, ls_tls_ebuf(), LS_TLS_EBUF_N); } #endif ================================================ FILE: libls/ls_xallocf.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "ls_xallocf.h" #include #include #include #include #include "ls_alloc_utils.h" #include "ls_panic.h" char *ls_xallocvf(const char *fmt, va_list vl) { va_list vl2; va_copy(vl2, vl); int n = vsnprintf(NULL, 0, fmt, vl); if (n < 0) { goto fail; } size_t nbuf = ((size_t) n) + 1; char *r = LS_XNEW(char, nbuf); if (vsnprintf(r, nbuf, fmt, vl2) < 0) { goto fail; } va_end(vl2); return r; fail: LS_PANIC_WITH_ERRNUM("ls_xallocvf: vsnprintf() failed", errno); } ================================================ FILE: libls/ls_xallocf.h ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ls_xallocf_h_ #define ls_xallocf_h_ #include #include "ls_compdep.h" // Functions below allocate a formatted C string (as with with 'malloc()'). // They panic on failure (including out-of-memory condition, bad format // string, etc). LS_ATTR_PRINTF(1, 0) char *ls_xallocvf(const char *fmt, va_list vl); LS_INHEADER LS_ATTR_PRINTF(1, 2) char *ls_xallocf(const char *fmt, ...) { va_list vl; va_start(vl, fmt); char *r = ls_xallocvf(fmt, vl); va_end(vl); return r; } #endif ================================================ FILE: libmoonvisit/CMakeLists.txt ================================================ file (GLOB sources "*.c") add_library (moonvisit OBJECT ${sources}) target_compile_definitions (moonvisit PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (moonvisit LUA) find_library (MATH_LIBRARY m) if (MATH_LIBRARY) target_link_libraries (moonvisit PUBLIC ${MATH_LIBRARY}) endif () set_target_properties (moonvisit PROPERTIES POSITION_INDEPENDENT_CODE ON) ================================================ FILE: libmoonvisit/README.txt ================================================ Just a tiny library that helps with traversing complex Lua structures. ================================================ FILE: libmoonvisit/moonvisit.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "moonvisit.h" #include #include #include #include #include #include #include #include static void oom_handler(void) { fputs("FATAL ERROR: out of memory (libmoonvisit).\n", stderr); abort(); } // Copies zero-terminated 'src' into buffer 'dst' of size 'ndst', possibly truncating, but always // zero-terminating the output (unless 'ndst' is 0, of course). // // Returns number of bytes written, not including terminating NUL. static size_t write_str(char *dst, size_t ndst, const char *src) { if (!ndst) { return 0; } size_t ncopy = strnlen(src, ndst - 1); memcpy(dst, src, ncopy); dst[ncopy] = '\0'; return ncopy; } bool moon_visit_err(MoonVisit *mv, const char *fmt, ...) { size_t off = 0; if (mv->where) { off += write_str(mv->errbuf + off, mv->nerrbuf - off, mv->where); off += write_str(mv->errbuf + off, mv->nerrbuf - off, ": "); } va_list vl; va_start(vl, fmt); int r = vsnprintf(mv->errbuf + off, mv->nerrbuf - off, fmt, vl); va_end(vl); if (r < 0) { mv->errbuf[0] = '\0'; return false; } return true; } int moon_visit_checktype_at( MoonVisit *mv, const char *what, int pos, int type) { int got_type = lua_type(mv->L, pos); if (got_type != type) { if (what) { moon_visit_err( mv, "%s: expected %s, found %s", what, lua_typename(mv->L, type), lua_typename(mv->L, got_type)); } else { moon_visit_err( mv, "expected %s, found %s", lua_typename(mv->L, type), lua_typename(mv->L, got_type)); } return -1; } return 0; } int moon_visit_str_f( MoonVisit *mv, int table_pos, const char *key, int (*f)(MoonVisit *mv, void *ud, const char *s, size_t ns), void *ud, bool skip_nil) { int top = lua_gettop(mv->L); int r; lua_getfield(mv->L, table_pos, key); if (skip_nil && lua_isnil(mv->L, -1)) { r = 0; goto done; } if (moon_visit_checktype_at(mv, key, -1, LUA_TSTRING) < 0) { r = -1; goto done; } size_t ns; const char *s = lua_tolstring(mv->L, -1, &ns); const char *old_where = mv->where; r = f(mv, ud, s, ns); mv->where = old_where; done: lua_settop(mv->L, top); return r; } int moon_visit_str( MoonVisit *mv, int table_pos, const char *key, char **ps, size_t *pn, bool skip_nil) { int r; lua_getfield(mv->L, table_pos, key); if (skip_nil && lua_isnil(mv->L, -1)) { r = 0; goto done; } if (moon_visit_checktype_at(mv, key, -1, LUA_TSTRING) < 0) { r = -1; goto done; } size_t ns; const char *s = lua_tolstring(mv->L, -1, &ns); char *buf = malloc(ns + 1); if (!buf) { oom_handler(); } memcpy(buf, s, ns + 1); *ps = buf; if (pn) { *pn = ns; } r = 1; done: lua_pop(mv->L, 1); return r; } int moon_visit_num( MoonVisit *mv, int table_pos, const char *key, double *p, bool skip_nil) { int r; lua_getfield(mv->L, table_pos, key); if (skip_nil && lua_isnil(mv->L, -1)) { r = 0; goto done; } if (moon_visit_checktype_at(mv, key, -1, LUA_TNUMBER) < 0) { r = -1; goto done; } *p = lua_tonumber(mv->L, -1); r = 1; done: lua_pop(mv->L, 1); return r; } int moon_visit_bool( MoonVisit *mv, int table_pos, const char *key, bool *p, bool skip_nil) { int r; lua_getfield(mv->L, table_pos, key); if (skip_nil && lua_isnil(mv->L, -1)) { r = 0; goto done; } if (moon_visit_checktype_at(mv, key, -1, LUA_TBOOLEAN) < 0) { r = -1; goto done; } *p = lua_toboolean(mv->L, -1); r = 1; done: lua_pop(mv->L, 1); return r; } int moon_visit_table_f( MoonVisit *mv, int table_pos, const char *key, int (*f)(MoonVisit *mv, void *ud, int kpos, int vpos), void *ud, bool skip_nil) { int r; lua_getfield(mv->L, table_pos, key); if (skip_nil && lua_isnil(mv->L, -1)) { r = 0; goto done; } r = moon_visit_table_f_at(mv, key, -1, f, ud); done: lua_pop(mv->L, 1); return r; } static inline bool aux_is_first_key_numeric(lua_State *L, int pos) { // L: ? int adj_pos = pos < 0 ? (pos - 1) : pos; lua_pushnil(L); // L: ? nil if (!lua_next(L, adj_pos)) { // L: ? return false; } // L: ? key value bool res = lua_isnumber(L, -2); lua_pop(L, 2); // L: ? return res; } static inline size_t aux_get_array_len(lua_State *L, int pos) { #if LUA_VERSION_NUM <= 501 return lua_objlen(L, pos); #else return lua_rawlen(L, pos); #endif } int moon_visit_table_f_at( MoonVisit *mv, const char *what, int pos, int (*f)(MoonVisit *mv, void *ud, int kpos, int vpos), void *ud) { int top = lua_gettop(mv->L); int r; if (moon_visit_checktype_at(mv, what, pos, LUA_TTABLE) < 0) { r = -1; goto done; } r = 0; const char *old_where = mv->where; if (aux_is_first_key_numeric(mv->L, pos)) { int adj_pos = pos < 0 ? (pos - 1) : pos; size_t len = aux_get_array_len(mv->L, pos); // L: ? for (size_t i = 1; i <= len; ++i) { lua_pushinteger(mv->L, i); // mv->L: ? i lua_rawgeti(mv->L, adj_pos, i); // mv->L: ? i value r = f(mv, ud, -2, -1); mv->where = old_where; if (r < 0) { break; } lua_pop(mv->L, 2); // mv->L: ? } } else { int adj_pos = pos < 0 ? (pos - 1) : pos; // mv->L: ? lua_pushnil(mv->L); // mv->L: ? nil while (lua_next(mv->L, adj_pos)) { // mv->L: ? key value r = f(mv, ud, -2, -1); mv->where = old_where; if (r < 0) { break; } lua_pop(mv->L, 1); // mv->L: ? key } } lua_settop(mv->L, top); done: return r; } int moon_visit_sint( MoonVisit *mv, int table_pos, const char *key, int64_t *p, bool skip_nil) { double d; int r = moon_visit_num(mv, table_pos, key, &d, skip_nil); if (r > 0) { if (isnan(d)) { moon_visit_err(mv, "%s: is NaN", key); return -1; } if (!(d >= -9223372036854775808.0 && d < 9223372036854775808.0)) { moon_visit_err(mv, "%s: value out of range INT64_MIN...INT64_MAX", key); return -1; } *p = d; } return r; } int moon_visit_uint( MoonVisit *mv, int table_pos, const char *key, uint64_t *p, bool skip_nil) { double d; int r = moon_visit_num(mv, table_pos, key, &d, skip_nil); if (r > 0) { if (isnan(d)) { moon_visit_err(mv, "%s: is NaN", key); return -1; } if (!(d >= 0.0 && d < 18446744073709551616.0)) { moon_visit_err(mv, "%s: value out of range 0...UINT64_MAX", key); return -1; } *p = d; } return r; } int moon_visit_scrutinize_table( MoonVisit *mv, int table_pos, const char *key, bool nil_ok) { lua_getfield(mv->L, table_pos, key); int type = lua_type(mv->L, -1); if (type == LUA_TNIL && nil_ok) { return 0; } if (moon_visit_checktype_at(mv, key, -1, LUA_TTABLE) < 0) { lua_pop(mv->L, 1); return -1; } return 0; } int moon_visit_scrutinize_str( MoonVisit *mv, int table_pos, const char *key, const char **out, size_t *out_len, bool nil_ok) { lua_getfield(mv->L, table_pos, key); int type = lua_type(mv->L, -1); if (type == LUA_TNIL && nil_ok) { *out = NULL; if (out_len) { *out_len = 0; } return 0; } if (moon_visit_checktype_at(mv, key, -1, LUA_TSTRING) < 0) { lua_pop(mv->L, 1); return -1; } if (out_len) { *out = lua_tolstring(mv->L, -1, out_len); } else { *out = lua_tostring(mv->L, -1); } return 0; } ================================================ FILE: libmoonvisit/moonvisit.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef moonvisit_h_ #define moonvisit_h_ #include #include #include #include #if ! defined(MOON_VISIT_PRINTF_ATTR) # if __GNUC__ >= 2 # define MOON_VISIT_PRINTF_ATTR(N, M) __attribute__((format(printf, N, M))) # else # define MOON_VISIT_PRINTF_ATTR(N, M) /*nothing*/ # endif #endif typedef struct { // Filled by user, never altered (as a pointer) by library. lua_State *L; // Filled by user, never altered (as a pointer) by library. // // On error, this buffer is written to, assuming it has the size of 'nerrbuf', and is always // zero-terminated (unless 'nerrbuf' is 0, of course). char *errbuf; // Filled by the user, never altered by library. size_t nerrbuf; // Filled by user, but might be altered (as a pointer) by the library in the process of // saving/restoring old value of 'where'. const char *where; } MoonVisit; // Prints a formatted string into 'mv->errbuf' (prepended with ": " if 'mv->where' is not // NULL). // // Returns true on success, false on "encoding error". MOON_VISIT_PRINTF_ATTR(2, 3) bool moon_visit_err(MoonVisit *mv, const char *fmt, ...); // Checks that the element at stack position 'pos' in 'mv->L' has type of 'type'. // The fact that 'pos' is a valid stack index is *not* checked, simply assumed to be true. // // 'type' must be one of LUA_T* constants. // // 'what' must be string description of the value that is being tested. // // On success, returns 0. // // On failure, returns -1 and writes the error message into 'mv->errbuf'. int moon_visit_checktype_at( MoonVisit *mv, const char *what, int pos, int type); int moon_visit_str_f( MoonVisit *mv, int table_pos, const char *key, int (*f)(MoonVisit *mv, void *ud, const char *s, size_t ns), void *ud, bool skip_nil); // Duplicates the string as if with 'malloc()', writing zero-terminated duplicated string into // '*ps'; and, if 'pn' is not NULL, the length into '*pn'. int moon_visit_str( MoonVisit *mv, int table_pos, const char *key, char **ps, size_t *pn, bool skip_nil); int moon_visit_num( MoonVisit *mv, int table_pos, const char *key, double *p, bool skip_nil); int moon_visit_bool( MoonVisit *mv, int table_pos, const char *key, bool *p, bool skip_nil); int moon_visit_table_f( MoonVisit *mv, int table_pos, const char *key, int (*f)(MoonVisit *mv, void *ud, int kpos, int vpos), void *ud, bool skip_nil); int moon_visit_table_f_at( MoonVisit *mv, const char *what, int pos, int (*f)(MoonVisit *mv, void *ud, int kpos, int vpos), void *ud); // Beware: current implementation can only faithfully (without precision loss) fetch integers that // fit into 'double' (on virtually every platform it means integers with absolute value <= 2^53). int moon_visit_sint( MoonVisit *mv, int table_pos, const char *key, int64_t *p, bool skip_nil); // Beware: current implementation can only faithfully (without precision loss) fetch integers that // fit into 'double' (on virtually every platform it means values that are <= 2^53). int moon_visit_uint( MoonVisit *mv, int table_pos, const char *key, uint64_t *p, bool skip_nil); // Pushes /t[key]/ on the stack, where /t/ is the table residing at stack position /table_pos/. // Checks that the result is a table. // If /nil_ok/ is true and the result is nil, pushes a nil instead of a table. int moon_visit_scrutinize_table( MoonVisit *mv, int table_pos, const char *key, bool nil_ok); // Pushes /t[key]/ on the stack, where /t/ is the table residing at stack position /table_pos/. // // Checks that the result is a string. If so, /*out/ is set to the contents of the string, // and, if /out_len != NULL/, writes the length of the string into /*out_len/. // // If /nil_ok/ is true and the result is nil, pushes a nil instead of a table, sets /*out/ to // NULL, and, if /out_len != NULL/, sets /*out_len/ to zero. int moon_visit_scrutinize_str( MoonVisit *mv, int table_pos, const char *key, const char **out, size_t *out_len, bool nil_ok); #endif ================================================ FILE: libprocalive/CMakeLists.txt ================================================ file (GLOB sources "*.c") add_library (procalive OBJECT ${sources}) target_compile_definitions (procalive PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (procalive LUA) find_library (MATH_LIBRARY m) if (MATH_LIBRARY) target_link_libraries (procalive PUBLIC ${MATH_LIBRARY}) endif () set_target_properties (procalive PROPERTIES POSITION_INDEPENDENT_CODE ON) ================================================ FILE: libprocalive/procalive_lfuncs.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "procalive_lfuncs.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if _POSIX_C_SOURCE < 200112L || defined(_GNU_SOURCE) # error "Unsupported feature test macros." #endif static __thread char errbuf[512]; static const char *my_strerror(int errnum) { // We introduce an /int/ variable in order to get a compilation warning if /strerror_r()/ is // still GNU-specific and returns a pointer to char. int r = strerror_r(errnum, errbuf, sizeof(errbuf)); return r == 0 ? errbuf : "unknown error or truncated error message"; } #ifdef LUA_MAXINTEGER # define MAXI \ (LUA_MAXINTEGER > (SIZE_MAX - 1) ? (SIZE_MAX - 1) : LUA_MAXINTEGER) #else # define MAXI INT_MAX #endif int procalive_lfunc_access(lua_State *L) { const char *path = luaL_checkstring(L, 1); if (access(path, F_OK) >= 0) { lua_pushboolean(L, 1); // L: true lua_pushnil(L); // L: true nil } else { int saved_errno = errno; lua_pushboolean(L, 0); // L: false if (saved_errno == ENOENT) { lua_pushnil(L); // L: false nil } else { lua_pushstring(L, my_strerror(saved_errno)); // L: false str } } return 2; } int procalive_lfunc_stat(lua_State *L) { const char *path = luaL_checkstring(L, 1); struct stat sb; if (stat(path, &sb) < 0) { int saved_errno = errno; lua_pushnil(L); // L: nil lua_pushstring(L, my_strerror(saved_errno)); // L: nil err return 2; } mode_t M = sb.st_mode; const char *M_str; if (S_ISREG(M)) { M_str = "regular"; } else if (S_ISDIR(M)) { M_str = "dir"; } else if (S_ISCHR(M)) { M_str = "chardev"; } else if (S_ISBLK(M)) { M_str = "blockdev"; } else if (S_ISFIFO(M)) { M_str = "fifo"; } else if (S_ISLNK(M)) { M_str = "symlink"; } else if (S_ISSOCK(M)) { M_str = "socket"; } else { M_str = "other"; } lua_pushstring(L, M_str); // L: res lua_pushnil(L); // L: res nil return 2; } static inline int get_lua_num_prealloc(size_t n) { return n < (size_t) INT_MAX ? (int) n : INT_MAX; } static bool push_glob_t(lua_State *L, glob_t *g) { size_t n = g->gl_pathc; if (n > (size_t) MAXI) { return false; } lua_createtable(L, get_lua_num_prealloc(n), 0); // L: array for (size_t i = 0; i < n; ++i) { lua_pushstring(L, g->gl_pathv[i]); // L: array str lua_rawseti(L, -2, i + 1); // L: array } return true; } int procalive_lfunc_glob(lua_State *L) { const char *pattern = luaL_checkstring(L, 1); glob_t g = {0}; int rc = glob(pattern, GLOB_MARK | GLOB_NOSORT, NULL, &g); int err_num; switch (rc) { case 0: if (!push_glob_t(L, &g)) { err_num = EOVERFLOW; goto fail; } // L: res goto ok; case GLOB_NOMATCH: lua_newtable(L); // L: res goto ok; case GLOB_ABORTED: err_num = EIO; goto fail; case GLOB_NOSPACE: err_num = ENOMEM; goto fail; default: err_num = EINVAL; goto fail; } ok: globfree(&g); // L: res lua_pushnil(L); // L: res nil return 2; fail: globfree(&g); lua_pushnil(L); // L: nil lua_pushstring(L, my_strerror(err_num)); // L: nil err return 2; } static pid_t parse_pid(const char *s, const char **out_err_msg) { char *endptr; errno = 0; intmax_t mres = strtoimax(s, &endptr, 10); if (errno != 0 || *s == '\0' || *endptr != '\0') { *out_err_msg = "cannot parse into intmax_t"; return -1; } pid_t res = mres; if (res <= 0 || res != mres) { *out_err_msg = "outside of valid range for pid_t"; return -1; } return res; } int procalive_lfunc_is_process_alive(lua_State *L) { luaL_checkany(L, 1); pid_t pid; if (lua_isstring(L, 1)) { const char *err_msg; pid = parse_pid(lua_tostring(L, 1), &err_msg); if (pid <= 0) { return luaL_argerror(L, 1, err_msg); } } else if (lua_isnumber(L, 1)) { double d = lua_tonumber(L, 1); if (!isgreaterequal(d, 1.0)) { return luaL_argerror(L, 1, "must be >= 1"); } if (d > INT_MAX) { return luaL_argerror(L, 1, "too large"); } int i = (int) d; pid = (pid_t) i; if (pid <= 0 || pid != i) { return luaL_argerror(L, 1, "outside of valid range for pid_t"); } } else { return luaL_argerror(L, 1, "neither number nor string"); } bool res = (kill(pid, 0) >= 0 || errno == EPERM); lua_pushboolean(L, res); // L: res return 1; } void procalive_lfuncs_register_all(lua_State *L) { // L: table lua_pushcfunction(L, procalive_lfunc_access); // L: table func lua_setfield(L, -2, "access"); // L: table lua_pushcfunction(L, procalive_lfunc_stat); // L: table func lua_setfield(L, -2, "stat"); // L: table lua_pushcfunction(L, procalive_lfunc_glob); // L: table func lua_setfield(L, -2, "glob"); // L: table lua_pushcfunction(L, procalive_lfunc_is_process_alive); // L: table func lua_setfield(L, -2, "is_process_alive"); // L: table } ================================================ FILE: libprocalive/procalive_lfuncs.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef libprocalive_procalive_lfuncs_h_ #define libprocalive_procalive_lfuncs_h_ #include // Lua function access(path): // Checks if a given path exists, as if with 'access(path, F_OK)'. If it does // exist, returns 'true, nil'. If it does not, returns 'false, nil'. If an error // occurs, returns 'false, err_msg'. int procalive_lfunc_access(lua_State *L); // Lua function stat(path): // Tries to get the type of the file at the given path. On success returns either of: // * "regular", // * "dir" (directory), // * "chardev" (character device), // * "blockdev" (block device), // * "fifo", // * "symlink", // * "socket", // * "other". // On failure returns '`'nil, err_msg'. int procalive_lfunc_stat(lua_State *L); // Lua function glob(pattern): // Performs glob expansion of pattern. A glob is a wildcard pattern like /tmp/*.txt // that can be applied as a filter to a list of existing file names. Supported // expansions are *, ? and [...]. Please refer to glob(7) for more information on // wildcard patterns. // // Note also that the globbing is performed with GLOB_MARK flag, so that in output, // directories have trailing slash appended to their name. // // Returns 'arr, nil' on success, where 'arr' is an array of strings; these are // existing file names that matched the given pattern. The order is arbitrary. // // On failure, returns 'nil, err_msg'. int procalive_lfunc_glob(lua_State *L); // Lua function is_process_alive(pid): // Checks if a process with PID 'pid' is currently alive. 'pid' must be either a // or a string. // Returns a boolean that indicates whether the process is alive. int procalive_lfunc_is_process_alive(lua_State *L); // Registers all functions described above into a table on the top of /L/'s stack. void procalive_lfuncs_register_all(lua_State *L); #endif ================================================ FILE: librunshell/CMakeLists.txt ================================================ file (GLOB sources "*.c") add_library (runshell OBJECT ${sources}) target_compile_definitions (runshell PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (runshell LUA) set_target_properties (runshell PROPERTIES POSITION_INDEPENDENT_CODE ON) ================================================ FILE: librunshell/README.txt ================================================ A thread-safe (modulo thread cancellation) version of system(). It also does not modify SIGQUIT/SIGINT signal dispositions. ================================================ FILE: librunshell/runshell.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "runshell.h" #include #include #include #include #include #include #include #include #include #include #include #if LUA_VERSION_NUM >= 504 # define my_pushfail(L_) luaL_pushfail(L_) #else # define my_pushfail(L_) lua_pushnil(L_) #endif #define CANNOT_FAIL(Expr_) \ do { \ if ((Expr_) < 0) { \ perror("librunshell: " #Expr_ " failed"); \ abort(); \ } \ } while (0) #define CANNOT_FAIL_PTH(Expr_) \ do { \ if ((errno = (Expr_)) != 0) { \ perror("librunshell: " #Expr_ " failed"); \ abort(); \ } \ } while (0) static __thread char errbuf[512]; #if _POSIX_C_SOURCE < 200112L || defined(_GNU_SOURCE) # error "Unsupported feature test macros." #endif static __thread char errbuf[512]; static const char *my_strerror(int errnum) { // We introduce an /int/ variable in order to get a compilation warning if /strerror_r()/ is // still GNU-specific and returns a pointer to char. int r = strerror_r(errnum, errbuf, sizeof(errbuf)); return r == 0 ? errbuf : "unknown error or truncated error message"; } extern char **environ; int runshell(const char *cmd) { if (!cmd) { fputs("librunshell: passed cmd == NULL (this is not supported)\n", stderr); abort(); } sigset_t ss_new; sigset_t ss_old; CANNOT_FAIL(sigemptyset(&ss_new)); CANNOT_FAIL(sigaddset(&ss_new, SIGCHLD)); CANNOT_FAIL(sigprocmask(SIG_BLOCK, &ss_new, &ss_old)); posix_spawnattr_t attr; CANNOT_FAIL_PTH(posix_spawnattr_init(&attr)); CANNOT_FAIL_PTH(posix_spawnattr_setsigmask(&attr, &ss_old)); CANNOT_FAIL_PTH(posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK)); char *const argv[] = { (char *) "sh", (char *) "-c", (char *) cmd, NULL, }; pid_t pid; int rc = posix_spawn( /*pid=*/ &pid, /*path=*/ "/bin/sh", /*file_actions=*/ NULL, /*attrp=*/ &attr, /*argv=*/ argv, /*envp=*/ environ); CANNOT_FAIL_PTH(posix_spawnattr_destroy(&attr)); int ret; int saved_errno; if (rc == 0) { saved_errno = 0; while (waitpid(pid, &ret, 0) < 0) { if (errno != EINTR) { ret = -1; saved_errno = errno; break; } } } else { ret = -1; saved_errno = rc; } CANNOT_FAIL(sigprocmask(SIG_SETMASK, &ss_old, NULL)); errno = saved_errno; return ret; } int runshell_l_os_execute_lua51ver(lua_State *L) { const char *cmd = luaL_optstring(L, 1, NULL); // L: ? if (!cmd) { lua_pushinteger(L, 1); // L: ? 1 return 1; } lua_pushinteger(L, runshell(cmd)); // L: ? code return 1; } int runshell_l_os_execute(lua_State *L) { const char *cmd = luaL_optstring(L, 1, NULL); // L: ? if (!cmd) { lua_pushboolean(L, 1); // L: ? true return 1; } int rc = runshell(cmd); if (rc < 0) { int saved_errno = errno; my_pushfail(L); // L: ? fail lua_pushstring(L, my_strerror(saved_errno)); // L: ? fail err_msg lua_pushinteger(L, saved_errno); // L: ? fail err_msg errno return 3; } int code; bool normal_exit = true; if (WIFEXITED(rc)) { code = WEXITSTATUS(rc); } else if (WIFSIGNALED(rc)) { code = WTERMSIG(rc); normal_exit = false; } else { code = rc; } if (normal_exit && code == 0) { lua_pushboolean(L, 1); // L: ? true } else { my_pushfail(L); // L: ? fail } lua_pushstring(L, normal_exit ? "exit" : "signal"); // L: ? is_ok what lua_pushinteger(L, code); // L: ? is_ok what code return 3; } ================================================ FILE: librunshell/runshell.h ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef luastatus_runshell_h_ #define luastatus_runshell_h_ #include // This function is like /system()/, but: // // 1. Does not support /cmd == NULL/: on a POSIX system, /system(NULL)/ must // return non-zero anyway; // // 2. Assumes no thread cancellation takes place, and is not a cancellation // point (nobody is going to cancel luastatus' widget threads); // // 3. Does not modify signal dispositions for SIGINT and/or SIGQUIT (we are // not going to modify disposition for these signals anyway); // // 4. Is otherwise thread-safe (POSIX does not guarantee thread-safety of // /system()/, and, on musl, it is not thread-safe). int runshell(const char *cmd); int runshell_l_os_execute(lua_State *L); int runshell_l_os_execute_lua51ver(lua_State *L); #endif ================================================ FILE: libsafe/CMakeLists.txt ================================================ file (GLOB sources "*.c") add_library (safe OBJECT ${sources}) target_compile_definitions (safe PUBLIC -D_POSIX_C_SOURCE=200809L) set_target_properties (safe PROPERTIES POSITION_INDEPENDENT_CODE ON) ================================================ FILE: libsafe/mut_safev.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef libsafe_mut_safev_h_ #define libsafe_mut_safev_h_ #include "safe_common.h" #include #include "safev.h" typedef struct { char *mut_s__; size_t mut_n__; } MUT_SAFEV; LIBSAFE_INHEADER MUT_SAFEV MUT_SAFEV_new_empty(void) { return (MUT_SAFEV) {.mut_s__ = NULL, .mut_n__ = 0}; } LIBSAFE_INHEADER MUT_SAFEV MUT_SAFEV_new_UNSAFE(char *buf, size_t nbuf) { return (MUT_SAFEV) { .mut_s__ = buf, .mut_n__ = nbuf, }; } #define MUT_SAVEF_STATIC_INIT_UNSAFE(Ptr_, Len_) \ { \ .mut_s__ = (Ptr_), \ .mut_n__ = (Len_), \ } LIBSAFE_INHEADER void MUT_SAFEV_set_at(MUT_SAFEV Mv, size_t i, char c) { LIBSAFE_ASSERT(i < Mv.mut_n__); Mv.mut_s__[i] = c; } LIBSAFE_INHEADER SAFEV MUT_SAFEV_TO_SAFEV(MUT_SAFEV Mv) { return SAFEV_new_UNSAFE(Mv.mut_s__, Mv.mut_n__); } #endif ================================================ FILE: libsafe/safe_common.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "safe_common.h" #include #include void libsafe_assert_failed__( const char *expr, const char *func, const char *file, int line) { fprintf( stderr, "LIBSAFE_ASSERT(%s) failed in %s at %s:%d.\n", expr, func, file, line); abort(); } ================================================ FILE: libsafe/safe_common.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef libsafe_safe_common_h_ #define libsafe_safe_common_h_ #if __GNUC__ >= 2 # define LIBSAFE_ATTR_UNUSED __attribute__((unused)) # define LIBSAFE_ATTR_NORETURN __attribute__((noreturn)) #else # define LIBSAFE_ATTR_UNUSED /*nothing*/ # define LIBSAFE_ATTR_NORETURN /*nothing*/ #endif LIBSAFE_ATTR_NORETURN void libsafe_assert_failed__( const char *expr, const char *func, const char *file, int line); #define LIBSAFE_INHEADER static inline LIBSAFE_ATTR_UNUSED #define LIBSAFE_ASSERT(Expr_) \ do { \ if (!(Expr_)) { \ libsafe_assert_failed__(#Expr_, __func__, __FILE__, __LINE__); \ } \ } while (0) #endif ================================================ FILE: libsafe/safev.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef libsafe_safev_h_ #define libsafe_safev_h_ #include "safe_common.h" #include #include #include typedef struct { const char *s__; size_t n__; } SAFEV; // Constructs an empty view. LIBSAFE_INHEADER SAFEV SAFEV_new_empty(void) { return (SAFEV) {.s__ = NULL, .n__ = 0}; } // Constructs a view of a buffer by pointer and length. LIBSAFE_INHEADER SAFEV SAFEV_new_UNSAFE(const char *ptr, size_t n) { return (SAFEV) { .s__ = ptr, .n__ = n, }; } // Constructs a view of a C string. LIBSAFE_INHEADER SAFEV SAFEV_new_from_cstr_UNSAFE(const char *cstr) { LIBSAFE_ASSERT(cstr != NULL); return (SAFEV) { .s__ = cstr, .n__ = strlen(cstr), }; } #define SAFEV_new_from_literal(StrLit_) SAFEV_new_from_cstr_UNSAFE("" StrLit_) // A static initializer for SAFEV (from a pointer and length). #define SAFEV_STATIC_INIT_UNSAFE(Ptr_, Len_) {.s__ = (Ptr_), .n__ = (Len_)} // A static initializer for SAFEV (from a string literal). #define SAFEV_STATIC_INIT_FROM_LITERAL(StrLit_) \ { \ .s__ = "" StrLit_, \ .n__ = sizeof(StrLit_) - 1, \ } // Peeks at a byte of a view. LIBSAFE_INHEADER char SAFEV_at(SAFEV v, size_t i) { LIBSAFE_ASSERT(i < v.n__); return v.s__[i]; } // Peeks at a byte of a view, or, if index is invalid, return 'alt'. LIBSAFE_INHEADER char SAFEV_at_or(SAFEV v, size_t i, char alt) { if (i >= v.n__) { return alt; } return v.s__[i]; } // Searches a view for a first occurence of character 'c'. // If found, returns the index. // If not found, returns (size_t) -1. LIBSAFE_INHEADER size_t SAFEV_index_of(SAFEV v, unsigned char c) { const char *pos = v.n__ ? memchr(v.s__, c, v.n__) : NULL; return pos ? (size_t) (pos - v.s__) : (size_t) -1; } // Checks if a view 'v' starts with a view 'prefix'. LIBSAFE_INHEADER bool SAFEV_starts_with(SAFEV v, SAFEV prefix) { if (prefix.n__ > v.n__) { return false; } if (!prefix.n__) { return true; } return memcmp(prefix.s__, v.s__, prefix.n__) == 0; } // Checks if a view 'v' equals to a view 'v1'. LIBSAFE_INHEADER bool SAFEV_equals(SAFEV v, SAFEV v1) { if (v.n__ != v1.n__) { return false; } if (!v.n__) { return true; } return memcmp(v.s__, v1.s__, v.n__) == 0; } // Constructs a subview of a view; 'i' is the starting index (inclusive), 'j' is the // ending index (exclusive). LIBSAFE_INHEADER SAFEV SAFEV_subspan(SAFEV v, size_t i, size_t j) { LIBSAFE_ASSERT(i <= j); LIBSAFE_ASSERT(j <= v.n__); return (SAFEV) { .s__ = v.s__ + i, .n__ = j - i, }; } // Equivalent to 'SAFEV_subspan(v, from_idx, SAFEV_len(v))'. LIBSAFE_INHEADER SAFEV SAFEV_suffix(SAFEV v, size_t from_idx) { LIBSAFE_ASSERT(from_idx <= v.n__); return (SAFEV) { .s__ = v.s__ + from_idx, .n__ = v.n__ - from_idx, }; } // Returns the pointer of a view. LIBSAFE_INHEADER const char *SAFEV_ptr_UNSAFE(SAFEV v) { return v.s__; } // Returns the length of a view. LIBSAFE_INHEADER size_t SAFEV_len(SAFEV v) { return v.n__; } // If 'v' ends with 'c', strips it off; otherwise, returns it unchanged. LIBSAFE_INHEADER SAFEV SAFEV_rstrip_once(SAFEV v, char c) { if (!v.n__) { return v; } if (SAFEV_at(v, v.n__ - 1) != c) { return v; } return SAFEV_subspan(v, 0, v.n__ - 1); } // Returns min(i, SAFEV_len(v)). LIBSAFE_INHEADER size_t SAFEV_trim_to_len(SAFEV v, size_t i) { if (i > v.n__) { i = v.n__; } return i; } // This is needed from 'SAFEV_FMT_ARG': returns // min(bound, SAFEV_len(v)), // assuming bound >= 0. LIBSAFE_INHEADER int SAFEV_bounded_len(SAFEV v, int bound) { LIBSAFE_ASSERT(bound >= 0); size_t res = v.n__; if (res > (unsigned) bound) { res = bound; } return res; } // Print a SAFEV as // // void print_sv(SAFEV v) // { // printf("v = '%.*s'\n", SAFEV_FMT_ARG(v, 1024)); // } #define SAFEV_FMT_ARG(V_, MaxLen_) \ SAFEV_bounded_len((V_), (MaxLen_)), \ (V_).s__ #endif ================================================ FILE: libwidechar/CMakeLists.txt ================================================ file (GLOB sources "*.c") add_library (widechar OBJECT ${sources}) include (CheckSymbolExists) set (CMAKE_REQUIRED_DEFINITIONS "-D_XOPEN_SOURCE=500") check_symbol_exists (wcwidth "wchar.h" LUASTATUS_HAVE_WCWIDTH) configure_file ("config.in.h" "config.generated.h") target_compile_definitions (widechar PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (widechar LUA) find_library (MATH_LIBRARY m) if (MATH_LIBRARY) target_link_libraries (widechar PUBLIC ${MATH_LIBRARY}) endif () set_target_properties (widechar PROPERTIES POSITION_INDEPENDENT_CODE ON) target_include_directories (widechar PUBLIC "${PROJECT_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}") ================================================ FILE: libwidechar/README.txt ================================================ Implementation of luastatus.libwidechar module. It contains functions working on wchar_t strings to: 1. Determine the width of a string; 2. Truncate a string to a given width; 3. Convert a byte string into a valid string of printable wchar_t's, replacing illegal sequences or unprintable characters with a given placeholder. ================================================ FILE: libwidechar/config.in.h ================================================ #ifndef luastatus_libwidechar_config_h_ #define luastatus_libwidechar_config_h_ #cmakedefine01 LUASTATUS_HAVE_WCWIDTH #endif ================================================ FILE: libwidechar/libwidechar.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #define _XOPEN_SOURCE 500 #include "libwidechar.h" #include #include #include #include #include #include #include #include #include "libwidechar_xspan.h" #include "config.generated.h" #if LUASTATUS_HAVE_WCWIDTH static inline int my_wcwidth(wchar_t c) { return wcwidth(c); } enum { IS_DUMMY_IMPLEMENTATION = 0 }; #else static inline int my_wcwidth(wchar_t c) { return c == L'\0' ? 0 : 1; } enum { IS_DUMMY_IMPLEMENTATION = 1 }; #endif #if defined(__NetBSD__) #define uselocale(x) ((locale_t) 0) #endif static inline bool next(mbstate_t *state, xspan x, xspan *new_x, wchar_t *out) { SAFEV unproc_v = xspan_unprocessed_v(x); size_t rc = mbrtowc(out, SAFEV_ptr_UNSAFE(unproc_v), SAFEV_len(unproc_v), state); if ((rc == (size_t) -1) || (rc == (size_t) -2)) { return false; } size_t n = rc == 0 ? 1 : rc; *new_x = x; xspan_advance(new_x, n); return true; } int libwidechar_width(SAFEV v, uint64_t *out_width) { xspan x = v_to_xspan(v); uint64_t width = 0; mbstate_t state = {0}; while (xspan_nonempty(x)) { wchar_t c; if (!next(&state, x, &x, &c)) { return -1; } int w = my_wcwidth(c); if (w < 0 || c == L'\0') { return -1; } width += w; } *out_width = width; return 0; } size_t libwidechar_truncate_to_width( SAFEV v, uint64_t max_width, uint64_t *out_resut_width) { xspan x = v_to_xspan(v); mbstate_t state = {0}; uint64_t width = 0; while (xspan_nonempty(x)) { xspan new_x; wchar_t c; if (!next(&state, x, &new_x, &c)) { return -1; } int w = my_wcwidth(c); if (w < 0 || c == L'\0') { return -1; } width += w; if (width > max_width) { width -= w; break; } x = new_x; } *out_resut_width = width; return xspan_processed_len(x); } void libwidechar_make_valid_and_printable( SAFEV v, SAFEV bad, void (*append)(void *ud, SAFEV segment), void *append_ud) { xspan x = v_to_xspan(v); mbstate_t state = {0}; while (xspan_nonempty(x)) { xspan new_x; wchar_t c; if (next(&state, x, &new_x, &c)) { if (my_wcwidth(c) >= 0 && c != L'\0') { // this wchar_t is good x = new_x; } else { // valid, but non-printable append(append_ud, xspan_processed_v(x)); append(append_ud, bad); x = xspan_skip_processed(new_x); } } else { // invalid append(append_ud, xspan_processed_v(x)); append(append_ud, bad); xspan_advance(&x, 1); x = xspan_skip_processed(x); } } append(append_ud, xspan_processed_v(x)); } typedef struct { locale_t native; locale_t old; } LocaleSavedData; static inline LocaleSavedData begin_locale(lua_State *L) { LocaleSavedData lsd = {0}; const char *err_msg; lsd.native = newlocale(LC_ALL_MASK, "", (locale_t) 0); if (lsd.native == (locale_t) 0) { err_msg = "begin_locale: newlocale() failed"; goto fail; } lsd.old = uselocale(lsd.native); if (lsd.old == (locale_t) 0) { err_msg = "begin_locale: uselocale() failed"; goto fail; } return lsd; fail: if (lsd.native != (locale_t) 0) { freelocale(lsd.native); } luaL_error(L, "%s", err_msg); // unreachable return (LocaleSavedData) {0}; } static inline void end_locale(LocaleSavedData lsd, lua_State *L) { if (uselocale(lsd.old) == (locale_t) 0) { luaL_error(L, "end_locale: uselocale() failed"); } freelocale(lsd.native); } static inline SAFEV v_from_lua_string(lua_State *L, int pos) { size_t ns; const char *s = lua_tolstring(L, pos, &ns); return SAFEV_new_UNSAFE(s, ns); } static inline uint64_t nonneg_double_to_u64(double d) { if (d >= 18446744073709551616.0) { return -1; } return d; } static inline size_t nonneg_double_to_size_t(double d) { static const double LIMIT = #if SIZE_MAX > UINT32_MAX 9223372036854775808.0 // 2^63 #else SIZE_MAX #endif ; if (d > LIMIT) { return -1; } return d; } static size_t extract_ij(lua_State *L, int pos, size_t if_absent) { double d = luaL_optnumber(L, pos, -1); if (isgreaterequal(d, 0.0)) { return nonneg_double_to_size_t(d); } return if_absent; } static SAFEV extract_string_with_ij(lua_State *L, int str_pos, int i_pos) { SAFEV v = v_from_lua_string(L, str_pos); size_t i = extract_ij(L, i_pos, 1); size_t j = extract_ij(L, i_pos + 1, SIZE_MAX); if (!i) { luaL_argerror(L, i_pos, "is zero (expected 1-based index)"); // unreachable return (SAFEV) {0}; } // convert one-based index into zero-based --i; // 'j' doesn't need to be converted because in Lua function, it is *inclusive* // index, but is instead used as an *exclusive* in the code below. i = SAFEV_trim_to_len(v, i); j = SAFEV_trim_to_len(v, j); if (j < i) { j = i; } return SAFEV_subspan(v, i, j); } static int lfunc_width(lua_State *L) { SAFEV v = extract_string_with_ij(L, 1, 2); LocaleSavedData lsd = begin_locale(L); uint64_t width; int rc = libwidechar_width(v, &width); end_locale(lsd, L); // L: ? if (rc < 0) { lua_pushnil(L); // L: ? nil } else { lua_pushnumber(L, width); // ? L: width } return 1; } static int lfunc_truncate_to_width(lua_State *L) { SAFEV v = extract_string_with_ij(L, 1, 3); double d_max_width = luaL_checknumber(L, 2); if (!isgreaterequal(d_max_width, 0.0)) { return luaL_argerror(L, 2, "negative or NaN"); } uint64_t max_width = nonneg_double_to_u64(d_max_width); LocaleSavedData lsd = begin_locale(L); uint64_t res_width; size_t res_len = libwidechar_truncate_to_width(v, max_width, &res_width); end_locale(lsd, L); if (res_len == (size_t) -1) { lua_pushnil(L); // L: nil lua_pushnil(L); // L: nil nil } else { SAFEV res = SAFEV_subspan(v, 0, res_len); lua_pushlstring(L, SAFEV_ptr_UNSAFE(res), SAFEV_len(res)); // L: result lua_pushnumber(L, res_width); // L: result result_width } return 2; } static void append_to_lua_buf_callback(void *ud, SAFEV segment) { luaL_Buffer *b = ud; luaL_addlstring(b, SAFEV_ptr_UNSAFE(segment), SAFEV_len(segment)); } static int lfunc_make_valid_and_printable(lua_State *L) { SAFEV v = extract_string_with_ij(L, 1, 3); SAFEV bad = v_from_lua_string(L, 2); luaL_Buffer b; luaL_buffinit(L, &b); LocaleSavedData lsd = begin_locale(L); libwidechar_make_valid_and_printable(v, bad, append_to_lua_buf_callback, &b); end_locale(lsd, L); luaL_pushresult(&b); // L: result return 1; } static int lfunc_is_dummy_implementation(lua_State *L) { lua_pushboolean(L, IS_DUMMY_IMPLEMENTATION); // L: bool return 1; } void libwidechar_register_lua_funcs(lua_State *L) { (void) L; #if ! defined(__NetBSD__) // L: table lua_pushcfunction(L, lfunc_width); // L: table func lua_setfield(L, -2, "width"); // L: table lua_pushcfunction(L, lfunc_truncate_to_width); // L: table func lua_setfield(L, -2, "truncate_to_width"); // L: table lua_pushcfunction(L, lfunc_make_valid_and_printable); // L: table func lua_setfield(L, -2, "make_valid_and_printable"); // L: table lua_pushcfunction(L, lfunc_is_dummy_implementation); // L: table func lua_setfield(L, -2, "is_dummy_implementation"); // L: table #endif } ================================================ FILE: libwidechar/libwidechar.h ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef libwidechar_h_ #define libwidechar_h_ #include #include #include #include "libsafe/safev.h" int libwidechar_width(SAFEV v, uint64_t *out_width); size_t libwidechar_truncate_to_width( SAFEV v, uint64_t max_width, uint64_t *out_resut_width); void libwidechar_make_valid_and_printable( SAFEV v, SAFEV bad, void (*append)(void *ud, SAFEV segment), void *append_ud); void libwidechar_register_lua_funcs(lua_State *L); #endif ================================================ FILE: libwidechar/libwidechar_compdep.h ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef libwidechar_compdep_h_ #define libwidechar_compdep_h_ #if __GNUC__ >= 2 # define LIBWIDECHAR_UNUSED __attribute__((unused)) #else # define LIBWIDECHAR_UNUSED /*nothing*/ #endif #define LIBWIDECHAR_INHEADER static inline LIBWIDECHAR_UNUSED #endif ================================================ FILE: libwidechar/libwidechar_xspan.h ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef libwidechar_xspan_h_ #define libwidechar_xspan_h_ #include #include #include "libsafe/safev.h" #include "libwidechar_compdep.h" typedef struct { SAFEV v; size_t cur; } xspan; LIBWIDECHAR_INHEADER xspan v_to_xspan(SAFEV v) { return (xspan) { .v = v, .cur = 0, }; } LIBWIDECHAR_INHEADER bool xspan_nonempty(xspan x) { return x.cur != SAFEV_len(x.v); } LIBWIDECHAR_INHEADER void xspan_advance(xspan *x, size_t n) { x->cur += n; } LIBWIDECHAR_INHEADER size_t xspan_processed_len(xspan x) { return x.cur; } LIBWIDECHAR_INHEADER SAFEV xspan_processed_v(xspan x) { return SAFEV_subspan(x.v, 0, x.cur); } LIBWIDECHAR_INHEADER SAFEV xspan_unprocessed_v(xspan x) { return SAFEV_suffix(x.v, x.cur); } LIBWIDECHAR_INHEADER xspan xspan_skip_processed(xspan x) { return (xspan) { .v = xspan_unprocessed_v(x), .cur = 0, }; } #endif ================================================ FILE: plugins/alsa/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_plugin (plugin-alsa $ $ ${sources}) target_compile_definitions (plugin-alsa PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-alsa LUA) target_include_directories (plugin-alsa PUBLIC "${PROJECT_SOURCE_DIR}") find_package (PkgConfig REQUIRED) pkg_check_modules (ALSA REQUIRED alsa) luastatus_target_build_with (plugin-alsa ALSA) luastatus_add_man_page (README.rst luastatus-plugin-alsa 7) ================================================ FILE: plugins/alsa/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-alsa .. :X-man-page-only: ##################### .. :X-man-page-only: .. :X-man-page-only: ######################### .. :X-man-page-only: ALSA plugin for luastatus .. :X-man-page-only: ######################### .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This plugin monitors volume and mute state of an ALSA channel. Options ======== The following options are supported: * ``card``: string Card name, defaults to ``"default"``. * ``channel``: string Channel name, defaults to ``"Master"``. * ``in_db``: boolean Whether or not to report normalized volume (in dBs). * ``capture``: boolean Whether or not this is a capture stream, as opposed to a playback one. Defaults to false. * ``timeout``: number If specified and not negative, this plugin will call ``cb`` with ``nil`` argument whenever the channel does not change its state in ``timeout`` seconds since the previous call to ``cb``. * ``make_self_pipe``: boolean If true, the ``wake_up()`` (see the `Functions`_ section) function will be available. Defaults to false. ``cb`` argument =============== On timeout, ``nil`` (if the ``timeout`` option has been specified). Otherwise, the argument is a table with the following entries: * ``mute``: boolean Whether or not the playback switch is turned off. (Only provided if the channel has the playback switch capability.) * ``vol``: table with the following entries: * ``cur``, ``min``, ``max``: numbers Current, minimal and maximal volume levels, correspondingly. (Only provided if the channel has the volume control capability.) Functions ========= The following functions are provided: * ``luastatus.plugin.wake_up()`` Forces a call to ``cb``. Only available if the ``make_self_pipe`` option was set to ``true``; otherwise, it throws an error. ================================================ FILE: plugins/alsa/alsa.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "libmoonvisit/moonvisit.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_evloop_lfuncs.h" #include "libls/ls_time_utils.h" #include "libls/ls_io_utils.h" typedef struct { char *card; char *channel; bool capture; bool in_db; double tmo; int pipefds[2]; } Priv; static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; free(p->card); free(p->channel); ls_close(p->pipefds[0]); ls_close(p->pipefds[1]); free(p); } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .card = NULL, .channel = NULL, .capture = false, .in_db = false, .tmo = -1, .pipefds = {-1, -1}, }; char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Parse card if (moon_visit_str(&mv, -1, "card", &p->card, NULL, true) < 0) goto mverror; if (!p->card) p->card = ls_xstrdup("default"); // Parse channel if (moon_visit_str(&mv, -1, "channel", &p->channel, NULL, true) < 0) goto mverror; if (!p->channel) p->channel = ls_xstrdup("Master"); // Parse capture if (moon_visit_bool(&mv, -1, "capture", &p->capture, true) < 0) goto mverror; // Parse in_db if (moon_visit_bool(&mv, -1, "in_db", &p->in_db, true) < 0) goto mverror; // Parse timeout if (moon_visit_num(&mv, -1, "timeout", &p->tmo, true) < 0) goto mverror; // Parse make_self_pipe bool mkpipe = false; if (moon_visit_bool(&mv, -1, "make_self_pipe", &mkpipe, true) < 0) goto mverror; if (mkpipe) { if (ls_self_pipe_open(p->pipefds) < 0) { LS_FATALF(pd, "ls_self_pipe_open: %s", ls_tls_strerror(errno)); goto error; } } return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); error: destroy(pd); return LUASTATUS_ERR; } static void register_funcs(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv; // L: table ls_self_pipe_push_luafunc(p->pipefds, L); // L: table func lua_setfield(L, -2, "wake_up"); // L: table } static bool card_has_nicename( const char *realname, snd_ctl_card_info_t *info, const char *nicename) { snd_ctl_t *ctl; if (snd_ctl_open(&ctl, realname, 0) < 0) return false; bool r = false; if (snd_ctl_card_info(ctl, info) >= 0) { const char *name = snd_ctl_card_info_get_name(info); r = (strcmp(name, nicename) == 0); } snd_ctl_close(ctl); return r; } static char *xalloc_card_realname(const char *nicename) { snd_ctl_card_info_t *info; if (snd_ctl_card_info_malloc(&info) < 0) ls_oom(); enum { NBUF = 32 }; char *buf = LS_XNEW(char, NBUF); int rcard = -1; while (snd_card_next(&rcard) >= 0 && rcard >= 0) { snprintf(buf, NBUF, "hw:%d", rcard); if (card_has_nicename(buf, info, nicename)) goto cleanup; } free(buf); buf = NULL; cleanup: snd_ctl_card_info_free(info); return buf; } typedef struct { int (*get_range)(snd_mixer_elem_t *, long *, long *); int (*get_cur)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long *); int (*get_switch)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, int *); } GetVolFuncs; static GetVolFuncs select_gv_funcs(bool capture, bool in_db) { return (GetVolFuncs) { .get_range = capture ? (in_db ? snd_mixer_selem_get_capture_dB_range : snd_mixer_selem_get_capture_volume_range) : (in_db ? snd_mixer_selem_get_playback_dB_range : snd_mixer_selem_get_playback_volume_range), .get_cur = capture ? (in_db ? snd_mixer_selem_get_capture_dB : snd_mixer_selem_get_capture_volume) : (in_db ? snd_mixer_selem_get_playback_dB : snd_mixer_selem_get_playback_volume), .get_switch = capture ? snd_mixer_selem_get_capture_switch : snd_mixer_selem_get_playback_switch, }; } static void push_vol_info(lua_State *L, snd_mixer_elem_t *elem, GetVolFuncs gv_funcs) { lua_createtable(L, 0, 2); // L: table lua_createtable(L, 0, 3); // L: table table long pmin, pmax; if (gv_funcs.get_range(elem, &pmin, &pmax) >= 0) { lua_pushnumber(L, pmin); // L: table table pmin lua_setfield(L, -2, "min"); // L: table table lua_pushnumber(L, pmax); // L: table table pmax lua_setfield(L, -2, "max"); // L: table table } long pcur; if (gv_funcs.get_cur(elem, 0, &pcur) >= 0) { lua_pushnumber(L, pcur); // L: table table pcur lua_setfield(L, -2, "cur"); // L: table table } lua_setfield(L, -2, "vol"); // L: table int notmute; if (gv_funcs.get_switch(elem, 0, ¬mute) >= 0) { lua_pushboolean(L, !notmute); // L: table !notmute lua_setfield(L, -2, "mute"); // L: table } // L: table } typedef struct { struct pollfd *data; size_t size; size_t nprefix; } PollFdSet; static inline PollFdSet pollfd_set_new(struct pollfd prefix) { if (prefix.fd >= 0) { return (PollFdSet) { .data = LS_M_XMEMDUP(&prefix, 1), .size = 1, .nprefix = 1, }; } else { return (PollFdSet) { .data = NULL, .size = 0, .nprefix = 0, }; } } static inline void pollfd_set_resize(PollFdSet *s, size_t n) { if (s->size != n) { s->data = LS_M_XREALLOC(s->data, n); s->size = n; } } static inline void pollfd_set_free(PollFdSet s) { free(s.data); } static bool iteration(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { Priv *p = pd->priv; bool ret = false; snd_mixer_t *mixer = NULL; snd_mixer_selem_id_t *sid = NULL; char *realname = NULL; PollFdSet pollfds = pollfd_set_new((struct pollfd) { .fd = p->pipefds[0], .events = POLLIN, }); if (!(realname = xalloc_card_realname(p->card))) { realname = ls_xstrdup(p->card); } int r; if ((r = snd_mixer_open(&mixer, 0)) < 0) { LS_FATALF(pd, "snd_mixer_open: %s", snd_strerror(r)); goto error; } if ((r = snd_mixer_attach(mixer, realname)) < 0) { LS_FATALF(pd, "snd_mixer_attach: %s", snd_strerror(r)); goto error; } if ((r = snd_mixer_selem_register(mixer, NULL, NULL)) < 0) { LS_FATALF(pd, "snd_mixer_selem_register: %s", snd_strerror(r)); goto error; } if ((r = snd_mixer_load(mixer)) < 0) { LS_FATALF(pd, "snd_mixer_load: %s", snd_strerror(r)); goto error; } if ((r = snd_mixer_selem_id_malloc(&sid)) < 0) { LS_FATALF(pd, "snd_mixer_selem_id_malloc: %s", snd_strerror(r)); goto error; } snd_mixer_selem_id_set_name(sid, p->channel); snd_mixer_elem_t *elem = snd_mixer_find_selem(mixer, sid); if (!elem) { LS_FATALF(pd, "cannot find channel '%s'", p->channel); goto error; } ret = true; LS_TimeDelta tmo = ls_double_to_TD(p->tmo, LS_TD_FOREVER); GetVolFuncs gv_funcs = select_gv_funcs(p->capture, p->in_db); bool is_timeout = false; while (1) { lua_State *L = funcs.call_begin(pd->userdata); if (is_timeout) { lua_pushnil(L); // L: nil } else { push_vol_info(L, elem, gv_funcs); // L: table } funcs.call_end(pd->userdata); int nextrafds = snd_mixer_poll_descriptors_count(mixer); pollfd_set_resize(&pollfds, pollfds.nprefix + nextrafds); if ((r = snd_mixer_poll_descriptors( mixer, pollfds.data + pollfds.nprefix, pollfds.size - pollfds.nprefix)) < 0) { LS_FATALF(pd, "snd_mixer_poll_descriptors: %s", snd_strerror(r)); goto error; } int nfds = ls_poll(pollfds.data, pollfds.size, tmo); if (nfds < 0) { LS_FATALF(pd, "poll: %s", ls_tls_strerror(errno)); goto error; } else if (nfds == 0) { is_timeout = true; } else { is_timeout = false; if (pollfds.nprefix && (pollfds.data[0].revents & POLLIN)) { char c; ssize_t unused = read(p->pipefds[0], &c, 1); (void) unused; } } unsigned short revents; if ((r = snd_mixer_poll_descriptors_revents( mixer, pollfds.data + pollfds.nprefix, pollfds.size - pollfds.nprefix, &revents)) < 0) { LS_FATALF(pd, "snd_mixer_poll_descriptors_revents: %s", snd_strerror(r)); goto error; } if (revents & (POLLERR | POLLNVAL)) { LS_FATALF(pd, "snd_mixer_poll_descriptors_revents() reported error condition"); goto error; } if (revents & POLLIN) { if ((r = snd_mixer_handle_events(mixer)) < 0) { LS_FATALF(pd, "snd_mixer_handle_events: %s", snd_strerror(r)); goto error; } } } error: if (sid) { snd_mixer_selem_id_free(sid); } if (mixer) { snd_mixer_close(mixer); } free(realname); pollfd_set_free(pollfds); return ret; } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { for (;;) { if (!iteration(pd, funcs)) { ls_sleep_simple(5.0); } } } LuastatusPluginIface luastatus_plugin_iface_v1 = { .init = init, .register_funcs = register_funcs, .run = run, .destroy = destroy, }; ================================================ FILE: plugins/backlight-linux/CMakeLists.txt ================================================ install (FILES backlight-linux.lua DESTINATION ${LUA_PLUGINS_DIR}) luastatus_add_man_page (README.rst luastatus-plugin-backlight-linux 7) ================================================ FILE: plugins/backlight-linux/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-backlight-linux .. :X-man-page-only: ################################ .. :X-man-page-only: .. :X-man-page-only: ############################################# .. :X-man-page-only: Linux-specific backlight plugin for luastatus .. :X-man-page-only: ############################################# .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This derived plugin shows the display backlight level when it changes. Functions ========= The following functions are provided: * ``widget(tbl)`` Constructs a ``widget`` table required by luastatus. ``tbl`` is a table with the following fields: **(required)** - ``cb``: function The callback that will be called with a display backlight level (a number from 0 to 1), or with ``nil``. **(optional)** - ``syspath``: string Path to the device directory, e.g.:: /sys/devices/pci0000:00/0000:00:02.0/drm/card0/card0-eDP-1/intel_backlight - ``timeout``: number Backlight information is hidden after the timeout in seconds (default is 2). - ``event`` The ``event`` entry of the resulting table (see ``luastatus`` documentation for the description of ``widget.event`` field). ================================================ FILE: plugins/backlight-linux/backlight-linux.lua ================================================ --[[ Copyright (C) 2015-2025 luastatus developers This file is part of luastatus. luastatus is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. luastatus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with luastatus. If not, see . --]] local P = {} local function read_num(path) local f = assert(io.open(path, 'r')) local r = f:read('*number') f:close() return r end function P.widget(tbl) local timeout = tbl.timeout or 2 return { plugin = 'udev', opts = { subsystem = 'backlight', }, cb = function(t) if t.what ~= 'event' then return tbl.cb(nil) end local s = t.syspath if tbl.syspath and tbl.syspath ~= s then return tbl.cb(nil) end luastatus.plugin.push_timeout(timeout) local b = read_num(s .. '/brightness') local mb = read_num(s .. '/max_brightness') if (not b) or (not mb) then return tbl.cb(nil) end return tbl.cb(b / mb) end, event = tbl.event, } end return P ================================================ FILE: plugins/battery-linux/CMakeLists.txt ================================================ install (FILES battery-linux.lua DESTINATION ${LUA_PLUGINS_DIR}) luastatus_add_man_page (README.rst luastatus-plugin-battery-linux 7) ================================================ FILE: plugins/battery-linux/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-battery-linux .. :X-man-page-only: ############################## .. :X-man-page-only: .. :X-man-page-only: ########################################### .. :X-man-page-only: Linux-specific battery plugin for luastatus .. :X-man-page-only: ########################################### .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This derived plugin periodically polls Linux ``sysfs`` for the state of a battery. It is also able to estimate the time remaining to full charge/discharge and the current battery consumption rate. Functions ========= The following functions are provided: * ``widget(tbl)`` Constructs a ``widget`` table required by luastatus. ``tbl`` is a table with the following fields: **(required)** - ``cb``: function The callback that will be called with a table with the following keys: + ``status``: string A string with battery status text, e.g. ``"Full"``, ``"Unknown"``, ``"Charging"``, ``"Discharging"``. Not present if the status cannot be read (for example, when the battery is missing). + ``capacity``: number A percentage representing battery capacity. Not present if the capacity cannot be read. + ``rem_time``: number Time (in hours) remaining to full charge/discharge. Usually only present on battery charge/discharge. + ``consumption``: number The current battery consumption/charge rate in watts. Usually only present on battery charge/discharge. **(optional)** - ``dev``: string Device directory name under ``/sys/class/power_supply``; default is ``"BAT0"``. - ``period``: number The period in seconds for polling ``sysfs``; default is 2 seconds. Because this plugin utilizes ``udev`` under the hood, it is able to respond to battery state changes (such as cable (un-)plugging) immediately, irrespective of the value of the period. - ``use_energy_full_design``: boolean If ``true``, the ``energy_full_design`` property (not ``energy_full``) will be used for capacity calculation. - ``event`` The ``event`` entry of the resulting table (see ``luastatus`` documentation for the description of ``widget.event`` field). ================================================ FILE: plugins/battery-linux/battery-linux.lua ================================================ --[[ Copyright (C) 2015-2025 luastatus developers This file is part of luastatus. luastatus is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. luastatus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with luastatus. If not, see . --]] local P = {} local function read_uevent(devpath) local f = io.open(devpath .. '/uevent', 'r') if not f then return nil end local r = {} for line in f:lines() do local key, value = line:match('POWER_SUPPLY_(.-)=(.*)') if key then r[key:lower()] = value end end f:close() return r end local function get_battery_info(devpath, use_energy_full_design) local p = read_uevent(devpath) if not p then return {} end -- Convert amperes to watts. if p.charge_full then p.energy_full = p.charge_full * p.voltage_now / 1e6 p.energy_now = p.charge_now * p.voltage_now / 1e6 -- Some drivers don't report power_now if p.current_now ~= nil then p.power_now = p.current_now * p.voltage_now / 1e6 end end local ef = use_energy_full_design and p.energy_full_design or p.energy_full -- A buggy driver can report energy_now as energy_full_design, which -- will lead to an overshoot in capacity. local capacity = math.min(math.floor(p.energy_now / ef * 100 + 0.5), 100) local r = {status = p.status, capacity = capacity} local pn = tonumber(p.power_now) if pn ~= nil and pn ~= 0 then pn = math.abs(pn) r.consumption = pn / 1e6 if p.status == 'Charging' then r.rem_time = (p.energy_full - p.energy_now) / pn elseif p.status == 'Discharging' or p.status == 'Not charging' then r.rem_time = p.energy_now / pn end end return r end function P.widget(tbl) local devpath if tbl._devpath then devpath = tbl._devpath else local dev = tbl.dev or 'BAT0' devpath = '/sys/class/power_supply/' .. dev end local period = tbl.period or 2 return { plugin = 'udev', opts = { subsystem = 'power_supply', timeout = period, greet = true }, cb = function() return tbl.cb(get_battery_info(devpath, tbl.use_energy_full_design)) end, event = tbl.event, } end return P ================================================ FILE: plugins/cpu-freq-linux/CMakeLists.txt ================================================ install (FILES cpu-freq-linux.lua DESTINATION ${LUA_PLUGINS_DIR}) luastatus_add_man_page (README.rst luastatus-plugin-cpu-freq-linux 7) ================================================ FILE: plugins/cpu-freq-linux/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-cpu-freq-linux .. :X-man-page-only: ############################### .. :X-man-page-only: .. :X-man-page-only: ################################################# .. :X-man-page-only: Linux-specific CPU frequency plugin for luastatus .. :X-man-page-only: ################################################# .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This derived plugin periodically polls Linux ``sysfs`` for the current frequency of each of the CPUs. Functions ========= The following functions are provided: * ``get_freqs(data)`` Returns either an array or nil. If the result is nil, no data is currently available (most likely reason for this is that the set of plugged-in CPUs has changed; if so, the subsequent calls will return non-nil). Each array entry corresponds to a subsequent CPU; it is a table with the following fields: * ``min`` (number): minimal frequency of this CPU; * ``max`` (number): maximal frequency of this CPU; * ``cur`` (number): current frequency of this CPU. All values are in KHz. In order to use this function, you are expected to maintain a table ``data``, initially empty, and pass it to ``get_freqs`` each time. The only thing the caller is allowed to do with this table is to set ``please_reload`` field to ``true``. If ``please_reload`` field is true when this function is called, the function will forcefully reload all information and reset ``please_reload`` field to ``nil``. The array is sorted by CPU index. * ``widget(tbl[, data])`` Constructs a ``widget`` table required by luastatus. If ``data`` is specified, it must be an empty table; you can set ``please_reload`` field in this table to force a full reload. On each reported event, before a call to ``tbl.cb``, this field is reset to ``nil``. ``tbl`` is a table with the following fields: **(required)** - ``cb``: a function The callback function that will be called with current CPU frequencies, or with ``nil``. The argument is the same as the return value of ``get_freqs`` (see above for description of ``get_freqs`` function for more information). **(optional)** - ``timer_opts``: table Options for the underlying ``timer`` plugin. - ``event`` The ``event`` entry of the resulting table (see ``luastatus`` documentation for the description of ``widget.event`` field). ================================================ FILE: plugins/cpu-freq-linux/cpu-freq-linux.lua ================================================ --[[ Copyright (C) 2025 luastatus developers This file is part of luastatus. luastatus is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. luastatus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with luastatus. If not, see . --]] -- Sorts an array of strings by the first digit group. local function numeric_sort(tbl) local function extract_first_number(s) return tonumber(s:match('[0-9]+') or '-1') end table.sort(tbl, function(a, b) return extract_first_number(a) < extract_first_number(b) end) end -- Escapes given cpu_dir for POSIX shell. local function escape_cpu_dir(cpu_dir) if cpu_dir:find('\0') then error('_cpu_dir contains NUL character') end local res, _ = cpu_dir:gsub([[']], [['\'']]) return string.format([['%s']], res) end -- Fetches list of all CPU paths and assigns to 'data.paths'. local function data_fetch_paths(data) local paths = {} local cpu_dir local cpu_dir_escaped if data._cpu_dir then cpu_dir = data._cpu_dir cpu_dir_escaped = escape_cpu_dir(cpu_dir) else cpu_dir = '/sys/devices/system/cpu' cpu_dir_escaped = cpu_dir end local f = assert(io.popen([[ cd ]] .. cpu_dir_escaped .. [[ || exit $? for x in cpu[0-9]*/; do [ -d "$x" ] || break printf '%s\n' "$x" done ]])) for p in f:lines() do table.insert(paths, string.format('%s/%s', cpu_dir, p)) end f:close() numeric_sort(paths) data.paths = paths end -- Reads a number from "${path}/${suffix}" for each CPU path ${path} in -- array 'data.paths'. -- Returns an array of numbers on success, nil on failure. local function data_read_from_all_paths(data, suffix) assert(data.paths) local r = {} for _, path in ipairs(data.paths) do local f = io.open(path .. suffix, 'r') if not f then return nil end local val = f:read('*number') f:close() if not val then return nil end assert(val > 0, 'reported frequency is zero or negative') r[#r + 1] = val end return r end -- Returns a boolean indicating whether all required fields ('data.paths', -- 'data.max_freqs', 'data.min_freqs') are available. local function data_is_ready(data) return (data.paths and data.max_freqs and data.min_freqs) ~= nil end -- Tries to load information and set all required fields ('data.paths', -- 'data.max_freqs', 'data.min_freqs'). -- Returns true on success, false on failure. local function data_reload_all(data) data.paths = nil data.max_freqs = nil data.min_freqs = nil data_fetch_paths(data) local my_max_freqs = data_read_from_all_paths(data, '/cpufreq/cpuinfo_max_freq') local my_min_freqs = data_read_from_all_paths(data, '/cpufreq/cpuinfo_min_freq') if (not my_max_freqs) or (not my_min_freqs) then return false end for i = 1, #data.paths do if my_max_freqs[i] < my_min_freqs[i] then return false end end data.max_freqs, data.min_freqs = my_max_freqs, my_min_freqs return true end local P = {} function P.get_freqs(data) if (data.please_reload) or (not data_is_ready(data)) then if not data_reload_all(data) then return nil end data.please_reload = nil end local cur_freqs = data_read_from_all_paths(data, '/cpufreq/scaling_cur_freq') if not cur_freqs then data_reload_all(data) return nil end local r = {} for i, f_cur_raw in ipairs(cur_freqs) do local f_max = data.max_freqs[i] local f_min = data.min_freqs[i] local f_cur = math.max(math.min(f_cur_raw, f_max), f_min) r[#r + 1] = {max = f_max, min = f_min, cur = f_cur} end return r end function P.widget(tbl, data) data = data or {} return { plugin = 'timer', opts = tbl.timer_opts, cb = function(_) return tbl.cb(P.get_freqs(data)) end, event = tbl.event, } end return P ================================================ FILE: plugins/cpu-usage-linux/CMakeLists.txt ================================================ install (FILES cpu-usage-linux.lua DESTINATION ${LUA_PLUGINS_DIR}) luastatus_add_man_page (README.rst luastatus-plugin-cpu-usage-linux 7) ================================================ FILE: plugins/cpu-usage-linux/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-cpu-usage-linux .. :X-man-page-only: ################################ .. :X-man-page-only: .. :X-man-page-only: ############################################# .. :X-man-page-only: Linux-specific CPU usage plugin for luastatus .. :X-man-page-only: ############################################# .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This derived plugin periodically polls Linux ``sysfs`` for the rate of CPU usage. Functions ========= The following functions are provided: * ``get_usage(cur, prev[, cpu])`` Returns current CPU usage rate, or ``nil`` if the amount of data collected is insufficient yet. ``cpu``, if passed and not ``nil``, specifies the CPU number, starting with 1. Otherwise, average rate will be calculated. In order to use this function, you are expected to maintain two tables, ``cur`` and ``prev``, both initially empty. After calling the function, you need to swap them, i.e. run ``cur, prev = prev, cur``. This function must be invoked each second. * ``widget(tbl)`` Constructs a ``widget`` table required by luastatus. ``tbl`` is a table with the following fields: **(required)** - ``cb``: a function The callback function that will be called with current CPU usage rate, or with ``nil`` if the usage rate can not yet be calculated or if the given ``cpu`` is not plugged in. **(optional)** - ``cpu``: a number CPU number, starting with 1. - ``event`` The ``event`` entry of the resulting table (see ``luastatus`` documentation for the description of ``widget.event`` field). ================================================ FILE: plugins/cpu-usage-linux/cpu-usage-linux.lua ================================================ --[[ Copyright (C) 2015-2025 luastatus developers This file is part of luastatus. luastatus is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. luastatus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with luastatus. If not, see . --]] local P = {} local DEFAULT_PROCPATH = '/proc' local function wrap0(x) return x < 0 and 0 or x end local function clear_table(t) for k, _ in pairs(t) do t[k] = nil end end local function get_usage_impl(procpath, cur, prev, cpu) local f = assert(io.open(procpath .. '/stat', 'r')) for _ = 1, (cpu or 0) do f:read('*line') end local line = f:read('*line') f:close() if not line then clear_table(cur) clear_table(prev) return nil end cur.user, cur.nice, cur.system, cur.idle, cur.iowait, cur.irq, cur.softirq, cur.steal, cur.guest, cur.guest_nice = string.match(line, 'cpu%d*%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)') assert(cur.user ~= nil, 'Line has unexpected format') cur.user = cur.user - cur.guest cur.nice = cur.nice - cur.guest_nice cur.IdleAll = cur.idle + cur.iowait cur.SysAll = cur.system + cur.irq + cur.softirq cur.VirtAll = cur.guest + cur.guest_nice cur.Total = cur.user + cur.nice + cur.SysAll + cur.IdleAll + cur.steal + cur.VirtAll if prev.user == nil then return nil end return (wrap0(cur.user - prev.user) + wrap0(cur.nice - prev.nice) + wrap0(cur.SysAll - prev.SysAll) + wrap0(cur.steal - prev.steal) + wrap0(cur.guest - prev.guest) ) / wrap0(cur.Total - prev.Total) end function P.get_usage(cur, prev, cpu) return get_usage_impl(DEFAULT_PROCPATH, cur, prev, cpu) end function P.widget(tbl) local procpath = tbl._procpath or DEFAULT_PROCPATH local cur, prev = {}, {} return { plugin = 'timer', opts = {period = 1}, cb = function() prev, cur = cur, prev return tbl.cb(get_usage_impl(procpath, cur, prev, tbl.cpu)) end, event = tbl.event, } end return P ================================================ FILE: plugins/dbus/.gitignore ================================================ lualib.generated.inc ================================================ FILE: plugins/dbus/CMakeLists.txt ================================================ file (GLOB sources "*.c" "zoo/*.c") luastatus_add_plugin (plugin-dbus $ $ ${sources}) target_compile_definitions (plugin-dbus PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-dbus LUA) target_include_directories (plugin-dbus PUBLIC "${PROJECT_SOURCE_DIR}") find_package (PkgConfig REQUIRED) pkg_check_modules (GLIB_STUFF REQUIRED glib-2.0 gio-2.0) luastatus_target_build_with (plugin-dbus GLIB_STUFF) # find pthreads set (CMAKE_THREAD_PREFER_PTHREAD TRUE) set (THREADS_PREFER_PTHREAD_FLAG TRUE) find_package (Threads REQUIRED) # link against pthread target_link_libraries (plugin-dbus PUBLIC Threads::Threads) luastatus_add_man_page (README.rst luastatus-plugin-dbus 7) luastatus_add_man_page (README_FN_CALL_METHOD.rst luastatus-plugin-dbus-fn-call-method 7) luastatus_add_man_page (README_FN_PROP.rst luastatus-plugin-dbus-fn-prop 7) luastatus_add_man_page (README_FN_MKVAL.rst luastatus-plugin-dbus-fn-mkval 7) # === lualib stuff below === function (add_stringified_file dest src target_name) add_custom_command ( OUTPUT "${dest}" COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/cstringify.sh" ARGS "${src}" "${dest}" MAIN_DEPENDENCY "${src}" VERBATIM) add_custom_target ("${target_name}" DEPENDS "${dest}") endfunction () add_stringified_file ( "${CMAKE_CURRENT_BINARY_DIR}/lualib.generated.inc" "${CMAKE_CURRENT_SOURCE_DIR}/lualib.lua" "stringify-lualib" ) add_dependencies (plugin-dbus "stringify-lualib") target_include_directories (plugin-dbus PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") ================================================ FILE: plugins/dbus/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-dbus .. :X-man-page-only: ##################### .. :X-man-page-only: .. :X-man-page-only: ########################## .. :X-man-page-only: D-Bus plugin for luastatus .. :X-man-page-only: ########################## .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This plugin subscribes to D-Bus signals. Options ======= The following options are supported: * ``greet``: boolean Whether or not a first call to ``cb`` with ``what="hello"`` should be made as soon as the widget starts. Defaults to false. * ``report_when_ready``: boolean Whether or not to call to ``cb`` with ``what="ready"`` once the plugin has subscribed to all events successfully. Note that there is no guarantee this will be done before any other call, including calls with ``what="signal"``, because glib has something called "priorities". You can simply ignore any calls before one with ``what="ready"``. Defaults to false. * ``timeout``: number If specified and not negative, this plugin calls ``cb`` with ``what="timeout"`` if no D-Bus signal has been received in ``timeout`` seconds. * ``signals``: array of tables Array of tables with the following entries (all are optional): - ``sender``: string Sender name to match on (unique or well-known name). - ``interface``: string D-Bus interface name to match on. - ``signal``: string D-Bus signal name to match on. - ``object_path``: string Object path to match on. - ``arg0``: string Contents of the first string argument to match on. - ``flags``: array of strings The following flags are recognized: + ``"match_arg0_namespace"`` Match first arguments that contain a bus or interface name with the given namespace. + ``"match_arg0_path"`` Match first arguments that contain an object path that is either equivalent to the given path, or one of the paths is a subpath of the other. - ``bus``: string Specify the bus to subscribe to the signal on: either ``"system"`` or ``"session"``; default is ``"session"``. ``cb`` argument =============== A table with a ``what`` entry. * If ``what`` is ``"hello"``, this is the first call to ``cb`` (only if the ``greet`` option was set to ``true``). * If ``what`` is ``"ready"``, the plugin has subscribed to all the events successfully (only if the ``report_when_ready`` option was set to ``true``). * It ``what`` is ``"timeout"``, a signal has not been received for the number of seconds specified as the ``timeout`` option. * If ``what`` is ``"signal"``, a signal has been received. In this case, the table has the following additional entries: - ``bus``: string Either ``"session"`` or ``"system"``. - ``sender``: string Unique sender name. - ``object_path``: string Object path. - ``interface``: string D-Bus interface name. - ``signal``: string Signal name. - ``parameters``: *D-Bus object* Signal arguments. D-Bus objects ============= D-Bus objects are marshalled as follows: .. rst2man does not support tables with headers, so let's just use bold. +-----------------------+------------------------+ | **D-Bus object type** | **Lua representation** | +-----------------------+------------------------+ | boolean | boolean | +-----------------------+------------------------+ | byte, int16, uint16, | string | | int32, uint32, int64, | | | uint64 | | +-----------------------+------------------------+ | double | number | +-----------------------+------------------------+ | string, object path, | string | | signature | | +-----------------------+------------------------+ | handle | special object with | | | value ``"handle"`` | +-----------------------+------------------------+ | array, tuple, dict | array | | entry | | +-----------------------+------------------------+ If an object cannot be marshalled, a special object with an error is generated instead. Special objects --------------- Special objects represent D-Bus objects that cannot be marshalled to Lua. A special object is a function that, when called, returns either of: * value; * ``nil``, error. Functions ========= This plugin provides functions to: 1. interact with other programs over D-Bus (calling methods and getting/setting properties); 2. construct D-Bus values and types from within Lua; this is needed for calling methods and setting properties. The full descriptions of all those functions would be a bit too long for a single man page. So the detailed descriptions are provided in other man pages (or .rst files): * **luastatus-plugin-dbus-fn-call-method(7)** (or ``README_FN_CALL_METHOD.rst`` file): functions to call methods of D-Bus objects: - ``luastatus.plugin.call_method``: calls a method of a D-Bus object. - ``luastatus.plugin.call_method_str``: same as above, but can only call methods accepting a single string argument or no arguments. It exists because it's easier to use it compared to the above, and also for backward compatibility. * **luastatus-plugin-dbus-fn-prop(7)** (or ``README_FN_PROP.rst`` file): functions to get/set properties of D-Bus objects: - ``luastatus.plugin.get_property``: get a property of a D-Bus object. - ``luastatus.plugin.get_all_properties``: get all properties of a D-Bus object. - ``luastatus.plugin.set_property``: set a property of a D-Bus object. - ``luastatus.plugin.set_property_str``: same as above, but can only set string properties. It exists because it's easier to use it compared to the above, and also for backward compatibility. * **luastatus-plugin-dbus-fn-mkval(7)** (or ``README_FN_MKVAL.rst`` file): functions to construct D-Bus values and types from within Lua: - module ``dbustypes``: + ``luastatus.plugin.dbustypes.mkval_from_fmt``: make a D-Bus value object from a type format string and a Lua value. + ``luastatus.plugin.dbustypes.parse_dtype``: parse a type format string into a D-Bus type object. + ``luastatus.plugin.dbustypes.mkval_of_dtype``: make a D-Bus value object from a D-Bus type object and a Lua value. There is also a module named ``dbustypes_lowlevel``. It is not documented, because this module is probably of little interest to the user. It is used by implementation of "high-level" ``dbustypes`` module. ================================================ FILE: plugins/dbus/README_FN_CALL_METHOD.rst ================================================ .. :X-man-page-only: luastatus-plugin-dbus-fn-call-method .. :X-man-page-only: #################################### .. :X-man-page-only: .. :X-man-page-only: ##################################################### .. :X-man-page-only: D-Bus plugin for luastatus: functions to call methods .. :X-man-page-only: ##################################################### .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== The **dbus** plugin for **luastatus** provides functions to: 1. interact with other programs over D-Bus (calling methods and getting/setting properties); 2. construct D-Bus values and types from within Lua; this is needed for calling methods and setting properties. For more information on **luastatus**, see **luastatus(1)**. For more information on the **dbus** plugin for luastatus, see **luastatus-plugin-dbus(7)**. This man page gives full descriptions of the functions to call methods of D-Bus objects belonging to other programs. All the functions take exactly one argument, which must be a table with string keys (we are going to refer to these keys as *fields*). Common fields ============= There are a number of fields that are accepted by all the functions listed here: * ``bus`` (string, **required**): the bus to use: either ``"system"`` or ``"session"``. * ``flag_no_autostart`` (boolean, optional): whether to pass ``G_DBUS_CALL_FLAGS_NO_AUTO_START`` flag (which means don't launch an owner for the destination name in response to this method invocation). Defaults to false. * ``timeout`` (number, optional): timeout to wait for the reply for, in seconds. Default is to use the proxy default timeout. Pass ``math.huge`` to wait forever. Functions ========= The following functions for calling methods of D-Bus objects are provided: * ``luastatus.plugin.call_method(params)`` Call a D-Bus method with the specified arguments. ``params`` must be a table with the following fields, in addition to the "common" fields: - ``dest`` (string): a unique or well-known name of the owner. - ``object_path`` (string): the path to the object. - ``interface`` (string): the name of the interface. - ``method`` (string): the name of the method. - ``args`` (D-Bus value object): the arguments for the call, as a tuple. The tuple can be constructed with ``luastatus.plugin.dbustypes.mkval_from_fmt`` or ``luastatus.plugin.dbustypes.mkval_of_dtype`` functions (see **luastatus-plugin-dbus-fn-mkval(7)** for more information). On success, returns ``true, result``, where ``result`` is the result of the call call, unpacked as described in section `Marshalling`_. On failure, returns ``false, err_msg, err_code``. * ``luastatus.plugin.call_method_str(params)`` Call a D-Bus method, either with no arguments or a single string argument. This function exists because the more generic function, presented above, is harder to use for simple use cases, and also for backward compatibility. ``params`` must be a table with the following fields, in addition to the "common" fields: - ``dest`` (string): a unique or well-known name of the owner. - ``object_path`` (string): the path to the object. - ``interface`` (string): the name of the interface. - ``method`` (string): the name of the method. - ``arg_str`` (optional, string): the argument for the call. If specified, the method will be called with a single string argument. If not specified, the method will be called with no arguments. On success, returns ``true, result``, where ``result`` is the result of the call call, unpacked as described in section `Marshalling`_. On failure, returns ``false, err_msg, err_code``. Marshalling =========== Please see the main man page (**luastatus-plugin-dbus**) or the main help file (``README.rst``), section "D-Bus objects", for information about how values are converted from D-Bus objects into Lua stuff. ================================================ FILE: plugins/dbus/README_FN_MKVAL.rst ================================================ .. :X-man-page-only: luastatus-plugin-dbus-fn-mkval .. :X-man-page-only: ############################## .. :X-man-page-only: .. :X-man-page-only: ############################################################ .. :X-man-page-only: D-Bus plugin for luastatus: functions to create D-Bus values .. :X-man-page-only: ############################################################ .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== The **dbus** plugin for **luastatus** provides functions to: 1. interact with other programs over D-Bus (calling methods and getting/setting properties); 2. construct D-Bus values and types from within Lua; this is needed for calling methods and setting properties. For more information on **luastatus**, see **luastatus(1)**. For more information on the **dbus** plugin for luastatus, see **luastatus-plugin-dbus(7)**. This man page gives full descriptions of the functions to construct D-Bus values and types from within Lua. All the functions described here are in the ``dbustypes`` module; so please note that you should call them as ``luastatus.plugin.dbustypes.some_func()``. Functions ========= The following functions for construction of D-Bus values and types from within Lua are provided: * ``luastatus.plugin.dbustypes.mkval_from_fmt(fmt, lua_val)`` Make a D-Bus value object from the type string ``fmt`` and Lua representation ``lua_val``. For information on type strings, see `DBus type system and type strings`_. For information on the required structure of ``lua_val``, see `Value conversion`_. * ``luastatus.plugin.dbustypes.mkval_of_dtype(dtype, lua_val)`` Make a D-Bus value object of type ``dtype`` from Lua representation ``lua_val``. A ``dtype`` value can be obtained via a call to ``luastatus.plugin.dbustypes.parse_dtype``. For information on the required structure of ``lua_val``, see `Value conversion`_. * ``luastatus.plugin.dbustypes.parse_dtype(fmt)`` Make a D-Bus type object from the type string ``fmt``. For information on type strings, see `DBus type system and type strings`_. DBus type system and type strings ================================= This section discusses the D-Bus type system and type strings. In D-Bus, each type can be expressed as a type string, which is a printable string, e.g. ``d`` or ``(susssasa{sv}i)``. We will refer to the type string of a type as the *spelling* of that type. For a more digestible description, the reader is advised to visit and/or . Basic types ----------- There are a number of *basic types*. The spelling of a basic type is always a single letter: * ``b``: boolean type. * ``y``: byte type. * ``n``, ``i``, ``x``: signed integer types, 16-bit, 32-bit and 64-bit, correspondingly. * ``q``, ``u``, ``t``: unsigned integer types, 16-bit, 32-bit and 64-bit, correspondingly. * ``d``: floating-point type (the same as Lua number). * ``s``: string type; note that the string must be valid UTF-8, and must not contain NUL characters (``\0``). * ``o``: D-Bus object path type. This is just like a string, but with additional requirements for the content. * ``g``: D-Bus signature type. This is just like a string, but with additional requirements for the content. * ``h``: "handle" type; it represents a file descriptor passed along with the call. There is no support for handles in this module; you can create the handle type, but you can't create a handle value object. The variant type ---------------- There is a "variant" type; its spelling is ``v``. It acts as a black box: you can put value of any type into a variant value, but the type system only sees the variant type. You can put a variant into a variant, too, so there may be many levels of variants. It is frequently used to hide implementation-related details. For example, the type ``a{sv}`` (array of dictionary entries from string to variant), more conveniently "dictionary from strings to anything", is used in Desktop Notifications Specification `for hints to notification daemon `_. Hints keys are always strings, but the value might be anything (see `the list of standard hints `_). ``suppress-sound`` is boolean, ``sound-file`` is string, ``x`` and ``y`` are int32. And also ``image-data`` is ``(iiibiiay)``. Array types ----------- There is a type that represents an array of something. Each element in the array must be of the same type. If the spelling of the element type is ``X``, then ``aX`` is the spelling of the type "array of values of type ``X``". For example, ``as`` means array of strings, ``aad`` means an array of arrays of floating-point values. Tuple types ----------- While a value of type "array of string" might have any length, the number of items in the tuple is fixed in the type signature. Also, unlike arrays, tuple can contain values of different types. So, a tuple can represent things like "three strings", "an int32 and a string" or "nothing at all" (empty tuple). Tuple items don't have names; instead, they are referred to by their index. The spelling of a tuple type consists of: 1. symbol ``(``, and 2. spellings of the types of all the items concatenated (without any delimiter), and 3. symbol ``)``. So, "three strings" is spelled as ``(sss)``, "an int32 and a string" is spelled as "(is)", and "nothing at all" is spelled as ``()``. The latter is called an empty tuple. Dict entry types ---------------- The D-Bus type system doesn't have a dictionary type; instead, it emulates dictionaries with arrays of *dict entries*. Basically, a dict entry is a pair of key and value, with the requirement that the key must be of basic type. Suppose we have two types, one with spelling ``K`` and another with spelling ``V``. We want to use the type with spelling ``K`` as the "key" type in a "dictionary", and use the type with spelling ``V`` as the "value" type. (Note that this can only be done if the "key" type is basic.) Then the spelling of dict entry for this key-value pair of types is ``{KV}``. For example, ``a{sv}`` means, literally, "array of dictionary entries from string to variant", but in more human terms means "dictionary from strings to anything". Value conversion ================ During the conversion from D-Bus object to Lua value, the conversion functions require the following correspondence between D-Bus types and Lua values: +------------------------+-------------------------+ | **D-Bus type string** | **Lua representation** | +------------------------+-------------------------+ | ``b`` | boolean | +------------------------+-------------------------+ | ``y`` | either string of length | | | 1, or a number (numeric | | | value of a byte) | +------------------------+-------------------------+ | ``n``, ``i``, ``x``, | either a string or a | | ``q``, ``u``, ``t`` | number | +------------------------+-------------------------+ | double | number | +------------------------+-------------------------+ | string, object path, | string | | signature | | +------------------------+-------------------------+ | handle | special object with | | | value ``"handle"`` | +------------------------+-------------------------+ | array, tuple | array (a table with | | | numeric keys) | +------------------------+-------------------------+ | dict entry | array of length 2: key | | | and value | +------------------------+-------------------------+ Creation of handles is not supported; it is possible to create a handle *type*, but trying to create a handle *value* leads to an error being thrown. Example ======= This example shows a notification with text in red color:: function show_notification() local summary = 'Hello' local body = 'World' local mkval = luastatus.plugin.dbustypes.mkval_from_fmt local args = mkval('(susssasa{sv}i)', { 'luastatus', -- appname 0, -- replaces_id "", -- icon summary, -- summary body, -- body {}, -- actions { -- hints {'fgcolor', mkval('s', '#ff0000')}, }, -1, -- timeout }) assert(luastatus.plugin.call_method({ bus = 'session', dest = 'org.freedesktop.Notifications', object_path = '/org/freedesktop/Notifications', interface = 'org.freedesktop.Notifications', method = 'Notify', args = args, })) end Notes ===== There is also a module named ``dbustypes_lowlevel``. It is not documented, because this module is probably of little interest to the user. It is used by implementation of "high-level" ``dbustypes`` module. ================================================ FILE: plugins/dbus/README_FN_PROP.rst ================================================ .. :X-man-page-only: luastatus-plugin-dbus-fn-prop .. :X-man-page-only: ############################# .. :X-man-page-only: .. :X-man-page-only: ########################################################### .. :X-man-page-only: D-Bus plugin for luastatus: functions to get/set properties .. :X-man-page-only: ########################################################### .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== The **dbus** plugin for **luastatus** provides functions to: 1. interact with other programs over D-Bus (calling methods and getting/setting properties); 2. construct D-Bus values and types from within Lua; this is needed for calling methods and setting properties. For more information on **luastatus**, see **luastatus(1)**. For more information on the **dbus** plugin for luastatus, see **luastatus-plugin-dbus(7)**. This man page gives full descriptions of the functions to get/set properties of D-Bus objects belonging to other programs. All the functions take exactly one argument, which must be a table with string keys (we are going to refer to these keys as *fields*). Common fields ============= There are a number of fields that are accepted by all the functions listed here: * ``bus`` (string, **required**): the bus to use: either ``"system"`` or ``"session"``. * ``flag_no_autostart`` (boolean, optional): whether to pass ``G_DBUS_CALL_FLAGS_NO_AUTO_START`` flag (which means don't launch an owner for the destination name in response to this method invocation). Defaults to false. * ``timeout`` (number, optional): timeout to wait for the reply for, in seconds. Default is to use the proxy default timeout. Pass ``math.huge`` to wait forever. Functions ========= The following functions for getting/setting properties of D-Bus objects are provided: * ``luastatus.plugin.get_property(params)`` Get a D-Bus property associated with an interface. ``params`` must be a table with the following fields, in addition to the "common" fields (all are **required**): - ``dest`` (string): a unique or well-known name of the owner of the property. - ``object_path`` (string): the path to the object to get a property of. - ``interface`` (string): the name of the interface. - ``property_name`` (string): the name of the property. On success, returns ``true, result``, where ``result`` is unmarshalled as described in section `Marshalling`_. On failure, returns ``false, err_msg, err_code``. * ``luastatus.plugin.get_all_propertes(params)`` Get all D-Bus properties associated with an interface. ``params`` must be the same as to ``get_property``, except that ``property_name`` should not be set. On success, returns ``true, result``, where ``result`` is the result of the ``GetAll`` call, unmarshalled as described in section `Marshalling`_. It looks like this:: {{{"Property1", "Value1"}, {"Property2", "Value2"}}} On failure, returns ``false, err_msg, err_code``. * ``luastatus.plugin.set_property(params)`` Set a D-Bus property associated with an interface. ``params`` must be the same as to ``get_property``, except that a new field ``value`` must be set. ``value`` must be a D-Bus value object; it can be constructed with ``luastatus.plugin.dbustypes.mkval_from_fmt`` or ``luastatus.plugin.dbustypes.mkval_of_dtype`` functions (see **luastatus-plugin-dbus-fn-mkval(7)** for more information). On success, returns ``true, result``, where ``result`` is an empty table (the ``Set`` method does not return anything). On failure, returns ``false, err_msg, err_code``. * ``luastatus.plugin.set_property_str(params)`` Set D-Bus property associated with an interface, to a string. ``params`` must be the same as to ``get_property``, except that a new string field ``value_str`` must be set. This function exists because the more generic function, presented above, is harder to use for simple use cases, and also for backward compatibility. On success, returns ``true, result``, where ``result`` is an empty table (the ``Set`` method does not return anything). On failure, returns ``false, err_msg, err_code``. Marshalling =========== Please see the main man page (**luastatus-plugin-dbus**) or the main help file (``README.rst``), section "D-Bus objects", for information about how values are converted from D-Bus objects into Lua stuff. ================================================ FILE: plugins/dbus/bustype2idx.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include #include "libls/ls_compdep.h" #include "libls/ls_panic.h" LS_INHEADER size_t bustype2idx(GBusType bus_type) { switch (bus_type) { case G_BUS_TYPE_SYSTEM: return 0; case G_BUS_TYPE_SESSION: return 1; default: LS_PANIC("bustype2idx: got unexpected GBusType value"); } } ================================================ FILE: plugins/dbus/cstringify.sh ================================================ #!/bin/sh if [ "$#" -ne 2 ]; then echo >&2 "USAGE: $0 INPUT OUTPUT" exit 2 fi # 1. Escape backslashes (\ -> \\) and double quotes (" -> \"). # 2. Replace tabs with \t. # 3. Try to handle C trigraphs. # 4. Insert \n at the end, enclose the whole line in double quotes. sed -r 's/[\\"]/\\\0/g; s/\t/\\t/g; s/\?/""\0""/g; s/.*/"\0\\n"/' -- "$1" > "$2" ================================================ FILE: plugins/dbus/cvt.c ================================================ /* * Copyright (C) 2015-2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "cvt.h" #include #include #include #include #include #include #include #include #include #include "libls/ls_compdep.h" #include "libls/ls_panic.h" #include "libls/ls_lua_compat.h" static int l_special_object(lua_State *L) { lua_pushvalue(L, lua_upvalueindex(1)); // L: upvalue1 if (lua_isnil(L, -1)) { lua_pushvalue(L, lua_upvalueindex(2)); // L: upvalue1 upvalue2 return 2; } else { // L: upvalue1 return 1; } } static void push_special_object(lua_State *L, const char *s, size_t ns, bool is_error) { if (ns == (size_t) -1) { ns = strlen(s); } int num_upvalues = 1; if (is_error) { lua_pushnil(L); num_upvalues = 2; } lua_pushlstring(L, s, ns); lua_pushcclosure(L, l_special_object, num_upvalues); } // forward declaration static void push_gvariant(lua_State *L, GVariant *var, unsigned recurlim); static void on_recur_lim(lua_State *L) { push_special_object(L, "depth limit exceeded", -1, true); } static inline void push_gvariant_strlike(lua_State *L, GVariant *var) { gsize ns; const gchar *s = g_variant_get_string(var, &ns); lua_pushlstring(L, s, ns); } static void push_gvariant_iterable(lua_State *L, GVariant *var, unsigned recurlim) { if (!recurlim--) { on_recur_lim(L); return; } GVariantIter iter; g_variant_iter_init(&iter, var); size_t n = g_variant_iter_n_children(&iter); if (n > (size_t) LS_LUA_MAXI) { push_special_object(L, "array would be too big", -1, true); return; } lua_createtable(L, ls_lua_num_prealloc(n), 0); // L: table GVariant *elem; for (size_t i = 1; (elem = g_variant_iter_next_value(&iter)); ++i) { push_gvariant(L, elem, recurlim); // L: table value g_variant_unref(elem); lua_rawseti(L, -2, i); // L: table } } static inline LS_ATTR_PRINTF(2, 3) void push_small_fstr(lua_State *L, const char *fmt, ...) { char buf[32]; va_list vl; va_start(vl, fmt); vsnprintf(buf, sizeof(buf), fmt, vl); va_end(vl); lua_pushstring(L, buf); } static void push_gvariant(lua_State *L, GVariant *var, unsigned recurlim) { LS_ASSERT(var != NULL); if (!recurlim--) { on_recur_lim(L); return; } switch (g_variant_classify(var)) { case G_VARIANT_CLASS_BOOLEAN: lua_pushboolean(L, !!g_variant_get_boolean(var)); break; case G_VARIANT_CLASS_BYTE: push_small_fstr(L, "%" PRIu8, (uint8_t) g_variant_get_byte(var)); break; case G_VARIANT_CLASS_INT16: push_small_fstr(L, "%" PRIi16, (int16_t) g_variant_get_int16(var)); break; case G_VARIANT_CLASS_UINT16: push_small_fstr(L, "%" PRIu16, (uint16_t) g_variant_get_uint16(var)); break; case G_VARIANT_CLASS_INT32: push_small_fstr(L, "%" PRIi32, (int32_t) g_variant_get_int32(var)); break; case G_VARIANT_CLASS_UINT32: push_small_fstr(L, "%" PRIu32, (uint32_t) g_variant_get_uint32(var)); break; case G_VARIANT_CLASS_INT64: push_small_fstr(L, "%" PRIi64, (int64_t) g_variant_get_int64(var)); break; case G_VARIANT_CLASS_UINT64: push_small_fstr(L, "%" PRIu64, (uint64_t) g_variant_get_uint64(var)); break; case G_VARIANT_CLASS_DOUBLE: lua_pushnumber(L, g_variant_get_double(var)); break; case G_VARIANT_CLASS_STRING: case G_VARIANT_CLASS_OBJECT_PATH: case G_VARIANT_CLASS_SIGNATURE: push_gvariant_strlike(L, var); break; case G_VARIANT_CLASS_VARIANT: { GVariant *boxed = g_variant_get_variant(var); push_gvariant(L, boxed, recurlim); g_variant_unref(boxed); } break; case G_VARIANT_CLASS_ARRAY: case G_VARIANT_CLASS_TUPLE: case G_VARIANT_CLASS_DICT_ENTRY: push_gvariant_iterable(L, var, recurlim); break; case G_VARIANT_CLASS_HANDLE: default: { const GVariantType *type = g_variant_get_type(var); const gchar *s = g_variant_type_peek_string(type); gsize ns = g_variant_type_get_string_length(type); push_special_object(L, s, ns, false); } break; } } void cvt(lua_State *L, GVariant *var) { if (!var) { lua_pushnil(L); return; } if (lua_checkstack(L, 210)) { push_gvariant(L, var, 200); } else { push_special_object(L, "out of memory", -1, true); } } ================================================ FILE: plugins/dbus/cvt.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include // "cvt" means "convert", as in "convert GVariant to Lua value". void cvt(lua_State *L, GVariant *var); ================================================ FILE: plugins/dbus/dbus.c ================================================ /* * Copyright (C) 2015-2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include #include "libls/ls_alloc_utils.h" #include "libls/ls_time_utils.h" #include "libls/ls_panic.h" #include "libmoonvisit/moonvisit.h" #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "bustype2idx.h" #include "cvt.h" #include "load_lualib.h" #include "zoo/zoo.h" typedef struct { char *sender; char *interface; char *signal; char *object_path; char *arg0; GDBusSignalFlags flags; } Signal; static void signal_free(Signal s) { free(s.sender); free(s.interface); free(s.signal); free(s.object_path); free(s.arg0); } typedef struct { Signal *data; size_t size; size_t capacity; } SignalList; static inline SignalList signal_list_new(void) { return (SignalList) {NULL, 0, 0}; } static inline void signal_list_add(SignalList *x, Signal s) { if (x->size == x->capacity) { x->data = LS_M_X2REALLOC(x->data, &x->capacity); } x->data[x->size++] = s; } static inline void signal_list_destroy(SignalList *x) { for (size_t i = 0; i < x->size; ++i) { signal_free(x->data[i]); } free(x->data); } typedef struct { SignalList subs[2]; double tmo; bool greet; bool report_when_ready; Zoo *zoo; } Priv; static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; for (int i = 0; i < 2; ++i) { signal_list_destroy(&p->subs[i]); } if (p->zoo) { zoo_destroy(p->zoo); } free(p); } static inline SignalList *get_sub(Priv *p, GBusType bus_type) { size_t idx = bustype2idx(bus_type); return &p->subs[idx]; } static int parse_bus_str(MoonVisit *mv, void *ud, const char *s, size_t ns) { (void) ns; GBusType *out = ud; if (strcmp(s, "session") == 0) { *out = G_BUS_TYPE_SESSION; return 1; } if (strcmp(s, "system") == 0) { *out = G_BUS_TYPE_SYSTEM; return 1; } moon_visit_err(mv, "unknown bus: '%s'", s); return -1; } static int parse_flags_elem(MoonVisit *mv, void *ud, int kpos, int vpos) { mv->where = "'signals' element, 'flags' element"; (void) kpos; GDBusSignalFlags *out = ud; if (moon_visit_checktype_at(mv, NULL, vpos, LUA_TSTRING) < 0) goto error; const char *s = lua_tostring(mv->L, vpos); if (strcmp(s, "match_arg0_namespace") == 0) { *out |= G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE; return 1; } if (strcmp(s, "match_arg0_path") == 0) { *out |= G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_PATH; return 1; } moon_visit_err(mv, "unknown flag: '%s'", s); error: return -1; } static int parse_signals_elem(MoonVisit *mv, void *ud, int kpos, int vpos) { mv->where = "'signals' element"; (void) kpos; Priv *p = ud; Signal s = {0}; if (moon_visit_checktype_at(mv, NULL, vpos, LUA_TTABLE) < 0) goto error; GBusType bus_type = G_BUS_TYPE_SESSION; if (moon_visit_str_f(mv, vpos, "bus", parse_bus_str, &bus_type, true) < 0) goto error; if (moon_visit_str(mv, vpos, "sender", &s.sender, NULL, true) < 0) goto error; if (moon_visit_str(mv, vpos, "interface", &s.interface, NULL, true) < 0) goto error; if (moon_visit_str(mv, vpos, "signal", &s.signal, NULL, true) < 0) goto error; if (moon_visit_str(mv, vpos, "object_path", &s.object_path, NULL, true) < 0) goto error; if (moon_visit_str(mv, vpos, "arg0", &s.arg0, NULL, true) < 0) goto error; if (moon_visit_table_f(mv, vpos, "flags", parse_flags_elem, &s.flags, true) < 0) goto error; signal_list_add(get_sub(p, bus_type), s); return 1; error: signal_free(s); return -1; } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .subs = {signal_list_new(), signal_list_new()}, .tmo = -1, .greet = false, .report_when_ready = false, .zoo = zoo_new(), }; char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Parse greet if (moon_visit_bool(&mv, -1, "greet", &p->greet, true) < 0) goto mverror; // Parse report_when_ready if (moon_visit_bool(&mv, -1, "report_when_ready", &p->report_when_ready, true) < 0) goto mverror; // Parse timeout if (moon_visit_num(&mv, -1, "timeout", &p->tmo, true) < 0) goto mverror; // Parse signals if (moon_visit_table_f(&mv, -1, "signals", parse_signals_elem, p, false) < 0) goto mverror; return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); //error: destroy(pd); return LUASTATUS_ERR; } static void register_funcs(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv; zoo_register_funcs(p->zoo, L); (void) load_lualib(pd, L); } typedef struct { LuastatusPluginData *pd; LuastatusPluginRunFuncs funcs; GDBusConnection *cnx_session; GDBusConnection *cnx_system; pthread_mutex_t mtx; } PluginRunArgs; static inline void set_str(lua_State *L, const char *s, const char *key) { if (s) { lua_pushstring(L, s); lua_setfield(L, -2, key); } } static void callback_signal( GDBusConnection *cnx, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { PluginRunArgs *args = (PluginRunArgs *) user_data; LS_PTH_CHECK(pthread_mutex_lock(&args->mtx)); const char *bus_name = ""; if (cnx == args->cnx_session) { bus_name = "session"; } else if (cnx == args->cnx_system) { bus_name = "system"; } lua_State *L = args->funcs.call_begin(args->pd->userdata); lua_createtable(L, 0, 7); // L: table set_str(L, "signal", "what"); set_str(L, bus_name, "bus"); set_str(L, sender_name, "sender"); set_str(L, object_path, "object_path"); set_str(L, interface_name, "interface"); set_str(L, signal_name, "signal"); cvt(L, parameters); // L: table value lua_setfield(L, -2, "parameters"); // L: table args->funcs.call_end(args->pd->userdata); LS_PTH_CHECK(pthread_mutex_unlock(&args->mtx)); } static void report_simple( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, const char *what) { lua_State *L = funcs.call_begin(pd->userdata); lua_createtable(L, 0, 1); // L: table lua_pushstring(L, what); // L: table string lua_setfield(L, -2, "what"); // L: table funcs.call_end(pd->userdata); } static gboolean callback_timeout(gpointer user_data) { PluginRunArgs *args = (PluginRunArgs *) user_data; LS_PTH_CHECK(pthread_mutex_lock(&args->mtx)); report_simple(args->pd, args->funcs, "timeout"); LS_PTH_CHECK(pthread_mutex_unlock(&args->mtx)); return G_SOURCE_CONTINUE; } static gboolean callback_idle(gpointer user_data) { PluginRunArgs *args = (PluginRunArgs *) user_data; LS_PTH_CHECK(pthread_mutex_lock(&args->mtx)); report_simple(args->pd, args->funcs, "ready"); LS_PTH_CHECK(pthread_mutex_unlock(&args->mtx)); return G_SOURCE_REMOVE; } static GDBusConnection *maybe_connect_and_subscribe( Priv *p, GBusType bus_type, gpointer userdata, GError **err) { SignalList *SL = get_sub(p, bus_type); if (!SL->size) { return NULL; } GDBusConnection *cnx = g_bus_get_sync(bus_type, NULL, err); if (*err) { return NULL; } LS_ASSERT(cnx != NULL); for (size_t i = 0; i < SL->size; ++i) { Signal s = SL->data[i]; g_dbus_connection_signal_subscribe( cnx, s.sender, s.interface, s.signal, s.object_path, s.arg0, s.flags, callback_signal, userdata, NULL); } return cnx; } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { Priv *p = pd->priv; GError *err = NULL; GDBusConnection *session_bus = NULL; GDBusConnection *system_bus = NULL; GMainLoop *mainloop = NULL; GMainContext *context = g_main_context_ref_thread_default(); GSource *source_tmo = NULL; GSource *source_idle = NULL; PluginRunArgs args = {.pd = pd, .funcs = funcs}; LS_PTH_CHECK(pthread_mutex_init(&args.mtx, NULL)); session_bus = maybe_connect_and_subscribe(p, G_BUS_TYPE_SESSION, &args, &err); if (err) { LS_FATALF(pd, "cannot connect to the session bus: %s", err->message); goto error; } system_bus = maybe_connect_and_subscribe(p, G_BUS_TYPE_SYSTEM, &args, &err); if (err) { LS_FATALF(pd, "cannot connect to the system bus: %s", err->message); goto error; } args.cnx_session = session_bus; args.cnx_system = system_bus; LS_TimeDelta TD = ls_double_to_TD(p->tmo, LS_TD_FOREVER); if (!ls_TD_is_forever(TD)) { int tmo_ms = ls_TD_to_poll_ms_tmo(TD); source_tmo = g_timeout_source_new(tmo_ms); g_source_set_callback(source_tmo, callback_timeout, &args, NULL); if (g_source_attach(source_tmo, context) == 0) { LS_FATALF(pd, "g_source_attach() failed (timeout source)"); goto error; } } if (p->greet) { report_simple(pd, funcs, "hello"); } if (p->report_when_ready) { source_idle = g_idle_source_new(); g_source_set_callback(source_idle, callback_idle, &args, NULL); if (g_source_attach(source_idle, context) == 0) { LS_FATALF(pd, "g_source_attach() failed (idle source)"); goto error; } } mainloop = g_main_loop_new(context, FALSE); g_main_loop_run(mainloop); error: if (source_tmo) { g_source_unref(source_tmo); } if (source_idle) { g_source_unref(source_idle); } if (context) { g_main_context_unref(context); } if (mainloop) { g_main_loop_unref(mainloop); } if (session_bus) { g_dbus_connection_close_sync(session_bus, NULL, NULL); g_object_unref(session_bus); } if (system_bus) { g_dbus_connection_close_sync(system_bus, NULL, NULL); g_object_unref(system_bus); } if (err) { g_error_free(err); } LS_PTH_CHECK(pthread_mutex_destroy(&args.mtx)); } LuastatusPluginIface luastatus_plugin_iface_v1 = { .init = init, .register_funcs = register_funcs, .run = run, .destroy = destroy, }; ================================================ FILE: plugins/dbus/load_lualib.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "load_lualib.h" #include #include #include #include "include/sayf_macros.h" #include "include/plugin_data_v1.h" static void report_and_pop_error(LuastatusPluginData *pd, lua_State *L, int rc) { const char *msg = lua_tostring(L, -1); if (!msg) { msg = "(error object cannot be converted to string)"; } LS_ERRF(pd, "cannot load lualib.lua: [%d] %s", rc, msg); lua_pop(L, 1); // L: ? } bool load_lualib(LuastatusPluginData *pd, lua_State *L) { static const char *LUA_SOURCE = #include "lualib.generated.inc" ; int rc; // L: ? table rc = luaL_loadbuffer(L, LUA_SOURCE, strlen(LUA_SOURCE), ""); if (rc != 0) { // L: ? table error report_and_pop_error(pd, L, rc); // L: ? table return false; } // L: ? table chunk lua_pushvalue(L, -2); // L: ? table chunk table rc = lua_pcall(L, 1, 0, 0); if (rc != 0) { // L: ? table error report_and_pop_error(pd, L, rc); // L: ? table return false; } // L: ? table return true; } ================================================ FILE: plugins/dbus/load_lualib.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include #include "include/plugin_data_v1.h" bool load_lualib(LuastatusPluginData *pd, lua_State *L); ================================================ FILE: plugins/dbus/lualib.lua ================================================ local PLUGIN_TABLE = ... local DT_LL = PLUGIN_TABLE.dbustypes_lowlevel -- Map with index: returns a new array 'ys' such that #ys == #xs and -- ys[i] = f(xs[i], i). local function imap(xs, f) local res = {} for i = 1, #xs do res[i] = f(xs[i], i) end return res end -- This table contains true for all type specifiers that specify a simple type. local IS_TYPE_SIMPLE = { b = true, y = true, n = true, q = true, i = true, u = true, x = true, t = true, h = true, d = true, s = true, o = true, g = true, v = true, } -- Parses fmt string 'fmt' starting with (1-based) position 'i'. -- Returns: 'dtype, j', where: -- * 'dtype' is the parsed D-Bus type object; -- * 'j' is the (1-based) position of the "leftover" (the part after the parsed prefix). local function parse_dtype_impl(fmt, i) assert(i <= #fmt, 'fmt string terminated too early') local c = fmt:sub(i, i) if IS_TYPE_SIMPLE[c] then return DT_LL.mktype_simple(c), i + 1 end if c == 'a' then local elem_dtype elem_dtype, i = parse_dtype_impl(fmt, i + 1) return DT_LL.mktype_array(elem_dtype), i end if c == '(' then i = i + 1 local items_dtypes = {} while fmt:sub(i, i) ~= ')' do local item_dtype item_dtype, i = parse_dtype_impl(fmt, i) items_dtypes[#items_dtypes + 1] = item_dtype end return DT_LL.mktype_tuple(items_dtypes), i + 1 end if c == '{' then i = i + 1 local k_dtype, v_dtype k_dtype, i = parse_dtype_impl(fmt, i) v_dtype, i = parse_dtype_impl(fmt, i) assert(fmt:sub(i, i) == '}', 'invalid dict entry type fmt') return DT_LL.mktype_dict_entry(k_dtype, v_dtype), i + 1 end error(string.format('invalid type specifier "%s"', c)) end -- Parses the whole fmt string. Returns the parsed D-Bus type object. local function parse_dtype(fmt) assert(type(fmt) == 'string', 'fmt string must be of string type') local dtype, i = parse_dtype_impl(fmt, 1) assert(i == #fmt + 1, 'extra data after fmt string') return dtype end -- Converts a Lua value 'x' into a D-Bus value object of type 'dtype'. local function mkval_of_dtype(dtype, x) local category = dtype:get_category() if category == 'simple' then return DT_LL.mkval_simple(dtype, x) end if category == 'array' then assert(type(x) == 'table', 'array type requires a table argument') local elem_dtype = dtype:get_elem_type() local elems = imap(x, function(y, _) return mkval_of_dtype(elem_dtype, y) end) return DT_LL.mkval_array(elem_dtype, elems) end if category == 'tuple' then assert(type(x) == 'table', 'tuple type requires a table argument') local item_dtypes = dtype:get_item_types() assert(#x == #item_dtypes, 'length of given array does not match the # of elements in tuple') local items = imap(x, function(y, i) return mkval_of_dtype(item_dtypes[i], y) end) return DT_LL.mkval_tuple(items) end if category == 'dict_entry' then assert(type(x) == 'table', 'dict entry type requires a table argument') local dtype_k, dtype_v = dtype:get_kv_types() assert(#x == 2, 'dict entry type requires array of size 2') local k = mkval_of_dtype(dtype_k, x[1]) local v = mkval_of_dtype(dtype_v, x[2]) return DT_LL.mkval_dict_entry(k, v) end error('unexpected type') -- this should never happen end -- Converts a Lua value 'x' into a D-Bus value object of type given by fmt string 'fmt'. local function mkval_from_fmt(fmt, x) return mkval_of_dtype(parse_dtype(fmt), x) end PLUGIN_TABLE.dbustypes = { mkval_from_fmt = mkval_from_fmt, mkval_of_dtype = mkval_of_dtype, parse_dtype = parse_dtype, } ================================================ FILE: plugins/dbus/zoo/README.txt ================================================ This stuff is called zoo because it's a kind of interactive zoo. --- "uncvt" means "unconvert": files zoo_uncvt_{type,val}.{c,h} do the inverse of what files ../cvt.{c,h} do: the latter can convert from GVariant objects to Lua values, the former provide a mechanism to create GVariant objects from within Lua. ================================================ FILE: plugins/dbus/zoo/zoo.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "zoo.h" #include #include #include #include #include #include #include #include #include "libls/ls_alloc_utils.h" #include "libls/ls_panic.h" #include "zoo_call_params.h" #include "zoo_uncvt_type.h" #include "zoo_uncvt_val.h" #include "../cvt.h" #include "../bustype2idx.h" static void failure(lua_State *L, GError *err) { lua_pushboolean(L, 0); lua_pushstring(L, err->message); } static void success(lua_State *L, GVariant *res) { lua_pushboolean(L, 1); cvt(L, res); } static const char *lookupf(Zoo_CallParams *p, const char *key) { for (Zoo_StrField *f = p->str_fields; f->key; ++f) { if (strcmp(key, f->key) == 0) { return f->value; } } char buf[128]; snprintf(buf, sizeof(buf), "lookupf: cannot find string field '%s'", key); LS_PANIC(buf); } struct Zoo { GDBusConnection *conns[2]; }; static int do_the_bloody_thing( lua_State *L, Zoo *z, Zoo_CallParams *p, const char *dest, const char *object_path, const char *interface_name, const char *method_name, GVariant *args) { GDBusConnection *conn = z->conns[bustype2idx(p->bus_type)]; if (!conn) { GError *err = NULL; conn = g_bus_get_sync(p->bus_type, NULL, &err); if (!conn) { LS_ASSERT(err != NULL); failure(L, err); g_error_free(err); // Dispose of /args/. if (g_variant_is_floating(args)) { g_variant_unref(args); } return 2; } LS_ASSERT(err == NULL); z->conns[bustype2idx(p->bus_type)] = conn; } GError *err = NULL; GVariant *res = g_dbus_connection_call_sync( conn, /*bus_name=*/ dest, /*object_path=*/ object_path, /*interface_name=*/ interface_name, /*method_name=*/ method_name, /*parameters=*/ args, /*reply_type=*/ NULL, /*flags=*/ p->flags, /*timeout_ms=*/ p->tmo_ms, /*cancellable=*/ NULL, /*error=*/ &err ); if (res) { LS_ASSERT(err == NULL); success(L, res); g_variant_unref(res); } else { LS_ASSERT(err != NULL); failure(L, err); g_error_free(err); } return 2; } static int l_call_method(lua_State *L) { Zoo *z = lua_touserdata(L, lua_upvalueindex(1)); Zoo_StrField str_fields[] = { {.key = "dest"}, {.key = "object_path"}, {.key = "interface"}, {.key = "method"}, {0}, }; Zoo_CallParams p = { .str_fields = str_fields, .gvalue_field_name = "args", .gvalue_field_must_be_tuple = true, }; zoo_call_params_parse(L, &p, 1); int nret = do_the_bloody_thing( L, z, &p, /*dest=*/ lookupf(&p, "dest"), /*object_path=*/ lookupf(&p, "object_path"), /*interface_name=*/ lookupf(&p, "interface"), /*method_name=*/ lookupf(&p, "method"), /*args=*/ p.gvalue ); zoo_call_params_free(&p); return nret; } static int l_call_method_str(lua_State *L) { Zoo *z = lua_touserdata(L, lua_upvalueindex(1)); Zoo_StrField str_fields[] = { {.key = "dest"}, {.key = "object_path"}, {.key = "interface"}, {.key = "method"}, {.key = "arg_str", .nullable = true}, {0}, }; Zoo_CallParams p = { .str_fields = str_fields, }; zoo_call_params_parse(L, &p, 1); const char *arg_str = lookupf(&p, "arg_str"); GVariant *args = arg_str ? g_variant_new("(s)", arg_str) : g_variant_new("()") ; int nret = do_the_bloody_thing( L, z, &p, /*dest=*/ lookupf(&p, "dest"), /*object_path=*/ lookupf(&p, "object_path"), /*interface_name=*/ lookupf(&p, "interface"), /*method_name=*/ lookupf(&p, "method"), /*args=*/ args ); zoo_call_params_free(&p); return nret; } static int l_get_property(lua_State *L) { Zoo *z = lua_touserdata(L, lua_upvalueindex(1)); Zoo_StrField str_fields[] = { {.key = "dest"}, {.key = "object_path"}, {.key = "interface"}, {.key = "property_name"}, {0}, }; Zoo_CallParams p = { .str_fields = str_fields, }; zoo_call_params_parse(L, &p, 1); int nret = do_the_bloody_thing( L, z, &p, /*dest=*/ lookupf(&p, "dest"), /*object_path=*/ lookupf(&p, "object_path"), /*interface_name=*/ "org.freedesktop.DBus.Properties", /*method_name=*/ "Get", /*args=*/ g_variant_new( "(ss)", lookupf(&p, "interface"), lookupf(&p, "property_name") ) ); zoo_call_params_free(&p); return nret; } static int l_get_all_properties(lua_State *L) { Zoo *z = lua_touserdata(L, lua_upvalueindex(1)); Zoo_StrField str_fields[] = { {.key = "dest"}, {.key = "object_path"}, {.key = "interface"}, {0}, }; Zoo_CallParams p = { .str_fields = str_fields, }; zoo_call_params_parse(L, &p, 1); int nret = do_the_bloody_thing( L, z, &p, /*dest=*/ lookupf(&p, "dest"), /*object_path=*/ lookupf(&p, "object_path"), /*interface_name=*/ "org.freedesktop.DBus.Properties", /*method_name=*/ "GetAll", /*args=*/ g_variant_new( "(s)", lookupf(&p, "interface") ) ); zoo_call_params_free(&p); return nret; } static int l_set_property_str(lua_State *L) { Zoo *z = lua_touserdata(L, lua_upvalueindex(1)); Zoo_StrField str_fields[] = { {.key = "dest"}, {.key = "object_path"}, {.key = "interface"}, {.key = "property_name"}, {.key = "value_str"}, {0}, }; Zoo_CallParams p = { .str_fields = str_fields, }; zoo_call_params_parse(L, &p, 1); int nret = do_the_bloody_thing( L, z, &p, /*dest=*/ lookupf(&p, "dest"), /*object_path=*/ lookupf(&p, "object_path"), /*interface_name=*/ "org.freedesktop.DBus.Properties", /*method_name=*/ "Set", /*args=*/ g_variant_new( "(ssv)", lookupf(&p, "interface"), lookupf(&p, "property_name"), g_variant_new("s", lookupf(&p, "value_str")) ) ); zoo_call_params_free(&p); return nret; } static int l_set_property(lua_State *L) { Zoo *z = lua_touserdata(L, lua_upvalueindex(1)); Zoo_StrField str_fields[] = { {.key = "dest"}, {.key = "object_path"}, {.key = "interface"}, {.key = "property_name"}, {0}, }; Zoo_CallParams p = { .str_fields = str_fields, .gvalue_field_name = "value", }; zoo_call_params_parse(L, &p, 1); int nret = do_the_bloody_thing( L, z, &p, /*dest=*/ lookupf(&p, "dest"), /*object_path=*/ lookupf(&p, "object_path"), /*interface_name=*/ "org.freedesktop.DBus.Properties", /*method_name=*/ "Set", /*args=*/ g_variant_new( "(ssv)", lookupf(&p, "interface"), lookupf(&p, "property_name"), g_variant_new_variant(p.gvalue) ) ); zoo_call_params_free(&p); return nret; } Zoo *zoo_new(void) { Zoo *z = LS_XNEW(Zoo, 1); *z = (Zoo) {0}; return z; } void zoo_register_funcs(Zoo *z, lua_State *L) { // L: ? table lua_newtable(L); // L: ? table table zoo_uncvt_type_register_mt_and_funcs(L); // L: ? table table zoo_uncvt_val_register_mt_and_funcs(L); // L: ? table table lua_setfield(L, -2, "dbustypes_lowlevel"); // L: ? table #define REG(Name_, F_) \ (lua_pushlightuserdata(L, z), \ lua_pushcclosure(L, (F_), 1), \ lua_setfield(L, -2, (Name_))) REG("call_method", l_call_method); REG("call_method_str", l_call_method_str); REG("get_property", l_get_property); REG("get_all_properties", l_get_all_properties); REG("set_property", l_set_property); REG("set_property_str", l_set_property_str); #undef REG } void zoo_destroy(Zoo *z) { for (size_t i = 0; i < 2; ++i) { GDBusConnection *conn = z->conns[i]; if (conn) { g_dbus_connection_close_sync(conn, NULL, NULL); g_object_unref(conn); } } free(z); } ================================================ FILE: plugins/dbus/zoo/zoo.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include struct Zoo; typedef struct Zoo Zoo; Zoo *zoo_new(void); void zoo_register_funcs(Zoo *z, lua_State *L); void zoo_destroy(Zoo *z); ================================================ FILE: plugins/dbus/zoo/zoo_call_params.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "zoo_call_params.h" #include #include #include #include #include "libls/ls_alloc_utils.h" #include "libls/ls_panic.h" #include "libls/ls_time_utils.h" #include "zoo_uncvt_val.h" #include "zoo_call_prot.h" static void check_type(lua_State *L, int pos, const char *key, int expected_type, bool nullable) { int t = lua_type(L, pos); if (t != expected_type) { (void) luaL_error( L, "%s: expected %s%s, found %s", key, lua_typename(L, expected_type), nullable ? " or nil" : "", lua_typename(L, t)); LS_MUST_BE_UNREACHABLE(); } } static void do_str(lua_State *L, const char *key, char **dst, bool nullable) { // L: ? table lua_getfield(L, 1, key); // L: ? table value if (nullable && lua_isnil(L, -1)) { goto done; } check_type(L, -1, key, LUA_TSTRING, nullable); size_t ns; const char *s = lua_tolstring(L, -1, &ns); if (!g_utf8_validate_len(s, ns, NULL)) { (void) luaL_error(L, "%s: value contains invalid UTF-8", key); LS_MUST_BE_UNREACHABLE(); } *dst = ls_xstrdup(s); done: lua_pop(L, 1); // L: ? table } static void do_gvalue(lua_State *L, const char *key, GVariant **dst, bool must_be_tuple) { // L: ? table lua_getfield(L, 1, key); // L: ? table value GVariant *v = zoo_uncvt_val_fetch_newref(L, -1, key); *dst = v; if (must_be_tuple) { if (!g_variant_type_is_tuple(g_variant_get_type(v))) { (void) luaL_error(L, "%s: is not of tuple type", key); LS_MUST_BE_UNREACHABLE(); } } lua_pop(L, 1); // L: ? table } static void maybe_do_bool(lua_State *L, const char *key, bool *dst) { // L: ? table lua_getfield(L, 1, key); // L: ? table value if (lua_isnil(L, -1)) { goto done; } check_type(L, -1, key, LUA_TBOOLEAN, true); *dst = lua_toboolean(L, -1); done: lua_pop(L, 1); // L: ? table } static void maybe_do_timeout(lua_State *L, const char *key, int *dst) { // L: ? table lua_getfield(L, 1, key); // L: ? table value if (lua_isnil(L, -1)) { goto done; } check_type(L, -1, key, LUA_TNUMBER, true); double d = lua_tonumber(L, -1); LS_TimeDelta TD; if (ls_double_to_TD_checked(d, &TD)) { *dst = ls_TD_to_poll_ms_tmo(TD); } else { (void) luaL_error(L, "%s: invalid timeout value", key); LS_MUST_BE_UNREACHABLE(); } done: lua_pop(L, 1); // L: ? table } static void do_bus_type(lua_State *L, const char *key, GBusType *dst) { // L: ? table lua_getfield(L, 1, key); // L: ? table value check_type(L, -1, key, LUA_TSTRING, false); const char *s = lua_tostring(L, -1); if (strcmp(s, "system") == 0) { *dst = G_BUS_TYPE_SYSTEM; } else if (strcmp(s, "session") == 0) { *dst = G_BUS_TYPE_SESSION; } else { (void) luaL_error(L, "%s: expected either 'system' or 'session'", key); LS_MUST_BE_UNREACHABLE(); } lua_pop(L, 1); // L: ? table } static int do_parse_throwable(lua_State *L) { Zoo_CallParams *p = lua_touserdata(L, lua_upvalueindex(1)); LS_ASSERT(lua_gettop(L) == 1); do_bus_type(L, "bus", &p->bus_type); for (Zoo_StrField *f = p->str_fields; f->key; ++f) { do_str(L, f->key, &f->value, f->nullable); } if (p->gvalue_field_name) { do_gvalue(L, p->gvalue_field_name, &p->gvalue, p->gvalue_field_must_be_tuple); } bool no_autostart = false; maybe_do_bool(L, "flag_no_autostart", &no_autostart); p->flags = no_autostart ? G_DBUS_CALL_FLAGS_NO_AUTO_START : 0; p->tmo_ms = -1; maybe_do_timeout(L, "timeout", &p->tmo_ms); return 0; } void zoo_call_params_free(Zoo_CallParams *p) { for (Zoo_StrField *f = p->str_fields; f->key; ++f) { free(f->value); } if (p->gvalue) { g_variant_unref(p->gvalue); } } void zoo_call_params_parse(lua_State *L, Zoo_CallParams *p, int arg) { luaL_checktype(L, arg, LUA_TTABLE); lua_pushvalue(L, arg); if (!zoo_call_prot(L, 1, 0, do_parse_throwable, p)) { zoo_call_params_free(p); lua_error(L); } } ================================================ FILE: plugins/dbus/zoo/zoo_call_params.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include #include #include typedef struct { const char *key; char *value; bool nullable; } Zoo_StrField; typedef struct { Zoo_StrField *str_fields; const char *gvalue_field_name; bool gvalue_field_must_be_tuple; GVariant *gvalue; GBusType bus_type; GDBusCallFlags flags; int tmo_ms; } Zoo_CallParams; void zoo_call_params_parse(lua_State *L, Zoo_CallParams *p, int arg); void zoo_call_params_free(Zoo_CallParams *p); ================================================ FILE: plugins/dbus/zoo/zoo_call_prot.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "zoo_call_prot.h" #include #include bool zoo_call_prot(lua_State *L, int nargs, int nresults, lua_CFunction f, void *f_ud) { // L: ? args lua_pushlightuserdata(L, f_ud); // L: ? args ud lua_pushcclosure(L, f, 1); // L: ? args func lua_insert(L, -(nargs + 1)); // L: ? func args return lua_pcall(L, nargs, nresults, 0) == 0; } ================================================ FILE: plugins/dbus/zoo/zoo_call_prot.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include bool zoo_call_prot(lua_State *L, int args, int nresults, lua_CFunction f, void *f_ud); ================================================ FILE: plugins/dbus/zoo/zoo_checkudata.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "zoo_checkudata.h" #include #include #include "libls/ls_panic.h" void *zoo_checkudata(lua_State *L, int pos, const char *tname, const char *what) { // L: ? void *ud = lua_touserdata(L, pos); if (!ud) { goto error; } if (!lua_getmetatable(L, pos)) { goto error; } // L: ? actual_mt lua_getfield(L, LUA_REGISTRYINDEX, tname); // L: ? actual_mt expected_mt if (!lua_rawequal(L, -1, -2)) { goto error; } lua_pop(L, 2); // L: ? return ud; error: (void) luaL_error(L, "%s: is not a '%s' userdata value", what, tname); LS_MUST_BE_UNREACHABLE(); } ================================================ FILE: plugins/dbus/zoo/zoo_checkudata.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include void *zoo_checkudata(lua_State *L, int pos, const char *tname, const char *what); ================================================ FILE: plugins/dbus/zoo/zoo_mt.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "zoo_mt.h" #include #include void zoo_mt_begin(lua_State *L, const char *mt_name) { // L: ? luaL_newmetatable(L, mt_name); // L: ? mt lua_pushvalue(L, -1); // L: ? mt mt lua_setfield(L, -2, "__index"); // L: ? mt } void zoo_mt_add_method(lua_State *L, const char *name, lua_CFunction f) { lua_pushcfunction(L, f); // L: ? mt f lua_setfield(L, -2, name); // L: ? mt } void zoo_mt_end(lua_State *L) { // L: ? mt lua_pop(L, 1); // L: ? } ================================================ FILE: plugins/dbus/zoo/zoo_mt.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include void zoo_mt_begin(lua_State *L, const char *mt_name); void zoo_mt_add_method(lua_State *L, const char *name, lua_CFunction f); void zoo_mt_end(lua_State *L); ================================================ FILE: plugins/dbus/zoo/zoo_uncvt_type.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "zoo_uncvt_type.h" #include #include #include #include #include #include #include "libls/ls_alloc_utils.h" #include "libls/ls_panic.h" #include "libls/ls_lua_compat.h" #include "zoo_call_prot.h" #include "zoo_checkudata.h" #include "zoo_mt.h" typedef struct { // This is an owning handle, not a borrow. // May be NULL, which means it's already been "finalized" (garbage-collected). GVariantType *t; } Tobj; static const char *MT_NAME = "io.shdown.luastatus.plugin.dbus.official.Type"; static Tobj *fetch_tobj(lua_State *L, int pos, const char *what) { return zoo_checkudata(L, pos, MT_NAME, what); } // Returns a borrow. static const GVariantType *fetch_gtype_borrow(lua_State *L, int pos, const char *what) { Tobj *tobj = fetch_tobj(L, pos, what); if (!tobj->t) { (void) luaL_error(L, "this type object has already been finalized"); LS_MUST_BE_UNREACHABLE(); } return tobj->t; } // /t/ must be an owning handle, which then gets "stolen". static void make_tobj_steal(lua_State *L, GVariantType *t) { LS_ASSERT(t != NULL); // L: ? Tobj *tobj = lua_newuserdata(L, sizeof(Tobj)); // L: ? ud tobj->t = t; luaL_getmetatable(L, MT_NAME); // L: ? ud mt lua_setmetatable(L, -2); // L: ? ud } // /t/ is a borrow, which gets copied. static void make_tobj_copy(lua_State *L, const GVariantType *t) { LS_ASSERT(t != NULL); make_tobj_steal(L, g_variant_type_copy(t)); } static int tobj_gc(lua_State *L) { Tobj *tobj = fetch_tobj(L, 1, "argument #1"); if (tobj->t) { g_variant_type_free(tobj->t); tobj->t = NULL; } return 0; } static int l_equals_to(lua_State *L) { // Both /t1/ and /t2/ are borrowed (STACK). const GVariantType *t1 = fetch_gtype_borrow(L, 1, "argument #1"); const GVariantType *t2 = fetch_gtype_borrow(L, 2, "argument #2"); lua_pushboolean(L, g_variant_type_equal(t1, t2)); return 1; } static int l_get_type_string(lua_State *L) { // /t/ is borrowed (STACK). const GVariantType *t = fetch_gtype_borrow(L, 1, "argument #1"); size_t ns = g_variant_type_get_string_length(t); const char *s = g_variant_type_peek_string(t); lua_pushlstring(L, s, ns); return 1; } static int l_get_category(lua_State *L) { // /t/ is borrowed (STACK). const GVariantType *t = fetch_gtype_borrow(L, 1, "argument #1"); if (g_variant_type_is_basic(t) || g_variant_type_is_variant(t)) { lua_pushstring(L, "simple"); } else if (g_variant_type_is_array(t)) { lua_pushstring(L, "array"); } else if (g_variant_type_is_dict_entry(t)) { lua_pushstring(L, "dict_entry"); } else if (g_variant_type_is_tuple(t)) { lua_pushstring(L, "tuple"); } else { return luaL_argerror(L, 1, "unknown type"); } return 1; } static int l_is_basic(lua_State *L) { // /t/ is borrowed (STACK). const GVariantType *t = fetch_gtype_borrow(L, 1, "argument #1"); lua_pushboolean(L, !!g_variant_type_is_basic(t)); return 1; } static int l_get_item_types(lua_State *L) { // /t/ is borrowed (STACK). const GVariantType *t = fetch_gtype_borrow(L, 1, "argument #1"); if (!g_variant_type_is_tuple(t)) { return luaL_argerror(L, 1, "is not a tuple type"); } // L: ? size_t n = g_variant_type_n_items(t); if (n > (size_t) LS_LUA_MAXI) { return luaL_error(L, "tuple type has too many elements"); } lua_createtable(L, ls_lua_num_prealloc(n), 0); // L: ? arr size_t i = 1; for ( const GVariantType *item_t = g_variant_type_first(t); item_t; item_t = g_variant_type_next(item_t)) { // L: ? arr make_tobj_copy(L, item_t); // L: ? arr type lua_rawseti(L, -2, i); // L: ? arr ++i; } return 1; } static int l_get_elem_type(lua_State *L) { // /t/ is borrowed (STACK). const GVariantType *t = fetch_gtype_borrow(L, 1, "argument #1"); if (!g_variant_type_is_array(t)) { return luaL_argerror(L, 1, "is not an array type"); } make_tobj_copy(L, g_variant_type_element(t)); return 1; } static int l_get_kv_types(lua_State *L) { // /t/ is borrowed (STACK). const GVariantType *t = fetch_gtype_borrow(L, 1, "argument #1"); if (!g_variant_type_is_dict_entry(t)) { return luaL_argerror(L, 1, "is not a dict entry type"); } // L: ? make_tobj_copy(L, g_variant_type_key(t)); // L: ? ktype make_tobj_copy(L, g_variant_type_value(t)); // L: ? ktype vtype return 2; } static int l_mktype_simple(lua_State *L) { static const char VALID[] = { 'b', // boolean 'y', // byte 'n', // int16 'q', // uint16 'i', // int32 'u', // uint32 'x', // int64 't', // uint64 'h', // handle 'd', // double 's', // string 'o', // object_path 'g', // signature 'v', // variant }; size_t ns; const char *s = luaL_checklstring(L, 1, &ns); if (ns != 1) { goto bad; } if (memchr(VALID, (unsigned char) s[0], sizeof(VALID)) == NULL) { goto bad; } make_tobj_steal(L, g_variant_type_new(s)); return 1; bad: return luaL_argerror(L, 1, "not a simple type name"); } static int l_mktype_array(lua_State *L) { // /t/ is borrowed (STACK). const GVariantType *t = fetch_gtype_borrow(L, 1, "argument #1"); make_tobj_steal(L, g_variant_type_new_array(t)); return 1; } static int l_mktype_dict_entry(lua_State *L) { // Both /tk/ and /tv/ are borrowed (STACK). const GVariantType *tk = fetch_gtype_borrow(L, 1, "argument #1"); const GVariantType *tv = fetch_gtype_borrow(L, 2, "argument #2"); if (!g_variant_type_is_basic(tk)) { return luaL_argerror(L, 1, "key type is not a basic type"); } make_tobj_steal(L, g_variant_type_new_dict_entry(tk, tv)); return 1; } typedef struct { const GVariantType **items; size_t n_copied; } MkTupleUD; static int throwable_l_mktype_tuple(lua_State *L) { MkTupleUD *ud = lua_touserdata(L, lua_upvalueindex(1)); luaL_checktype(L, 1, LUA_TTABLE); size_t n = ls_lua_array_len(L, 1); ud->items = LS_XNEW(const GVariantType *, n); for (size_t i = 1; i <= n; ++i) { lua_rawgeti(L, 1, i); // /t/ is borrowed (ARRAY). const GVariantType *t = fetch_gtype_borrow(L, -1, "array element"); // Since /t/ is ARRAY-borrowed, we copy it right away. ud->items[ud->n_copied++] = g_variant_type_copy(t); lua_pop(L, 1); } GVariantType *res = g_variant_type_new_tuple(ud->items, n); make_tobj_steal(L, res); return 1; } static int l_mktype_tuple(lua_State *L) { MkTupleUD ud = {0}; bool ok = zoo_call_prot(L, lua_gettop(L), 1, throwable_l_mktype_tuple, &ud); for (size_t i = 0; i < ud.n_copied; ++i) { g_variant_type_free((GVariantType *) ud.items[i]); } free(ud.items); if (ok) { return 1; } else { return lua_error(L); } } const GVariantType *zoo_uncvt_type_fetch_borrow(lua_State *L, int pos, const char *what) { return fetch_gtype_borrow(L, pos, what); } void zoo_uncvt_type_bake_steal(GVariantType *t, lua_State *L) { make_tobj_steal(L, t); } static void register_mt(lua_State *L) { zoo_mt_begin(L, MT_NAME); zoo_mt_add_method(L, "get_type_string", l_get_type_string); zoo_mt_add_method(L, "get_category", l_get_category); zoo_mt_add_method(L, "is_basic", l_is_basic); zoo_mt_add_method(L, "get_item_types", l_get_item_types); zoo_mt_add_method(L, "get_elem_type", l_get_elem_type); zoo_mt_add_method(L, "get_kv_types", l_get_kv_types); zoo_mt_add_method(L, "equals_to", l_equals_to); zoo_mt_add_method(L, "__gc", tobj_gc); zoo_mt_end(L); } void zoo_uncvt_type_register_mt_and_funcs(lua_State *L) { register_mt(L); #define REG(Name_, F_) (lua_pushcfunction(L, (F_)), lua_setfield(L, -2, (Name_))) REG("mktype_simple", l_mktype_simple); REG("mktype_array", l_mktype_array); REG("mktype_dict_entry", l_mktype_dict_entry); REG("mktype_tuple", l_mktype_tuple); #undef REG } ================================================ FILE: plugins/dbus/zoo/zoo_uncvt_type.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include // Returns a borrow. const GVariantType *zoo_uncvt_type_fetch_borrow(lua_State *L, int pos, const char *what); // Steals /t/. void zoo_uncvt_type_bake_steal(GVariantType *t, lua_State *L); void zoo_uncvt_type_register_mt_and_funcs(lua_State *L); ================================================ FILE: plugins/dbus/zoo/zoo_uncvt_val.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "zoo_uncvt_val.h" #include #include #include #include #include #include #include #include #include "libls/ls_panic.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_lua_compat.h" #include "zoo_checkudata.h" #include "zoo_call_prot.h" #include "zoo_uncvt_type.h" #include "zoo_mt.h" #include "../cvt.h" typedef struct { // This is an owning handle (separate reference to GVariant), not a borrow. // This is never a "floating reference". // May be NULL, which means it's already been "finalized" (garbage-collected). GVariant *v; } Vobj; static const char *MT_NAME = "io.shdown.luastatus.plugin.dbus.official.Value"; static Vobj *fetch_vobj(lua_State *L, int pos, const char *what) { return zoo_checkudata(L, pos, MT_NAME, what); } static GVariant *fetch_gvar_borrow(lua_State *L, int pos, const char *what) { Vobj *vobj = fetch_vobj(L, pos, what); if (!vobj->v) { (void) luaL_error(L, "this value object has already been finalized"); LS_MUST_BE_UNREACHABLE(); } return vobj->v; } static void make_vobj_steal(lua_State *L, GVariant *v) { LS_ASSERT(v != NULL); LS_ASSERT(!g_variant_is_floating(v)); // L: ? Vobj *vobj = lua_newuserdata(L, sizeof(Vobj)); // L: ? ud vobj->v = v; luaL_getmetatable(L, MT_NAME); // L: ? ud mt lua_setmetatable(L, -2); // L: ? ud } static void make_vobj_from_floating(lua_State *L, GVariant *v) { LS_ASSERT(v != NULL); LS_ASSERT(g_variant_is_floating(v)); make_vobj_steal(L, g_variant_ref_sink(v)); } static int vobj_gc(lua_State *L) { Vobj *vobj = fetch_vobj(L, 1, "argument #1"); if (vobj->v) { g_variant_unref(vobj->v); vobj->v = NULL; } return 0; } static bool fetch_bool(lua_State *L, int arg) { luaL_checktype(L, arg, LUA_TBOOLEAN); return lua_toboolean(L, arg); } static uint8_t fetch_byte(lua_State *L, int arg) { int t = lua_type(L, arg); if (t == LUA_TNUMBER) { return lua_tointeger(L, arg); } else if (t == LUA_TSTRING) { size_t ns; const char *s = lua_tolstring(L, arg, &ns); if (ns != 1) { return luaL_argerror(L, arg, "string for 'byte' value is not of length 1"); } return s[0]; } else { return luaL_argerror(L, arg, "expected either a number of a string"); } } static uint64_t fetch_int(lua_State *L, int arg, bool is_signed) { int t = lua_type(L, arg); if (t == LUA_TNUMBER) { return lua_tointeger(L, arg); } else if (t == LUA_TSTRING) { const char *s = lua_tostring(L, arg); char *endptr; unsigned long long res; errno = 0; if (is_signed) { res = strtoll(s, &endptr, 0); } else { res = strtoull(s, &endptr, 0); } if (errno != 0 || s[0] == '\0' || *endptr != '\0') { const char *errmsg = is_signed ? "cannot convert this string to signed integer" : "cannot convert this string to unsigned integer"; return luaL_argerror(L, arg, errmsg); } return res; } else { return luaL_argerror(L, arg, "expected either a number of a string"); } } static const char *fetch_utf8(lua_State *L, int arg) { size_t ns; const char *s = luaL_checklstring(L, arg, &ns); if (!g_utf8_validate_len(s, ns, NULL)) { (void) luaL_argerror(L, arg, "this strings contains invalid UTF-8"); LS_MUST_BE_UNREACHABLE(); } return s; } static GVariant *mkval_simple_impl(lua_State *L) { // /t/ is borrowed (STACK). const GVariantType *t = zoo_uncvt_type_fetch_borrow(L, 1, "argument #1"); if (g_variant_type_get_string_length(t) != 1) { goto bad_type; } const char *type_str = g_variant_type_peek_string(t); char type_sigil = type_str[0]; switch (type_sigil) { case 'b': return g_variant_new_boolean(fetch_bool(L, 2)); case 'y': return g_variant_new_byte(fetch_byte(L, 2)); case 'n': return g_variant_new_int16(fetch_int(L, 2, true)); case 'q': return g_variant_new_uint16(fetch_int(L, 2, false)); case 'i': return g_variant_new_int32(fetch_int(L, 2, true)); case 'u': return g_variant_new_uint32(fetch_int(L, 2, false)); case 'x': return g_variant_new_int64(fetch_int(L, 2, true)); case 't': return g_variant_new_uint64(fetch_int(L, 2, false)); case 'd': return g_variant_new_double(luaL_checknumber(L, 2)); case 's': return g_variant_new_string(fetch_utf8(L, 2)); case 'v': return g_variant_new_variant(fetch_gvar_borrow(L, 2, "argument #2")); } if (type_sigil == 'o') { const char *s = luaL_checkstring(L, 2); if (!g_variant_is_object_path(s)) { (void) luaL_argerror(L, 2, "not a valid D-Bus object path"); LS_MUST_BE_UNREACHABLE(); } return g_variant_new_object_path(s); } if (type_sigil == 'g') { const char *s = luaL_checkstring(L, 2); if (!g_variant_is_signature(s)) { (void) luaL_argerror(L, 2, "not a valid D-Bus type signature"); LS_MUST_BE_UNREACHABLE(); } return g_variant_new_signature(s); } if (type_sigil == 'h') { (void) luaL_error(L, "creation of handles is not supported"); LS_MUST_BE_UNREACHABLE(); } bad_type: (void) luaL_error(L, "not a simple type"); LS_MUST_BE_UNREACHABLE(); } static int l_mkval_simple(lua_State *L) { make_vobj_from_floating(L, mkval_simple_impl(L)); return 1; } static int l_mkval_dict_entry(lua_State *L) { // Both /k/ and /v/ are borrowed (STACK). GVariant *k = fetch_gvar_borrow(L, 1, "argument #1"); GVariant *v = fetch_gvar_borrow(L, 2, "argument #2"); if (!g_variant_type_is_basic(g_variant_get_type(k))) { return luaL_argerror(L, 1, "key is not of a basic type"); } make_vobj_from_floating(L, g_variant_new_dict_entry(k, v)); return 1; } typedef struct { GVariant **items; size_t n_refd; } MkSomethingUD; static void free_ud(MkSomethingUD ud) { for (size_t i = 0; i < ud.n_refd; ++i) { g_variant_unref(ud.items[i]); } free(ud.items); } static int throwable_l_mkval_tuple(lua_State *L) { MkSomethingUD *ud = lua_touserdata(L, lua_upvalueindex(1)); luaL_checktype(L, 1, LUA_TTABLE); size_t n = ls_lua_array_len(L, 1); ud->items = LS_XNEW(GVariant *, n); for (size_t i = 1; i <= n; ++i) { lua_rawgeti(L, 1, i); // /v/ is borrowed (ARRAY). GVariant *v = fetch_gvar_borrow(L, -1, "array element"); // Since /v/ is ARRAY-borrowed, we ref it right away. ud->items[ud->n_refd++] = g_variant_ref(v); lua_pop(L, 1); } GVariant *res = g_variant_new_tuple(ud->items, n); make_vobj_from_floating(L, res); return 1; } static int l_mkval_tuple(lua_State *L) { MkSomethingUD ud = {0}; bool ok = zoo_call_prot(L, lua_gettop(L), 1, throwable_l_mkval_tuple, &ud); free_ud(ud); if (ok) { return 1; } else { return lua_error(L); } } static int throwable_l_mkval_array(lua_State *L) { MkSomethingUD *ud = lua_touserdata(L, lua_upvalueindex(1)); // /t/ is borrowed (STACK). const GVariantType *t = zoo_uncvt_type_fetch_borrow(L, 1, "argument #1"); luaL_checktype(L, 2, LUA_TTABLE); size_t n = ls_lua_array_len(L, 2); ud->items = LS_XNEW(GVariant *, n); for (size_t i = 1; i <= n; ++i) { lua_rawgeti(L, 2, i); // /v/ is borrowed (ARRAY). GVariant *v = fetch_gvar_borrow(L, -1, "array element"); // Check the type of /v/. if (!g_variant_type_equal(t, g_variant_get_type(v))) { char msg[128]; snprintf( msg, sizeof(msg), "type of array element #%zu doesn't match the passed element type", i); return luaL_error(L, "%s", msg); } // Since /v/ is ARRAY-borrowed, we ref it right away. ud->items[ud->n_refd++] = g_variant_ref(v); lua_pop(L, 1); } GVariant *res = g_variant_new_array(t, ud->items, n); make_vobj_from_floating(L, res); return 1; } static int l_mkval_array(lua_State *L) { MkSomethingUD ud = {0}; bool ok = zoo_call_prot(L, lua_gettop(L), 1, throwable_l_mkval_array, &ud); free_ud(ud); if (ok) { return 1; } else { return lua_error(L); } } static int l_get_type(lua_State *L) { // /v/ is borrowed (STACK). GVariant *v = fetch_gvar_borrow(L, 1, "argument #1"); GVariantType *t = g_variant_type_copy(g_variant_get_type(v)); zoo_uncvt_type_bake_steal(t, L); return 1; } static int l_equals_to(lua_State *L) { // Both /a/ and /b/ are borrowed (STACK). GVariant *a = fetch_gvar_borrow(L, 1, "argument #1"); GVariant *b = fetch_gvar_borrow(L, 2, "argument #2"); lua_pushboolean(L, !!g_variant_equal(a, b)); return 1; } static int l_to_lua(lua_State *L) { // /v/ is borrowed (STACK). GVariant *v = fetch_gvar_borrow(L, 1, "argument #1"); cvt(L, v); return 1; } GVariant *zoo_uncvt_val_fetch_newref(lua_State *L, int pos, const char *what) { GVariant *v = fetch_gvar_borrow(L, pos, what); return g_variant_ref(v); } static void register_mt(lua_State *L) { zoo_mt_begin(L, MT_NAME); zoo_mt_add_method(L, "get_type", l_get_type); zoo_mt_add_method(L, "equals_to", l_equals_to); zoo_mt_add_method(L, "to_lua", l_to_lua); zoo_mt_add_method(L, "__gc", vobj_gc); zoo_mt_end(L); } void zoo_uncvt_val_register_mt_and_funcs(lua_State *L) { register_mt(L); #define REG(Name_, F_) (lua_pushcfunction(L, (F_)), lua_setfield(L, -2, (Name_))) REG("mkval_simple", l_mkval_simple); REG("mkval_dict_entry", l_mkval_dict_entry); REG("mkval_tuple", l_mkval_tuple); REG("mkval_array", l_mkval_array); #undef REG } ================================================ FILE: plugins/dbus/zoo/zoo_uncvt_val.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include // Returns a new reference to the value. // This reference is never a "floating reference". GVariant *zoo_uncvt_val_fetch_newref(lua_State *L, int pos, const char *what); void zoo_uncvt_val_register_mt_and_funcs(lua_State *L); ================================================ FILE: plugins/disk-io-linux/CMakeLists.txt ================================================ install (FILES disk-io-linux.lua DESTINATION ${LUA_PLUGINS_DIR}) luastatus_add_man_page (README.rst luastatus-plugin-disk-io-linux 7) ================================================ FILE: plugins/disk-io-linux/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-disk-io-linux .. :X-man-page-only: ############################## .. :X-man-page-only: .. :X-man-page-only: ################################################# .. :X-man-page-only: Linux-specific disk I/O rate plugin for luastatus .. :X-man-page-only: ################################################# .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This derived plugin periodically polls Linux ``procfs`` and calculates disk I/O rates for all disk devices. Functions ========= The following functions are provided: * ``read_diskstats(old, divisor)`` Reads current ``diskstats`` measurements and also produces an array with deltas (number of bytes read/written per second for each disk device). ``divisor`` must be the period, in seconds, that this function is called. The function will average ``read_bytes`` and ``written_bytes`` over this period. Returns ``new, deltas``, where ``new`` is the table with current measurements. ``old`` must be a table with previous measurements (or an empty table if this is the first call to this function). ``deltas`` is an array of tables with the following entries: - ``num_major`` (number): disk device's major number; - ``num_minor`` (number): disk device's minor number; - ``name`` (string): disk device's name, as specified in ``/proc/diskstats``; - ``read_bytes`` (number): number of bytes read per second, averaged over the last ``divisor`` seconds; - ``written_bytes`` (number): number of bytes written per second, averaged over the last ``divisor`` seconds. **NOTE:** ``read_bytes`` and ``written_bytes`` entries may be negative; this means an overflow happened in kernel's statistics. If any of these numbers is negative, it is recommended simply not to include this disk device in current output. In order to use this function, you are expected to maintain a table ``old``, initially empty, and pass it to ``read_diskstats`` as the first argument each time, then set it to the first return value. For example:: local old = {} local period = 5 -- each 'period' seconds: local new, deltas = plugin.diskstats(old, period) old = new -- somehow use 'deltas' * ``widget(tbl)`` Constructs a ``widget`` table required by luastatus. ``tbl`` is a table with the following fields: **(required)** - ``cb``: a function The callback function that will be called with current deltas. The argument is the same as the second return value (``deltas``) of ``read_diskstats`` (see above for description of ``read_diskstats`` function for more information). **(optional)** - ``period``: table Period, in seconds, to query updates. Defaults to 1. - ``event`` The ``event`` entry of the resulting table (see ``luastatus`` documentation for the description of ``widget.event`` field). ================================================ FILE: plugins/disk-io-linux/disk-io-linux.lua ================================================ --[[ Copyright (C) 2025 luastatus developers This file is part of luastatus. luastatus is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. luastatus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with luastatus. If not, see . --]] local PATTERN = '^%s*' .. ('(%S+)%s+'):rep(10) -- This is a kernel constant; it's not related to the actual device's characteristics. local SECTOR_SIZE = 512 local P = {} local function do_with_file(f, callback) local is_ok, err = pcall(callback) f:close() if not is_ok then error(err) end end function P.read_diskstats(old, divisor, _proc_path) _proc_path = _proc_path or '/proc' local factor = SECTOR_SIZE / divisor local new = {} local deltas = {} local f = assert(io.open(_proc_path .. '/diskstats', 'r')) do_with_file(f, function() for line in f:lines() do local num_major_str, num_minor_str, name, _, _, read_str, _, _, _, written_str = line:match(PATTERN) if not num_major_str then -- line does not match the pattern. Kinda weird. -- Let's just stop processing the file. break end local key = string.format('%s:%s:%s', num_major_str, num_minor_str, name) local old_entry = old[key] local read = assert(tonumber(read_str)) local written = assert(tonumber(written_str)) if old_entry then deltas[#deltas + 1] = { num_major = assert(tonumber(num_major_str)), num_minor = assert(tonumber(num_minor_str)), name = name, read_bytes = factor * (read - old_entry.read), written_bytes = factor * (written - old_entry.written), } end new[key] = {read = read, written = written} end end) return new, deltas end function P.widget(tbl) local period = tbl.period or 1 local old = {} return { plugin = 'timer', opts = { period = period, }, cb = function(_) local new, deltas = P.read_diskstats(old, period, tbl._proc_path) old = new return tbl.cb(deltas) end, event = tbl.event, } end return P ================================================ FILE: plugins/file-contents-linux/CMakeLists.txt ================================================ install (FILES file-contents-linux.lua DESTINATION ${LUA_PLUGINS_DIR}) luastatus_add_man_page (README.rst luastatus-plugin-file-contents-linux 7) ================================================ FILE: plugins/file-contents-linux/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-file-contents-linux .. :X-man-page-only: #################################### .. :X-man-page-only: .. :X-man-page-only: ################################################# .. :X-man-page-only: Linux-specific file contents plugin for luastatus .. :X-man-page-only: ################################################# .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This derived plugin monitors the content of a file. Functions ========= The following functions are provided: * ``widget(tbl)`` Constructs a ``widget`` table required by luastatus. ``tbl`` is a table with the following fields: **(required)** * ``filename``: string Path to the file to monitor. * ``cb``: function The callback that will be called with ``filename`` opened for reading. **(optional)** * ``timeout``, ``flags`` Better do not touch (or see the code). * ``event`` The ``event`` entry of the resulting table (see ``luastatus`` documentation for the description of ``widget.event`` field). ================================================ FILE: plugins/file-contents-linux/file-contents-linux.lua ================================================ --[[ Copyright (C) 2015-2025 luastatus developers This file is part of luastatus. luastatus is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. luastatus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with luastatus. If not, see . --]] local P = {} function P.widget(tbl) local flags = tbl.flags or {'close_write', 'delete_self', 'oneshot'} local timeout = tbl.timeout or 5 return { plugin = 'inotify', opts = { watch = {}, greet = true, }, cb = function() if not luastatus.plugin.add_watch(tbl.filename, flags) then luastatus.plugin.push_timeout(timeout) error('add_watch() failed') end local f = assert(io.open(tbl.filename, 'r')) local is_ok, res = pcall(tbl.cb, f) f:close() if not is_ok then error(res) end return res end, event = tbl.event, } end return P ================================================ FILE: plugins/fs/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_plugin (plugin-fs $ $ ${sources}) target_compile_definitions (plugin-fs PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-fs LUA) target_include_directories (plugin-fs PUBLIC "${PROJECT_SOURCE_DIR}") # find pthreads set (CMAKE_THREAD_PREFER_PTHREAD TRUE) set (THREADS_PREFER_PTHREAD_FLAG TRUE) find_package (Threads REQUIRED) # link against pthread target_link_libraries (plugin-fs PUBLIC Threads::Threads) luastatus_add_man_page (README.rst luastatus-plugin-fs 7) ================================================ FILE: plugins/fs/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-fs .. :X-man-page-only: ##################### .. :X-man-page-only: .. :X-man-page-only: ############################### .. :X-man-page-only: disk usage plugin for luastatus .. :X-man-page-only: ############################### .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This plugin monitors file system usage. It is timer-driven, plus a wake-up FIFO can be specified. Options ======== The following options are supported: * ``paths``: array of strings For each of these paths, an information on usage of the file system it belongs to is returned. * ``globs``: array of strings Same as ``paths`` but accepts glob patterns. It is not an error if the pattern expands to nothing. Useful for monitoring filesystems which are mounted at runtime. * ``enable_dyn_paths``: boolean Whether to enable support for adding/removing paths dynamically (in run time). If set to true, functions ``add_dyn_path``, ``remove_dyn_path``, ``get_max_dyn_paths`` will be available. Defaults to false. * ``period``: number A number of seconds to sleep before calling ``cb`` again. May be fractional. Defaults to 10. * ``fifo``: string Path to an existent FIFO. The plugin does not create FIFO itself. To force a wake-up, ``touch(1)`` the FIFO, that is, open it for writing and then close. ``cb`` argument =============== A table where keys are paths and values are tables with the following entries: * ``total``: number of bytes total; * ``free``: number of bytes free; * ``avail``: number of bytes free for unprivileged users. Functions ========= If ``enable_dyn_paths`` option was set to true, then: * the set of "dynamic" path, initially empty, is maintained in run time; * each call to ``cb`` also includes all paths from this set; * the functions related to adding/removing paths dynamically are provided. The following functions that are related to adding/removing dynamically are provided if ``enable_dyn_paths`` option was set to true: * ``luastatus.plugin.add_dyn_path(path)`` Tries to add ``path`` (which must be a string) to the set of "dynamic" paths. On successful insertion, it returns ``true``. If the path is already in the set, returns ``false``. * ``luastatus.plugin.remove_dyn_path(path)`` Tries to remove ``path`` (which must be string) from the set of "dynamic" paths. On successful removal, it returns ``true``. If the path is not in the set, returns ``false``. ================================================ FILE: plugins/fs/fs.c ================================================ /* * Copyright (C) 2015-2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "libmoonvisit/moonvisit.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_strarr.h" #include "libls/ls_fifo_device.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_time_utils.h" #include "libls/ls_panic.h" #include "strlist.h" typedef struct { LS_StringArray paths; LS_StringArray globs; double period; char *fifo; bool dyn_paths_enabled; Strlist dyn_paths; pthread_mutex_t dyn_mtx; } Priv; static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; ls_strarr_destroy(p->paths); ls_strarr_destroy(p->globs); free(p->fifo); if (p->dyn_paths_enabled) { strlist_destroy(p->dyn_paths); LS_PTH_CHECK(pthread_mutex_destroy(&p->dyn_mtx)); } free(p); } static int parse_paths_elem(MoonVisit *mv, void *ud, int kpos, int vpos) { mv->where = "'paths' element"; (void) kpos; Priv *p = ud; if (moon_visit_checktype_at(mv, NULL, vpos, LUA_TSTRING) < 0) return -1; const char *s = lua_tostring(mv->L, vpos); ls_strarr_append_s(&p->paths, s); return 1; } static int parse_globs_elem(MoonVisit *mv, void *ud, int kpos, int vpos) { mv->where = "'globs' element"; (void) kpos; Priv *p = ud; if (moon_visit_checktype_at(mv, NULL, vpos, LUA_TSTRING) < 0) return -1; const char *s = lua_tostring(mv->L, vpos); ls_strarr_append_s(&p->globs, s); return 1; } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .paths = ls_strarr_new(), .globs = ls_strarr_new(), .dyn_paths_enabled = false, .period = 10.0, .fifo = NULL, }; char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Parse paths if (moon_visit_table_f(&mv, -1, "paths", parse_paths_elem, p, true) < 0) goto mverror; // Parse globs if (moon_visit_table_f(&mv, -1, "globs", parse_globs_elem, p, true) < 0) goto mverror; // Parse period if (moon_visit_num(&mv, -1, "period", &p->period, true) < 0) goto mverror; if (!ls_double_is_good_time_delta(p->period)) { LS_FATALF(pd, "period is invalid"); goto error; } // Parse fifo if (moon_visit_str(&mv, -1, "fifo", &p->fifo, NULL, true) < 0) goto mverror; // Parse enable_dyn_paths bool enable_dyn_paths = false; if (moon_visit_bool(&mv, -1, "enable_dyn_paths", &enable_dyn_paths, true) < 0) { goto mverror; } if (enable_dyn_paths) { p->dyn_paths_enabled = true; p->dyn_paths = strlist_new(); LS_PTH_CHECK(pthread_mutex_init(&p->dyn_mtx, NULL)); } // Warn if both paths and globs are empty and /enable_dyn_paths/ is false if (!enable_dyn_paths && !ls_strarr_size(p->paths) && !ls_strarr_size(p->globs)) { LS_WARNF(pd, "both paths and globs are empty, and enable_dyn_paths is false"); } return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); error: destroy(pd); return LUASTATUS_ERR; } static bool push_for(LuastatusPluginData *pd, lua_State *L, const char *path) { struct statvfs st; if (statvfs(path, &st) < 0) { LS_WARNF(pd, "statvfs: %s: %s", path, ls_tls_strerror(errno)); return false; } lua_createtable(L, 0, 3); // L: table lua_pushnumber(L, ((double) st.f_frsize) * st.f_blocks); // L: table n lua_setfield(L, -2, "total"); // L: table lua_pushnumber(L, ((double) st.f_frsize) * st.f_bfree); // L: table n lua_setfield(L, -2, "free"); // L: table lua_pushnumber(L, ((double) st.f_frsize) * st.f_bavail); // L: table n lua_setfield(L, -2, "avail"); // L: table return true; } typedef struct { lua_State *L; LuastatusPluginData *pd; LuastatusPluginRunFuncs funcs; } Call; static inline Call start_call(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { lua_State *L = funcs.call_begin(pd->userdata); lua_newtable(L); return (Call) { .L = L, .pd = pd, .funcs = funcs, }; } static inline void add_path_to_call(Call c, const char *path) { LS_ASSERT(path != NULL); if (push_for(c.pd, c.L, path)) { lua_setfield(c.L, -2, path); } } static inline void end_call(Call c) { c.funcs.call_end(c.pd->userdata); } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { Priv *p = pd->priv; LS_FifoDevice dev = ls_fifo_device_new(); LS_TimeDelta TD = ls_double_to_TD_or_die(p->period); while (1) { // make a call Call call = start_call(pd, funcs); for (size_t i = 0; i < ls_strarr_size(p->paths); ++i) { add_path_to_call(call, ls_strarr_at(p->paths, i, NULL)); } if (p->dyn_paths_enabled) { LS_PTH_CHECK(pthread_mutex_lock(&p->dyn_mtx)); for (size_t i = 0; i < p->dyn_paths.size; ++i) { add_path_to_call(call, p->dyn_paths.data[i]); } LS_PTH_CHECK(pthread_mutex_unlock(&p->dyn_mtx)); } for (size_t i = 0; i < ls_strarr_size(p->globs); ++i) { const char *pattern = ls_strarr_at(p->globs, i, NULL); glob_t gbuf; switch (glob(pattern, GLOB_NOSORT, NULL, &gbuf)) { case 0: case GLOB_NOMATCH: break; default: LS_WARNF(pd, "glob() failed (out of memory?)"); } for (size_t j = 0; j < gbuf.gl_pathc; ++j) { add_path_to_call(call, gbuf.gl_pathv[j]); } globfree(&gbuf); } end_call(call); // wait if (ls_fifo_device_open(&dev, p->fifo) < 0) { LS_WARNF(pd, "ls_fifo_device_open: %s: %s", p->fifo, ls_tls_strerror(errno)); } if (ls_fifo_device_wait(&dev, TD) < 0) { LS_FATALF(pd, "ls_fifo_device_wait: %s: %s", p->fifo, ls_tls_strerror(errno)); goto error; } } error: ls_fifo_device_close(&dev); } static int lfunc_add_dyn_path(lua_State *L) { Priv *p = lua_touserdata(L, lua_upvalueindex(1)); const char *path = luaL_checkstring(L, 1); LS_ASSERT(p->dyn_paths_enabled); LS_PTH_CHECK(pthread_mutex_lock(&p->dyn_mtx)); bool rc = strlist_push(&p->dyn_paths, path); LS_PTH_CHECK(pthread_mutex_unlock(&p->dyn_mtx)); lua_pushboolean(L, rc); return 1; } static int lfunc_remove_dyn_path(lua_State *L) { Priv *p = lua_touserdata(L, lua_upvalueindex(1)); const char *path = luaL_checkstring(L, 1); LS_ASSERT(p->dyn_paths_enabled); LS_PTH_CHECK(pthread_mutex_lock(&p->dyn_mtx)); bool rc = strlist_remove(&p->dyn_paths, path); LS_PTH_CHECK(pthread_mutex_unlock(&p->dyn_mtx)); lua_pushboolean(L, rc); return 1; } // Note: this Lua function is provided for backward compatibility purposes only. static int lfunc_get_max_dyn_paths(lua_State *L) { lua_pushinteger(L, INT_MAX); return 1; } static void register_funcs(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv; if (!p->dyn_paths_enabled) { return; } // L: ? table lua_pushlightuserdata(L, p); // L: ? table ud lua_pushcclosure(L, lfunc_add_dyn_path, 1); // L: ? table func lua_setfield(L, -2, "add_dyn_path"); // L: ? table lua_pushlightuserdata(L, p); // L: ? table ud lua_pushcclosure(L, lfunc_remove_dyn_path, 1); // L: ? table func lua_setfield(L, -2, "remove_dyn_path"); // L: ? table // Note: function /get_max_dyn_paths/ is provided for backward compatibility purposes only. lua_pushcfunction(L, lfunc_get_max_dyn_paths); // L: ? table func lua_setfield(L, -2, "get_max_dyn_paths"); // L: ? table } LuastatusPluginIface luastatus_plugin_iface_v1 = { .init = init, .register_funcs = register_funcs, .run = run, .destroy = destroy, }; ================================================ FILE: plugins/fs/strlist.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "strlist.h" #include #include #include "libls/ls_alloc_utils.h" #include "libls/ls_panic.h" Strlist strlist_new(void) { return (Strlist) {0}; } static void resize_to(Strlist *x, size_t new_size) { x->data = LS_M_XREALLOC(x->data, new_size); x->size = new_size; } #define BAD_INDEX ((size_t) -1) static size_t find(Strlist *x, const char *s) { char **data = x->data; size_t size = x->size; for (size_t i = 0; i < size; ++i) { if (strcmp(data[i], s) == 0) { return i; } } return BAD_INDEX; } bool strlist_push(Strlist *x, const char *s) { LS_ASSERT(s != NULL); if (find(x, s) != BAD_INDEX) { return false; } // Increase the size. resize_to(x, x->size + 1); // Insert the new element. x->data[x->size - 1] = ls_xstrdup(s); return true; } static void remove_at(Strlist *x, size_t i) { LS_ASSERT(i < x->size); // Free the string at index /i/. free(x->data[i]); // Move the last element to position /i/. size_t i_last = x->size - 1; x->data[i] = x->data[i_last]; // Decrease the size. resize_to(x, x->size - 1); } bool strlist_remove(Strlist *x, const char *s) { LS_ASSERT(s != NULL); size_t i = find(x, s); if (i == BAD_INDEX) { return false; } remove_at(x, i); return true; } void strlist_destroy(Strlist x) { for (size_t i = 0; i < x.size; ++i) { free(x.data[i]); } free(x.data); } ================================================ FILE: plugins/fs/strlist.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef strlist_h_ #define strlist_h_ #include #include typedef struct { char **data; size_t size; } Strlist; Strlist strlist_new(void); bool strlist_push(Strlist *x, const char *s); bool strlist_remove(Strlist *x, const char *s); void strlist_destroy(Strlist x); #endif ================================================ FILE: plugins/imap/CMakeLists.txt ================================================ install (FILES imap.lua DESTINATION ${LUA_PLUGINS_DIR}) luastatus_add_man_page (README.rst luastatus-plugin-imap 7) ================================================ FILE: plugins/imap/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-imap .. :X-man-page-only: ##################### .. :X-man-page-only: .. :X-man-page-only: ########################### .. :X-man-page-only: IMAPv4 plugin for luastatus .. :X-man-page-only: ########################### .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This derived plugin monitors the number of unread mails in an IMAP mailbox. Functions ========= * ``widget(tbl)`` Constructs a ``widget`` table required by luastatus. ``tbl`` is a table with the following fields: **(required)** - ``cb``: function The callback that will be called with the number of unread mails, or with ``nil`` if it is unknown. - ``host``: string Host name. - ``port``: number Port number. - ``login``, ``password``: strings Your credentials. - ``mailbox``: string Mailbox name (typically ``"Inbox"``). - ``error_sleep_period``: number Number of seconds to sleep after an error. **(optional)** - ``use_ssl``: boolean Whether to use SSL (you probably should set it to ``true``). Defaults to false. - ``verbose``: boolean Whether to be verbose (useful for troubleshooting). Default to false. - ``timeout``: number Idle timeout, in seconds, after which to reconnect; you probably want to set this to something like ``5 * 60``. - ``handshake_timeout``: number SSL handshake timeout, in seconds. - ``event`` The ``event`` entry of the resulting table (see ``luastatus`` documentation for the description of ``widget.event`` field). ================================================ FILE: plugins/imap/imap.lua ================================================ --[[ Copyright (C) 2015-2025 luastatus developers This file is part of luastatus. luastatus is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. luastatus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with luastatus. If not, see . --]] local socket = require 'socket' local ssl = require 'ssl' local log = print ---------------------------------------------------------------------------------------------------- local IMAP_TIMEOUT_ERROR = {} local IMAP = {} local function safely_wrap_ssl(conn) local new_conn local conn_to_close = conn local is_ok, err = pcall(function() new_conn = assert(ssl.wrap(conn, { mode = 'client', protocol = 'tlsv1', cafile = '/etc/ssl/certs/ca-certificates.crt', verify = 'peer', options = 'all', })) conn_to_close = new_conn assert(new_conn:dohandshake()) end) if not is_ok then conn_to_close:close() error(err) end return new_conn end function IMAP:open(host, port, params) local conn = socket.tcp() conn:connect(host, port) conn:settimeout(params.handshake_timeout) if params.use_ssl then conn = safely_wrap_ssl(conn) end conn:settimeout(params.timeout) self.__index = self return setmetatable({ conn = conn, last_id = 1, verbose = params.verbose, }, self) end function IMAP:receive() local line, err = self.conn:receive() if line == nil then error(err == 'wantread' and IMAP_TIMEOUT_ERROR or err) end if self.verbose then log('<', line) end return line end function IMAP:send(str) if self.verbose then log('>', str) end local data = str .. '\r\n' local nsent = 0 while nsent ~= #data do nsent = assert(self.conn:send(data, nsent + 1)) end end function IMAP:command(query, ondata) local id = string.format('a%04d', self.last_id) self.last_id = self.last_id + 1 local pattern = '^' .. id .. ' (%w+)' self:send(id .. ' ' .. query) while true do local line = self:receive() local resp = line:match(pattern) if resp ~= nil then return resp == 'OK', line end if ondata ~= nil then ondata(line) end end end function IMAP:close() self.conn:close() end ---------------------------------------------------------------------------------------------------- local P = {} function P.widget(tbl) local error_sleep_period = tbl.error_sleep_period -- to prevent busy-looping if an invalid value is passed to /luastatus.plugin.push_period/. assert(type(error_sleep_period) == 'number' and error_sleep_period > 0, 'invalid "error_sleep_period" value') local mbox = nil local function connect() mbox = IMAP:open(tbl.host, tbl.port, { use_ssl = tbl.use_ssl, timeout = tbl.timeout, handshake_timeout = tbl.handshake_timeout, verbose = tbl.verbose, }) assert(mbox:command(string.format('LOGIN %s %s', tbl.login, tbl.password))) assert(mbox:command('SELECT ' .. tbl.mailbox)) end local function get_unseen() local unseen repeat local finish = true assert(mbox:command('STATUS ' .. tbl.mailbox .. ' (UNSEEN)', function(line) if not line:match('^%*') then return end local m = line:match('^%* STATUS .* %(UNSEEN (%d+)%)%s*$') if m then unseen = m else finish = false end end)) until finish assert(unseen ~= nil) return tonumber(unseen) end local function idle() local done_sent = false assert(mbox:command('IDLE', function(line) if not done_sent and line:match('^%*') then mbox:send('DONE') done_sent = true end end)) end local function iteration() if mbox == nil then connect() else idle() end return get_unseen() end local last_content = tbl.cb(nil) return { plugin = 'timer', opts = {period = 0}, cb = function() local is_ok, obj = pcall(iteration) if is_ok then last_content = tbl.cb(obj) return last_content end if mbox ~= nil then mbox:close() mbox = nil end if obj == IMAP_TIMEOUT_ERROR then return last_content else if tbl.verbose then log('!', obj) end luastatus.plugin.push_period(error_sleep_period) last_content = tbl.cb(nil) return last_content end end, event = tbl.event, } end return P ================================================ FILE: plugins/inotify/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_plugin ( plugin-inotify $ $ $ ${sources} ) include (CheckSymbolExists) check_symbol_exists (inotify_init1 "sys/inotify.h" HAVE_INOTIFY_INIT1) configure_file ("probes.in.h" "probes.generated.h") target_compile_definitions (plugin-inotify PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-inotify LUA) target_include_directories (plugin-inotify PUBLIC "${PROJECT_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}") luastatus_add_man_page (README.rst luastatus-plugin-inotify 7) ================================================ FILE: plugins/inotify/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-inotify .. :X-man-page-only: ######################## .. :X-man-page-only: .. :X-man-page-only: ############################ .. :X-man-page-only: inotify plugin for luastatus .. :X-man-page-only: ############################ .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This plugin monitors inotify events. Options ======= * ``watch``: table A table in which keys are the paths to watch and values are the tables with even names, for example, ``{["/home/user"] = {"create", "delete", "move"}}`` (see the `Events and flag names`_ section). * ``greet``: boolean Whether or not to call ``cb`` with ``what="hello"`` as soon as the widget starts. Defaults to false. * ``timeout``: number If specified and not negative, this plugin calls ``cb`` with ``what="timeout"`` if no event has occurred in ``timeout`` seconds. ``cb`` argument =============== A table with ``what`` entry. * If ``what`` is ``"hello"``, the function is being called for the first time (and the ``greet`` option was set to ``true``). * If ``what`` is ``"timeout"``, the function has not been called for the number of seconds specified as the ``timeout`` option. * If ``what`` is ``"event"``, an inotify event has been read; in this case, the table has the following additional entries: - ``wd``: integer Watch descriptor (see the `Functions`_ section) of the event. - ``mask``: table For each event name or event flag (see the `Events and flag names`_ section), this table contains an entry with key equal to its name and ``true`` value. - ``cookie``: number Unique cookie associating related events (or, if there are no associated related events, a zero). - ``name``: string (optional) Present only when an event is returned for a file inside a watched directory; identifies the filename within the watched directory. Functions ========= Each file being watched is assigned a *watch descriptor*, which is a non-negative integer. * ``wds = luastatus.plugin.get_initial_wds()`` Returns a table that maps *initial* paths to their watch descriptors. * ``wd = luastatus.plugin.add_watch(path, events)`` Add a new file to watch. Returns a watch descriptor on success, or ``nil`` on failure. * ``is_ok = luastatus.plugin.remove_watch(wd)`` Removes a watch by its watch descriptor. Returns ``true`` on success, or ``false`` on failure. * ``tbl = luastatus.plugin.get_supported_events()`` Returns a table with all events supported by the plugin. In this table, keys are events names, values are strings which represent the mode of the event. The value is ``"i"`` for an input-only event (can only be listened to, never occurs in a returned event), ``"o"`` for an output-only event (only occurs in a returned event, cannot be listened to), ``"io"`` for an event that can be both listened to and occur in a returned event. This set depends on the version of glibc that the plugin has been compiled with, which is something a widget should not be required to know. By calling this function, the widget can query which events are supported by the plugin. Testing which events are actually supported by the Linux kernel that the system is running is a harder problem. The ``inotify(7)`` man page gives kernel versions in which certain events were introduced (e.g. ``IN_DONT_FOLLOW (since Linux 2.6.15)``). One should probably parse the output of ``uname -r`` command or the contents of ``/proc/sys/kernel/osrelease`` file in order to check for the version of the kernel. * ``luastatus.plugin.push_timeout(seconds)`` Changes the timeout for one iteration. The following functions are provided as a part of "procalive" function set. These functions are available in plugins, including this one, that can be used to watch the state of some process(es): * ``is_ok, err_msg = luastatus.plugin.access(path)`` Checks if a given path exists, as if with ``access(path, F_OK)``. If it does exist, returns ``true, nil``. If it does not, returns ``false, nil``. If an error occurs, returns ``false, err_msg``. * ``file_type, err_msg = luastatus.plugin.stat(path)`` Tries to get the type of the file at the given path. On success returns either of: ``"regular"``, ``"dir"`` (directory), ``"chardev"`` (character device), ``"blockdev"`` (block device), ``"fifo"``, ``"symlink"``, ``"socket"``, ``"other"``. On failure returns ``nil, err_msg``. * ``arr, err_msg = luastatus.plugin.glob(pattern)`` Performs glob expansion of ``pattern``. A glob is a wildcard pattern like ``/tmp/*.txt`` that can be applied as a filter to a list of existing file names. Supported expansions are ``*``, ``?`` and ``[...]``. Please refer to ``glob(7)`` for more information on wildcard patterns. Note also that the globbing is performed with ``GLOB_MARK`` flag, so that in output, directories have trailing slash appended to their name. Returns ``arr, nil`` on success, where ``arr`` is an array of strings; these are existing file names that matched the given pattern. The order is arbitrary. On failure, returns ``nil, err_msg``. * ``is_alive = luastatus.plugin.is_process_alive(pid)`` Checks if a process with PID ``pid`` is currently alive. ``pid`` must be either a number or a string. Returns a boolean that indicates whether the process is alive. Events and flag names ===================== Each ``IN_*`` constant defined in ```` corresponds to a string obtained from its name by dropping the initial ``IN_`` and making the rest lower-case, e.g. ``IN_CLOSE_WRITE`` corresponds to ``"close_write"``. See ``inotify(7)`` for details. ================================================ FILE: plugins/inotify/inotify.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "libmoonvisit/moonvisit.h" #include "libls/ls_algo.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_evloop_lfuncs.h" #include "libls/ls_io_utils.h" #include "libls/ls_time_utils.h" #include "libprocalive/procalive_lfuncs.h" #include "inotify_compat.h" typedef struct { char *path; int wd; } Watch; typedef struct { Watch *data; size_t size; size_t capacity; } WatchList; static inline WatchList watch_list_new(void) { return (WatchList) {NULL, 0, 0}; } static inline void watch_list_add(WatchList *x, const char *path, int wd) { if (x->size == x->capacity) { x->data = LS_M_X2REALLOC(x->data, &x->capacity); } x->data[x->size++] = (Watch) {ls_xstrdup(path), wd}; } static inline void watch_list_free(WatchList *x) { for (size_t i = 0; i < x->size; ++i) free(x->data[i].path); free(x->data); } typedef struct { int fd; WatchList init_watch; bool greet; double tmo; LS_PushedTimeout pushed_tmo; } Priv; static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; ls_close(p->fd); watch_list_free(&p->init_watch); ls_pushed_timeout_destroy(&p->pushed_tmo); free(p); } typedef struct { uint32_t mask; bool in; bool out; const char *name; } EventType; static const EventType EVENT_TYPES[] = { {IN_ACCESS, true, true, "access"}, {IN_ATTRIB, true, true, "attrib"}, {IN_CLOSE_WRITE, true, true, "close_write"}, {IN_CLOSE_NOWRITE, true, true, "close_nowrite"}, {IN_CREATE, true, true, "create"}, {IN_DELETE, true, true, "delete"}, {IN_DELETE_SELF, true, true, "delete_self"}, {IN_MODIFY, true, true, "modify"}, {IN_MOVE_SELF, true, true, "move_self"}, {IN_MOVED_FROM, true, true, "moved_from"}, {IN_MOVED_TO, true, true, "moved_to"}, {IN_OPEN, true, true, "open"}, {IN_ALL_EVENTS, true, false, "all_events"}, {IN_MOVE, true, false, "move"}, {IN_CLOSE, true, false, "close"}, #ifdef IN_DONT_FOLLOW {IN_DONT_FOLLOW, true, false, "dont_follow"}, #endif #ifdef IN_EXCL_UNLINK {IN_EXCL_UNLINK, true, false, "excl_unlink"}, #endif {IN_MASK_ADD, true, false, "mask_add"}, {IN_ONESHOT, true, false, "oneshot"}, #ifdef IN_ONLYDIR {IN_ONLYDIR, true, false, "onlydir"}, #endif #ifdef IN_MASK_CREATE {IN_MASK_CREATE, true, false, "mask_create"}, #endif {IN_IGNORED, false, true, "ignored"}, {IN_ISDIR, false, true, "isdir"}, {IN_Q_OVERFLOW, false, true, "q_overflow"}, {IN_UNMOUNT, false, true, "unmount"}, }; enum { EVENT_TYPES_NUM = LS_ARRAY_SIZE(EVENT_TYPES) }; #define EVENT_TYPES_END (EVENT_TYPES + EVENT_TYPES_NUM) static int parse_evlist_elem(MoonVisit *mv, void *ud, int kpos, int vpos) { mv->where = "element of event names list"; (void) kpos; uint32_t *mask = ud; if (moon_visit_checktype_at(mv, NULL, vpos, LUA_TSTRING) < 0) goto error; const char *s = lua_tostring(mv->L, vpos); for (const EventType *et = EVENT_TYPES; et != EVENT_TYPES_END; ++et) { if (et->in && strcmp(et->name, s) == 0) { *mask |= et->mask; return 1; } } moon_visit_err(mv, "unknown input event '%s'", s); error: return -1; } static int parse_watch_entry(MoonVisit *mv, void *ud, int kpos, int vpos) { mv->where = "'watch' entry"; LuastatusPluginData *pd = ud; Priv *p = pd->priv; // Parse key if (moon_visit_checktype_at(mv, "key", kpos, LUA_TSTRING) < 0) goto error; const char *path = lua_tostring(mv->L, kpos); // Parse value uint32_t mask = 0; if (moon_visit_table_f_at(mv, "value", vpos, parse_evlist_elem, &mask) < 0) goto error; // Add watch int wd = inotify_add_watch(p->fd, path, mask); if (wd < 0) { LS_ERRF(pd, "inotify_add_watch: %s: %s", path, ls_tls_strerror(errno)); } else { watch_list_add(&p->init_watch, path, wd); } return 1; error: return -1; } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .fd = -1, .init_watch = watch_list_new(), .greet = false, .tmo = -1, }; ls_pushed_timeout_init(&p->pushed_tmo); char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; if ((p->fd = compat_inotify_init(false, true)) < 0) { LS_FATALF(pd, "inotify_init: %s", ls_tls_strerror(errno)); goto error; } // Parse greet if (moon_visit_bool(&mv, -1, "greet", &p->greet, true) < 0) goto mverror; // Parse timeout if (moon_visit_num(&mv, -1, "timeout", &p->tmo, true) < 0) goto mverror; // Parse watch if (moon_visit_table_f(&mv, -1, "watch", parse_watch_entry, pd, false) < 0) goto mverror; return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); error: destroy(pd); return LUASTATUS_ERR; } static int l_add_watch(lua_State *L) { char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Check we have at least two arguments luaL_checkany(L, 1); luaL_checkany(L, 2); // Parse first arg if (moon_visit_checktype_at(&mv, "argument #1", 1, LUA_TSTRING) < 0) goto mverror; const char *path = lua_tostring(L, 1); // Parse second arg uint32_t mask = 0; if (moon_visit_table_f_at(&mv, "argument #2", 2, parse_evlist_elem, &mask) < 0) goto mverror; // Add watch LuastatusPluginData *pd = lua_touserdata(L, lua_upvalueindex(1)); Priv *p = pd->priv; int wd = inotify_add_watch(p->fd, path, mask); if (wd < 0) { LS_ERRF(pd, "inotify_add_watch: %s: %s", path, ls_tls_strerror(errno)); lua_pushnil(L); } else { lua_pushinteger(L, wd); } return 1; mverror: return luaL_error(L, "%s", errbuf); } static int l_remove_watch(lua_State *L) { int wd = luaL_checkinteger(L, 1); LuastatusPluginData *pd = lua_touserdata(L, lua_upvalueindex(1)); Priv *p = pd->priv; if (inotify_rm_watch(p->fd, wd) < 0) { LS_ERRF(pd, "inotify_rm_watch: %d: %s", wd, ls_tls_strerror(errno)); lua_pushboolean(L, false); } else { lua_pushboolean(L, true); } return 1; } static int l_get_initial_wds(lua_State *L) { LuastatusPluginData *pd = lua_touserdata(L, lua_upvalueindex(1)); Priv *p = pd->priv; lua_newtable(L); // L: table for (size_t i = 0; i < p->init_watch.size; ++i) { lua_pushinteger(L, p->init_watch.data[i].wd); // L: table wd lua_setfield(L, -2, p->init_watch.data[i].path); // L: table } return 1; } static int l_get_supported_events(lua_State *L) { lua_createtable(L, 0, EVENT_TYPES_NUM); // L: table for (const EventType *et = EVENT_TYPES; et != EVENT_TYPES_END; ++et) { const char *v = et->in ? (et->out ? "io" : "i") : "o"; lua_pushstring(L, v); // L: table v lua_setfield(L, -2, et->name); // L: table } return 1; } static void register_funcs(LuastatusPluginData *pd, lua_State *L) { // L: table lua_pushlightuserdata(L, pd); // L: table pd lua_pushcclosure(L, l_add_watch, 1); // L: table closure lua_setfield(L, -2, "add_watch"); // L: table // L: table lua_pushlightuserdata(L, pd); // L: table pd lua_pushcclosure(L, l_remove_watch, 1); // L: table closure lua_setfield(L, -2, "remove_watch"); // L: table // L: table lua_pushlightuserdata(L, pd); // L: table pd lua_pushcclosure(L, l_get_initial_wds, 1); // L: table closure lua_setfield(L, -2, "get_initial_wds"); // L: table // L: table lua_pushcfunction(L, l_get_supported_events); // L: table func lua_setfield(L, -2, "get_supported_events"); // L: table procalive_lfuncs_register_all(L); // L: table Priv *p = pd->priv; // L: table ls_pushed_timeout_push_luafunc(&p->pushed_tmo, L); // L: table func lua_setfield(L, -2, "push_timeout"); // L: table } static void push_event(lua_State *L, const struct inotify_event *event) { // L: - lua_createtable(L, 0, 4); // L: table lua_pushstring(L, "event"); // L: table string lua_setfield(L, -2, "what"); // L: table lua_pushinteger(L, event->wd); // L: table wd lua_setfield(L, -2, "wd"); // L: table lua_newtable(L); // L: table table for (const EventType *et = EVENT_TYPES; et != EVENT_TYPES_END; ++et) { if (et->out && (event->mask & et->mask)) { lua_pushboolean(L, true); // L: table table true lua_setfield(L, -2, et->name); // L: table table } } lua_setfield(L, -2, "mask"); // L: table lua_pushnumber(L, event->cookie); // L: table cookie lua_setfield(L, -2, "cookie"); // L: table if (event->len) { lua_pushstring(L, event->name); // L: table name lua_setfield(L, -2, "name"); // L: table } } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { Priv *p = pd->priv; // We allocate the buffer for /struct inotify_event/'s on the heap rather than on the stack in // order to get the maximum possible alignment for it and not resort to compiler-dependent hacks // like this one recommended by inotify(7): // /__attribute__ ((aligned(__alignof__(struct inotify_event))))/. enum { NBUF = sizeof(struct inotify_event) + NAME_MAX + 2 }; char *buf = LS_XNEW(char, NBUF); if (p->greet) { lua_State *L = funcs.call_begin(pd->userdata); lua_createtable(L, 0, 1); // L: table lua_pushstring(L, "hello"); // L: table string lua_setfield(L, -2, "what"); // L: table funcs.call_end(pd->userdata); } LS_TimeDelta default_tmo = ls_double_to_TD(p->tmo, LS_TD_FOREVER); while (1) { LS_TimeDelta TD = ls_pushed_timeout_fetch(&p->pushed_tmo, default_tmo); int nfds = ls_wait_input_on_fd(p->fd, TD); if (nfds < 0) { LS_FATALF(pd, "ls_wait_input_on_fd: %s", ls_tls_strerror(errno)); goto error; } else if (nfds == 0) { lua_State *L = funcs.call_begin(pd->userdata); lua_createtable(L, 0, 1); // L: table lua_pushstring(L, "timeout"); // L: table string lua_setfield(L, -2, "what"); // L: table funcs.call_end(pd->userdata); } else { ssize_t r = read(p->fd, buf, NBUF); if (r < 0) { if (errno == EINTR) { continue; } LS_FATALF(pd, "read: %s", ls_tls_strerror(errno)); goto error; } else if (r == 0) { LS_FATALF(pd, "read() from the inotify file descriptor returned 0"); goto error; } else if (r == NBUF) { LS_FATALF(pd, "got an event with filename length > NAME_MAX+1"); goto error; } const struct inotify_event *event; for (char *ptr = buf; ptr < buf + r; ptr += sizeof(struct inotify_event) + event->len) { event = (const struct inotify_event *) ptr; push_event(funcs.call_begin(pd->userdata), event); funcs.call_end(pd->userdata); } } } error: free(buf); } LuastatusPluginIface luastatus_plugin_iface_v1 = { .init = init, .register_funcs = register_funcs, .run = run, .destroy = destroy, }; ================================================ FILE: plugins/inotify/inotify_compat.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef inotify_compat_h_ #define inotify_compat_h_ #include #include #include #include "libls/ls_io_utils.h" #include "libls/ls_compdep.h" #include "probes.generated.h" LS_INHEADER int compat_inotify_init(bool nonblock, bool cloexec) { #if HAVE_INOTIFY_INIT1 return inotify_init1((nonblock ? IN_NONBLOCK : 0) | (cloexec ? IN_CLOEXEC : 0)); #else int fd = inotify_init(); if (fd < 0) { return -1; } if (nonblock) { ls_make_nonblock(fd); } if (cloexec) { ls_make_cloexec(fd); } return fd; #endif } #endif ================================================ FILE: plugins/inotify/probes.in.h ================================================ #ifndef luastatus_plugin_inotify_probes_h_ #define luastatus_plugin_inotify_probes_h_ #cmakedefine01 HAVE_INOTIFY_INIT1 #endif ================================================ FILE: plugins/is-program-running/CMakeLists.txt ================================================ install (FILES is-program-running.lua DESTINATION ${LUA_PLUGINS_DIR}) luastatus_add_man_page (README.rst luastatus-plugin-is-program-running 7) ================================================ FILE: plugins/is-program-running/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-is-program-running .. :X-man-page-only: ################################### .. :X-man-page-only: .. :X-man-page-only: ######################################################### .. :X-man-page-only: Plugin for luastatus which checks if a program is running .. :X-man-page-only: ######################################################### .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This derived plugin checks whether a certain program is running, although in order to this plugin to work, the program should indicate its state via one of the following widespread mechanisms: 1. PID file; 2. Creation of a file. The file must have a fixed path; 3. Creation of a file (with any name) in an otherwise-empty directory. The directory must have a fixed path. Functions ========= The following functions are provided: * ``make_watcher(kind, path)`` Returns a watcher; a watcher is a function that takes no argument and returns a single boolean value indicating whether the program is running. Kind must be either ``"pidfile"``, ``"file_exists"``, ``"dir_nonempty"`` or ``"dir_nonempty_with_hidden"``: * ``"pidfile"`` means check by PID file located at path ``path``; * ``"file_exists"`` means check by existence of a file or directory at path ``path``; * ``"dir_nonempty"`` means check by whether a directory located at path ``path`` is non-empty (**excluding** hidden files); * ``"dir_nonempty_with_hidden"`` means check by whether a directory located at path ``path`` is non-empty (**including** hidden files). * ``widget(tbl)`` Constructs a ``widget`` table required by luastatus. ``tbl`` is a table with the following fields: **(required)** - ``cb``: a function The callback function that will be called either with a boolean (if ``many`` is not specified) or a table (if ``many`` is specified). **(exactly one of the following is required)** - ``kind`` and ``path``: strings Please refer to the documentation for the ``make_watcher`` function above for semantics of ``kind`` and ``path``. If ``kind`` and ``path`` are specified, the plugin will only monitor state of a single program, and ``cb`` will be called with a boolean argument. - ``many``: array If specified, the plugin will monitor states of N programs, where N is the length of the ``many`` array. The array should contain tables (each being a watcher specification) with the following entries: * ``id``: string A string used to identify this entry in the ``many`` array; * ``kind`` and ``path``: strings Please refer to the documentation for the ``make_watcher`` function above for semantics of ``kind`` and ``path``. If ``many`` is specified, ``cb`` will be called with a table argument in which keys are identification strings (``id`` entry in the watcher specification; see above), and values are booleans indicating whether the corresponding process is running. **(optional)** - ``timer_opts`` Options to pass to the underlying ``timer`` plugin. Note that this includes the period with which this plugin will check if the program(s) is running. The period of the ``timer`` plugin defaults to 1 second. - ``event`` The ``event`` entry of the resulting table (see ``luastatus`` documentation for the description of ``widget.event`` field). ================================================ FILE: plugins/is-program-running/is-program-running.lua ================================================ --[[ Copyright (C) 2015-2025 luastatus developers This file is part of luastatus. luastatus is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. luastatus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with luastatus. If not, see . --]] local P = {} local function check_glob(pattern, with_hidden) local res = luastatus.plugin.glob(pattern) if (not res) or (#res == 0) then return false end if not with_hidden then return true end for _, s in ipairs(res) do -- Check if 's' ends with either "/./" or "/../" to skip "." and ".." -- entries that get appended if the glob ends with ".*". if not s:match('/%.%.?/?$') then return true end end return false end function P.make_watcher(kind, path) assert(type(kind) == 'string') assert(type(path) == 'string') if kind == 'pidfile' then return function() local f = io.open(path, 'r') if not f then return false end local pid = f:read('*line') f:close() if not pid then return false end local is_ok, is_alive = pcall(luastatus.plugin.is_process_alive, pid) return is_ok and is_alive end elseif kind == 'file_exists' then return function() return luastatus.plugin.access(path) end elseif kind == 'dir_nonempty' then return function() return check_glob(path .. '/*') end elseif kind == 'dir_nonempty_with_hidden' then return function() return check_glob(path .. '/*') or check_glob(path .. '/.*', true) end else error(string.format('unknown kind "%s"', kind)) end end function P.widget(tbl) local watcher_ids local watchers local single_watcher if tbl.many then assert(P.kind == nil) assert(P.path == nil) watchers = {} watcher_ids = {} for _, elem in ipairs(tbl.many) do table.insert(watcher_ids, elem.id) table.insert(watchers, P.make_watcher(elem.knid, elem.path)) end else single_watcher = P.make_watcher(tbl.kind, tbl.path) end return { plugin = 'timer', opts = tbl.timer_opts, cb = function() if single_watcher then return tbl.cb(single_watcher()) else local t = {} for i, id in ipairs(watcher_ids) do local watcher = watchers[i] t[id] = watcher() end return tbl.cb(t) end end, event = tbl.event, } end return P ================================================ FILE: plugins/mem-usage-linux/CMakeLists.txt ================================================ install (FILES mem-usage-linux.lua DESTINATION ${LUA_PLUGINS_DIR}) luastatus_add_man_page (README.rst luastatus-plugin-mem-usage-linux 7) ================================================ FILE: plugins/mem-usage-linux/README.rst ================================================ .. :X-man-page-only: luastatus-mem-usage-linux .. :X-man-page-only: ######################### .. :X-man-page-only: .. :X-man-page-only: ################################################ .. :X-man-page-only: Linux-specific memory usage plugin for luastatus .. :X-man-page-only: ################################################ .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This derived plugin periodically polls Linux ``procfs`` for memory usage. Functions ========= * ``get_usage()`` Returns a table with two entries, ``avail`` and ``total``. Both are tables that have ``value`` (a number) and ``unit`` (a string) entries; you can assume ``unit`` is always ``"kB"``. * ``widget(tbl)`` Constructs a ``widget`` table required by luastatus. ``tbl`` is a table with the following fields: **(required)** - ``cb``: function The callback that will be called with a table, which the same as one returned by ``get_usage()``. **(optional)** - ``timer_opts``: table Options for the underlying ``timer`` plugin. - ``event`` The ``event`` entry of the resulting table (see ``luastatus`` documentation for the description of ``widget.event`` field). ================================================ FILE: plugins/mem-usage-linux/mem-usage-linux.lua ================================================ --[[ Copyright (C) 2015-2025 luastatus developers This file is part of luastatus. luastatus is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. luastatus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with luastatus. If not, see . --]] local P = {} local DEFAULT_PROCPATH = '/proc' local function do_with_file(f, callback) local is_ok, err = pcall(callback) f:close() if not is_ok then error(err) end end local function get_usage_impl(procpath) local f = assert(io.open(procpath .. '/meminfo', 'r')) local r = {} do_with_file(f, function() for line in f:lines() do local key, value, unit = line:match('(%w+):%s+(%w+)%s+(%w+)') if key == 'MemTotal' then r.total = {value = assert(tonumber(value)), unit = unit} elseif key == 'MemAvailable' then r.avail = {value = assert(tonumber(value)), unit = unit} end end end) return r end function P.get_usage() return get_usage_impl(DEFAULT_PROCPATH) end function P.widget(tbl) local procpath = tbl._procpath or DEFAULT_PROCPATH return { plugin = 'timer', opts = tbl.timer_opts, cb = function() return tbl.cb(get_usage_impl(procpath)) end, event = tbl.event, } end return P ================================================ FILE: plugins/mpd/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_plugin ( plugin-mpd $ $ $ ${sources} ) target_compile_definitions (plugin-mpd PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-mpd LUA) target_include_directories (plugin-mpd PUBLIC "${PROJECT_SOURCE_DIR}") luastatus_add_man_page (README.rst luastatus-plugin-mpd 7) ================================================ FILE: plugins/mpd/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-mpd .. :X-man-page-only: ##################### .. :X-man-page-only: .. :X-man-page-only: ######################## .. :X-man-page-only: mpd plugin for luastatus .. :X-man-page-only: ######################## .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This plugin monitors state of an mpd server. Options ======= * ``hostname``: string Hostname to connect to. Default is to connect to the local host. An absolute path to a UNIX domain socket can also be specified (``port`` and ``bind`` are ignored then). * ``port``: number Port to connect to. Default is 6600. * ``password``: string Server's password. * ``timeout``: number If specified and not negative, the number of seconds to wait before calling ``cb`` with ``what="timeout"`` again (after a connection has been established). May be fractional. * ``retry_in``: number Number of seconds to retry in after the connection is lost. A negative value means do not retry and return immediately. May be fractional. Defaults to 10. * ``retry_fifo``: string Path to an existent FIFO. The plugin does not create FIFO itself. To force a reconnect, ``touch(1)`` the FIFO, that is, open it for writing and then close. * ``events``: array of strings List of MPD subsystems to poll for changes in. See the description of ``idle`` command at https://www.musicpd.org/doc/html/protocol.html#querying-mpd-s-status for the complete list. Default is ``{"mixer","player"}``. * ``enable_tcp_keepalive``: bool Whether or not to enable TCP keepalive. Defaults to ``false``. This option is ignored if the plugin is configured to connect to a UNIX domain socket. * ``bind``: table If provided, the plugin will bind the (TCP, not UNIX) socket to a specific address. The parameters for the binding are specified by this table. If the plugin is otherwise configured to connect to a UNIX domain socket (via ``hostname`` option that starts with a slash), this option is ignored. If this option is provided, it must be a table with the following keys: - ``addr``: the value must be a string representing either IPv4 or IPv6 address. - ``ipver``: the value must be a string, either ``"ipv4"`` for IPv4 address or ``"ipv6"`` for IPv6 address. Note that there is no default for the ``ipver`` field, and there is no guessing what sort of address it is; if the ``bind`` table is present, but does not contain ``ipver`` key, the plugin will fail to initialize. ``cb`` argument =============== A table with ``what`` entry. * If ``what`` is ``"connecting"``, the plugin is now connecting to the server. * If ``what`` is ``"update"``, either the plugin has just connected to the server and queried its state, or the server has just changed its state. Additionally, the following entries are provided: - ``song``: table with server's response to the ``currentsong`` command. Surprisingly, it is not documented at all, so here is an example (all values are strings): .. rst2man does not support tables with headers, so let's just use bold. +----------------------+-----------------------------+ | **Key** | **Value** | +----------------------+-----------------------------+ | file | Sensou to Heiwa.mp3 | +----------------------+-----------------------------+ | Last-Modified | 2016-07-31T09:56:31Z | +----------------------+-----------------------------+ | Artist | ALI PROJECT | +----------------------+-----------------------------+ | AlbumArtist | ALI PROJECT | +----------------------+-----------------------------+ | Title | 戦争と平和 | +----------------------+-----------------------------+ | Album | Erotic & Heretic | +----------------------+-----------------------------+ | Track | 8 | +----------------------+-----------------------------+ | Date | 2002-02-20 | +----------------------+-----------------------------+ | Genre | J-Pop | +----------------------+-----------------------------+ | Disc | 1/1 | +----------------------+-----------------------------+ | Time | 260 | +----------------------+-----------------------------+ | Pos | 0 | +----------------------+-----------------------------+ | Id | 4 | +----------------------+-----------------------------+ - ``status``: table with server's response to the ``status`` command. See the ``status`` command description at https://www.musicpd.org/doc/html/protocol.html#querying-mpd-s-status. All values are strings. * It ``what`` is ``"timeout"``, the server hasn't changed its state for the number of seconds specified as the ``timeout`` option. * If ``what`` is ``"error"``, the connection has been lost; the plugin is now going to sleep and try to reconnect. This is only reported if ``retry_in`` is non-negative. ================================================ FILE: plugins/mpd/connect.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "connect.h" #include #include #include #include #include #include #include #include #include #include "include/plugin_data_v1.h" #include "include/sayf_macros.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_osdep.h" #include "libls/ls_io_utils.h" #include "libls/ls_panic.h" int unixdom_open(LuastatusPluginData *pd, const char *path) { LS_ASSERT(path != NULL); int fd = -1; struct sockaddr_un saun = {.sun_family = AF_UNIX}; size_t npath = strlen(path); if (npath + 1 > sizeof(saun.sun_path)) { LS_ERRF(pd, "socket path is too long: %s", path); goto error; } memcpy(saun.sun_path, path, npath + 1); fd = ls_cloexec_socket(PF_UNIX, SOCK_STREAM, 0); if (fd < 0) { LS_ERRF(pd, "socket: %s", ls_tls_strerror(errno)); goto error; } if (connect(fd, (struct sockaddr *) &saun, sizeof(saun)) < 0) { LS_ERRF(pd, "connect: %s: %s", path, ls_tls_strerror(errno)); goto error; } return fd; error: ls_close(fd); return -1; } static int do_bind_to_addr(int fd, const char *addr_str, BindAddrFamily family) { switch (family) { case FAMILY_NONE: return 0; case FAMILY_IPV4: { LS_ASSERT(addr_str != NULL); struct sockaddr_in sa = { .sin_family = AF_INET, }; if (!inet_pton(AF_INET, addr_str, &sa.sin_addr)) { goto bad_str; } return bind(fd, (struct sockaddr *) &sa, sizeof(sa)); } case FAMILY_IPV6: { LS_ASSERT(addr_str != NULL); struct sockaddr_in6 sa = { .sin6_family = AF_INET6, }; if (!inet_pton(AF_INET6, addr_str, &sa.sin6_addr)) { goto bad_str; } return bind(fd, (struct sockaddr *) &sa, sizeof(sa)); } } LS_MUST_BE_UNREACHABLE(); bad_str: errno = EINVAL; return -1; } static int bind_addr_family2af(BindAddrFamily family) { switch (family) { case FAMILY_NONE: return AF_UNSPEC; case FAMILY_IPV4: return AF_INET; case FAMILY_IPV6: return AF_INET6; } LS_MUST_BE_UNREACHABLE(); } int inetdom_open( LuastatusPluginData *pd, const char *hostname, const char *service, const char *bind_addr, BindAddrFamily bind_addr_family) { LS_ASSERT(service != NULL); struct addrinfo *ai = NULL; int fd = -1; int af = bind_addr_family2af(bind_addr_family); struct addrinfo hints = { .ai_family = af, .ai_socktype = SOCK_STREAM, .ai_protocol = 0, .ai_flags = AI_ADDRCONFIG, }; int gai_r = getaddrinfo(hostname, service, &hints, &ai); if (gai_r) { if (gai_r == EAI_SYSTEM) { LS_ERRF(pd, "getaddrinfo: %s", ls_tls_strerror(errno)); } else { LS_ERRF(pd, "getaddrinfo: %s", gai_strerror(gai_r)); } ai = NULL; goto cleanup; } for (struct addrinfo *pai = ai; pai; pai = pai->ai_next) { fd = ls_cloexec_socket(pai->ai_family, pai->ai_socktype, pai->ai_protocol); if (fd < 0) { LS_WARNF(pd, "(candiate) socket: %s", ls_tls_strerror(errno)); continue; } if (do_bind_to_addr(fd, bind_addr, bind_addr_family) < 0) { LS_WARNF(pd, "(candiate) cannot bind to address: %s", ls_tls_strerror(errno)); close(fd); fd = -1; continue; } if (connect(fd, pai->ai_addr, pai->ai_addrlen) < 0) { LS_WARNF(pd, "(candiate) connect: %s", ls_tls_strerror(errno)); close(fd); fd = -1; continue; } break; } if (fd < 0) { LS_ERRF(pd, "can't connect to any of the candidates"); } cleanup: if (ai) { freeaddrinfo(ai); } return fd; } ================================================ FILE: plugins/mpd/connect.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef connect_h_ #define connect_h_ #include "include/plugin_data_v1.h" typedef enum { FAMILY_NONE, FAMILY_IPV4, FAMILY_IPV6, } BindAddrFamily; int unixdom_open(LuastatusPluginData *pd, const char *path); int inetdom_open( LuastatusPluginData *pd, const char *hostname, const char *service, const char *bind_addr, BindAddrFamily bind_addr_family); #endif ================================================ FILE: plugins/mpd/fuzz/.gitignore ================================================ harness_check_greeting harness_get_resp_type harness_write_quoted harness_append_kv findings_check_greeting findings_get_resp_type findings_write_quoted findings_append_kv ================================================ FILE: plugins/mpd/fuzz/build.sh ================================================ #!/bin/sh set -e if [ -z "$CC" ]; then echo >&2 "You must set the 'CC' environment variable." echo >&2 "Hint: you probably want to set 'CC' to 'some-directory/afl-gcc'." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. do_build_variant() { local extra_opts=$1 local output=$2 printf '%s\n' >&2 "Building with $extra_opts -> $output" $CC -Wall -Wextra -O3 -fsanitize=undefined -std=c99 -D_POSIX_C_SOURCE=200809L \ $extra_opts \ -I"$luastatus_root" \ ./harness.c \ ../safe_haven.c \ "$luastatus_root"/libls/ls_string.c \ "$luastatus_root"/libls/ls_alloc_utils.c \ "$luastatus_root"/libls/ls_panic.c \ "$luastatus_root"/libls/ls_cstring_utils.c \ "$luastatus_root"/libsafe/*.c \ -o "$output" } do_build_variant -DMODE_CHECK_GREETING=1 harness_check_greeting do_build_variant -DMODE_GET_RESP_TYPE=1 harness_get_resp_type do_build_variant -DMODE_WRITE_QUOTED=1 harness_write_quoted do_build_variant -DMODE_APPEND_KV=1 harness_append_kv ================================================ FILE: plugins/mpd/fuzz/clear.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" rm -rf \ ./findings_check_greeting \ ./findings_get_resp_type \ ./findings_write_quoted \ ./findings_append_kv ================================================ FILE: plugins/mpd/fuzz/fuzz.sh ================================================ #!/bin/sh set -e if [ -z "$XXX_AFL_DIR" ]; then echo >&2 "You must set the 'XXX_AFL_DIR' environment variable." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" case "$1" in 1) suffix=check_greeting ;; 2) suffix=get_resp_type ;; 3) suffix=write_quoted ;; 4) suffix=append_kv ;; *) printf '%s\n' >&2 "USAGE: $0 {1 | 2 | 3 | 4}" exit 2 ;; esac findings_dir=./findings_"$suffix" testcases_dir=./testcases_"$suffix" harness_binary=./harness_"$suffix" mkdir -p "$findings_dir" export UBSAN_OPTIONS=halt_on_error=1 export AFL_EXIT_WHEN_DONE=1 "$XXX_AFL_DIR"/afl-fuzz -i "$testcases_dir" -o "$findings_dir" -t 5 "$harness_binary" @@ ================================================ FILE: plugins/mpd/fuzz/fuzz_all.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" for i in 1 2 3 4; do ./fuzz.sh "$i" done ================================================ FILE: plugins/mpd/fuzz/gen_testcases.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. common_args_noab='--length=5-20 --random-seed=123' common_args="--a=1:a --b=1:b $common_args_noab" # check_greeting "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases_check_greeting \ $common_args \ --mut-prefix='OK MPD ' \ --num-files=10 \ --extra-testcase='pfx_only:OK MPD ' # get_resp_type: OK "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases_get_resp_type \ $common_args \ --mut-prefix='OK' \ --extra-testcase='pfx_only:OK' \ --num-files=5 \ --file-prefix='ok_' # get_resp_type: ACK "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases_get_resp_type \ $common_args \ --mut-prefix='ACK ' \ --num-files=5 \ --file-prefix='ack_' # get_resp_type: other "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases_get_resp_type \ $common_args \ --num-files=5 \ --file-prefix='other_' # write_quoted "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases_write_quoted \ $common_args_noab \ --a=1:'"' \ --b=1:xyz \ --a-is-important \ --num-files=10 # append_kv "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases_append_kv \ $common_args \ --mut-substrings='|: |:|' \ --num-files=10 ================================================ FILE: plugins/mpd/fuzz/harness.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include "libls/ls_strarr.h" #include "libsafe/safev.h" #include "fuzz_utils/fuzz_utils.h" #include "../safe_haven.h" #if MODE_CHECK_GREETING static void do_the_thing(SAFEV v) { char c = is_good_greeting(v); fuzz_utils_used(&c, 1); } #elif MODE_GET_RESP_TYPE static void do_the_thing(SAFEV v) { char c = response_type(v); fuzz_utils_used(&c, 1); } #elif MODE_WRITE_QUOTED static void do_the_thing(SAFEV v) { char *buf; size_t len; FILE *f = open_memstream(&buf, &len); if (!f) { perror("open_memstream"); abort(); } write_quoted(f, v); if (fclose(f) < 0) { perror("fclose"); abort(); } fuzz_utils_used(buf, len); free(buf); } #elif MODE_APPEND_KV static void do_the_thing(SAFEV v) { LS_StringArray sa = ls_strarr_new_reserve(1024, 2); append_line_to_kv_strarr(&sa, v); size_t n = ls_strarr_size(sa); for (size_t i = 0; i < n; ++i) { size_t ns; const char *s = ls_strarr_at(sa, i, &ns); fuzz_utils_used(s, ns); } ls_strarr_destroy(sa); } #else # error "Please define to 1 either: MODE_CHECK_GREETING || MODE_GET_RESP_TYPE || MODE_WRITE_QUOTED || MODE_APPEND_KV." #endif int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "USAGE: harness_* INPUT_FILE\n"); return 2; } int fd_in = open(argv[1], O_RDONLY | O_CLOEXEC); if (fd_in < 0) { perror(argv[1]); abort(); } FuzzInput input = fuzz_input_new_prealloc(1024); if (fuzz_input_read(fd_in, &input) < 0) { perror("read"); abort(); } SAFEV v = SAFEV_new_UNSAFE(input.data, input.size); do_the_thing(v); fuzz_input_free(input); close(fd_in); return 0; } ================================================ FILE: plugins/mpd/fuzz/testcases_append_kv/testcase_000 ================================================ aaaaaa: ================================================ FILE: plugins/mpd/fuzz/testcases_append_kv/testcase_001 ================================================ aaabaaa: ================================================ FILE: plugins/mpd/fuzz/testcases_append_kv/testcase_002 ================================================ aaaaabaaaab ================================================ FILE: plugins/mpd/fuzz/testcases_append_kv/testcase_003 ================================================ aaababaabaabb: aaa ================================================ FILE: plugins/mpd/fuzz/testcases_append_kv/testcase_004 ================================================ baaaab:b ================================================ FILE: plugins/mpd/fuzz/testcases_append_kv/testcase_005 ================================================ bbaabbbabbbaaabbaa ================================================ FILE: plugins/mpd/fuzz/testcases_append_kv/testcase_006 ================================================ abbbbbab: a ================================================ FILE: plugins/mpd/fuzz/testcases_append_kv/testcase_007 ================================================ bbbbbbaabb:bbabbab ================================================ FILE: plugins/mpd/fuzz/testcases_append_kv/testcase_008 ================================================ bbbabbbabbbbbbbbb ================================================ FILE: plugins/mpd/fuzz/testcases_append_kv/testcase_009 ================================================ bbb: bb ================================================ FILE: plugins/mpd/fuzz/testcases_check_greeting/testcase_000 ================================================ aaaaaa ================================================ FILE: plugins/mpd/fuzz/testcases_check_greeting/testcase_001 ================================================ Oaaabaaa ================================================ FILE: plugins/mpd/fuzz/testcases_check_greeting/testcase_002 ================================================ OKaaaabaaaaababaaaaaba ================================================ FILE: plugins/mpd/fuzz/testcases_check_greeting/testcase_003 ================================================ OK babbaaaa ================================================ FILE: plugins/mpd/fuzz/testcases_check_greeting/testcase_004 ================================================ OK Mbaaaabb ================================================ FILE: plugins/mpd/fuzz/testcases_check_greeting/testcase_005 ================================================ OK MPababbabbbaabaabab ================================================ FILE: plugins/mpd/fuzz/testcases_check_greeting/testcase_006 ================================================ OK MPDabbaabbaabbbabbabbbb ================================================ FILE: plugins/mpd/fuzz/testcases_check_greeting/testcase_007 ================================================ OK MPD bbbbbbbbbbbbaaa ================================================ FILE: plugins/mpd/fuzz/testcases_check_greeting/testcase_008 ================================================ bbabbbbb ================================================ FILE: plugins/mpd/fuzz/testcases_check_greeting/testcase_009 ================================================ Obbbbbbbbbbb ================================================ FILE: plugins/mpd/fuzz/testcases_check_greeting/testcase_pfx_only ================================================ OK MPD ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/ack_testcase_000 ================================================ aaaaaa ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/ack_testcase_001 ================================================ Abaabaaa ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/ack_testcase_002 ================================================ ACaaaabaababbabbbbabba ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/ack_testcase_003 ================================================ ACKbbbbabba ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/ack_testcase_004 ================================================ ACK bbbbbbb ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/ok_testcase_000 ================================================ aaaaaa ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/ok_testcase_001 ================================================ Obaabaaa ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/ok_testcase_002 ================================================ OKaaaabaababbabbbbabba ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/ok_testcase_003 ================================================ bbbbabba ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/ok_testcase_004 ================================================ Obbbbbbb ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/ok_testcase_pfx_only ================================================ OK ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/other_testcase_000 ================================================ aaaaaa ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/other_testcase_001 ================================================ baabaaa ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/other_testcase_002 ================================================ aaaabaababbabbbbabba ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/other_testcase_003 ================================================ bbbbabba ================================================ FILE: plugins/mpd/fuzz/testcases_get_resp_type/other_testcase_004 ================================================ bbbbbbb ================================================ FILE: plugins/mpd/fuzz/testcases_write_quoted/testcase_000 ================================================ """""" ================================================ FILE: plugins/mpd/fuzz/testcases_write_quoted/testcase_001 ================================================ """"y""""" ================================================ FILE: plugins/mpd/fuzz/testcases_write_quoted/testcase_002 ================================================ """""""z""""""y"x""x ================================================ FILE: plugins/mpd/fuzz/testcases_write_quoted/testcase_003 ================================================ xz""""z"z"y""""z" ================================================ FILE: plugins/mpd/fuzz/testcases_write_quoted/testcase_004 ================================================ y"x""z"z ================================================ FILE: plugins/mpd/fuzz/testcases_write_quoted/testcase_005 ================================================ z""xx"z"y ================================================ FILE: plugins/mpd/fuzz/testcases_write_quoted/testcase_006 ================================================ x"y"z"yy"yyy"yxx ================================================ FILE: plugins/mpd/fuzz/testcases_write_quoted/testcase_007 ================================================ "xyyxyx"zy"zy"zxyy ================================================ FILE: plugins/mpd/fuzz/testcases_write_quoted/testcase_008 ================================================ xxxxzxx" ================================================ FILE: plugins/mpd/fuzz/testcases_write_quoted/testcase_009 ================================================ zyyyyzxyyxyxxx ================================================ FILE: plugins/mpd/line_reader.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "line_reader.h" #include #include #include #include "libls/ls_alloc_utils.h" #include "libsafe/safev.h" LineReader line_reader_new(size_t prealloc) { return (LineReader) { .buf = LS_XNEW(char, prealloc), .capacity = prealloc, }; } int line_reader_read_line(LineReader *LR, FILE *f, SAFEV *out) { ssize_t r = getline(&LR->buf, &LR->capacity, f); if (r < 0) { return -1; } SAFEV raw_v = SAFEV_new_UNSAFE(LR->buf, r); *out = SAFEV_rstrip_once(raw_v, '\n'); return 0; } void line_reader_destroy(LineReader *LR) { free(LR->buf); } ================================================ FILE: plugins/mpd/line_reader.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef line_reader_h_ #define line_reader_h_ #include #include #include "libsafe/safev.h" typedef struct { char *buf; size_t capacity; } LineReader; LineReader line_reader_new(size_t prealloc); int line_reader_read_line(LineReader *LR, FILE *f, SAFEV *out); void line_reader_destroy(LineReader *LR); #endif ================================================ FILE: plugins/mpd/mpd.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "libmoonvisit/moonvisit.h" #include "libls/ls_string.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_time_utils.h" #include "libls/ls_fifo_device.h" #include "libls/ls_strarr.h" #include "libls/ls_io_utils.h" #include "libls/ls_panic.h" #include "libsafe/safev.h" #include "connect.h" #include "safe_haven.h" #include "line_reader.h" typedef struct { char *hostname; uint64_t port; char *password; double tmo; double retry_tmo; char *retry_fifo; LS_String idle_str; bool enable_tcp_keepalive; char *bind_addr; BindAddrFamily bind_addr_family; } Priv; static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; free(p->hostname); free(p->password); free(p->retry_fifo); ls_string_free(p->idle_str); free(p->bind_addr); free(p); } static int parse_events_elem(MoonVisit *mv, void *ud, int kpos, int vpos) { mv->where = "'events' element"; (void) kpos; Priv *p = ud; if (moon_visit_checktype_at(mv, NULL, vpos, LUA_TSTRING) < 0) return -1; const char *s = lua_tostring(mv->L, vpos); ls_string_append_c(&p->idle_str, ' '); ls_string_append_s(&p->idle_str, s); return 1; } static int parse_ipver(const char *s) { if (strcmp(s, "ipv4") == 0) { return FAMILY_IPV4; } if (strcmp(s, "ipv6") == 0) { return FAMILY_IPV6; } return -1; } static int parse_bind_params(Priv *p, MoonVisit *mv, int table_pos) { int old_top = lua_gettop(mv->L); // mv->L: ? if (moon_visit_scrutinize_table(mv, table_pos, "bind", true) < 0) { goto fail; } // mv->L: ? bind if (lua_isnil(mv->L, -1)) { goto ok; } if (moon_visit_str(mv, -1, "addr", &p->bind_addr, NULL, false) < 0) { goto fail; } const char *ipver; if (moon_visit_scrutinize_str(mv, -1, "ipver", &ipver, NULL, false) < 0) { goto fail; } // mv->L: ? bind ipver int family = parse_ipver(ipver); if (family < 0) { moon_visit_err(mv, "bind.ipver is invalid"); goto fail; } p->bind_addr_family = family; ok: lua_settop(mv->L, old_top); return 0; fail: lua_settop(mv->L, old_top); return -1; } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .hostname = NULL, .port = 6600, .password = NULL, .tmo = -1, .retry_tmo = 10, .retry_fifo = NULL, .idle_str = ls_string_new_from_s("idle"), .enable_tcp_keepalive = false, .bind_addr = NULL, .bind_addr_family = FAMILY_NONE, }; char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Parse hostname if (moon_visit_str(&mv, -1, "hostname", &p->hostname, NULL, true) < 0) goto mverror; // Parse port if (moon_visit_uint(&mv, -1, "port", &p->port, true) < 0) goto mverror; if (p->port > 65535) { LS_FATALF(pd, "port is not a valid port number"); goto error; } // Parse password if (moon_visit_str(&mv, -1, "password", &p->password, NULL, true) < 0) goto mverror; if (p->password && (strchr(p->password, '\n'))) { LS_FATALF(pd, "password contains a line break"); goto error; } // Parse timeout if (moon_visit_num(&mv, -1, "timeout", &p->tmo, true) < 0) goto mverror; // Parse retry_in if (moon_visit_num(&mv, -1, "retry_in", &p->retry_tmo, true) < 0) goto mverror; // Parse retry_fifo if (moon_visit_str(&mv, -1, "retry_fifo", &p->retry_fifo, NULL, true) < 0) goto mverror; // Parse enable_tcp_keepalive if (moon_visit_bool(&mv, -1, "enable_tcp_keepalive", &p->enable_tcp_keepalive, true) < 0) { goto mverror; } // Parse bind if (parse_bind_params(p, &mv, -1) < 0) { goto mverror; } // Parse events int has_events = moon_visit_table_f(&mv, -1, "events", parse_events_elem, p, true); if (has_events < 0) { goto mverror; } if (!has_events) { ls_string_append_s(&p->idle_str, " mixer player"); } ls_string_append_b(&p->idle_str, "\n", 2); // append '\n' and '\0' return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); error: destroy(pd); return LUASTATUS_ERR; } static void kv_strarr_table_push(LS_StringArray sa, lua_State *L) { size_t n = ls_strarr_size(sa); LS_ASSERT(n % 2 == 0); lua_newtable(L); // L: table for (size_t i = 0; i < n; i += 2) { size_t nkey; const char *key = ls_strarr_at(sa, i, &nkey); lua_pushlstring(L, key, nkey); // L: table key size_t nvalue; const char *value = ls_strarr_at(sa, i + 1, &nvalue); lua_pushlstring(L, value, nvalue); // L: table key value lua_settable(L, -3); // L: table } } static void report_status( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, const char *what) { lua_State *L = funcs.call_begin(pd->userdata); lua_createtable(L, 0, 1); // L: table lua_pushstring(L, what); // L: table what lua_setfield(L, -2, "what"); // L: table funcs.call_end(pd->userdata); } typedef struct { FILE *f; LineReader LR; SAFEV line_v; } Context; static inline int read_line(Context *ctx) { return line_reader_read_line(&ctx->LR, ctx->f, &ctx->line_v); } static void log_io_error(LuastatusPluginData *pd, Context *ctx) { if (feof(ctx->f)) { LS_ERRF(pd, "connection closed"); } else { LS_ERRF(pd, "I/O error: %s", ls_tls_strerror(errno)); } } static void log_bad_response( LuastatusPluginData *pd, Context *ctx, const char *where) { LS_ERRF( pd, "server said (%s): %.*s", where, SAFEV_FMT_ARG(ctx->line_v, 1024)); } static int loop_until_ok( LuastatusPluginData *pd, Context *ctx, LS_StringArray *kv) { for (;;) { if (read_line(ctx) < 0) { log_io_error(pd, ctx); return -1; } switch (response_type(ctx->line_v)) { case RESP_OK: return 0; case RESP_ACK: log_bad_response(pd, ctx, "in loop"); return -1; case RESP_OTHER: if (kv) { append_line_to_kv_strarr(kv, ctx->line_v); } } } } static void interact( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, int fd) { Priv *p = pd->priv; Context ctx = { .f = NULL, .LR = line_reader_new(1024), .line_v = SAFEV_new_empty(), }; LS_StringArray kv_song = ls_strarr_new(); LS_StringArray kv_status = ls_strarr_new(); int fd_to_close = fd; if (!(ctx.f = fdopen(fd, "r+"))) { LS_ERRF(pd, "can't fdopen connection fd: %s", ls_tls_strerror(errno)); goto done; } fd_to_close = -1; // read and check the greeting if (read_line(&ctx) < 0) { log_io_error(pd, &ctx); goto done; } if (!is_good_greeting(ctx.line_v)) { log_bad_response(pd, &ctx, "to greeting"); goto done; } // send the password, if specified if (p->password) { fputs("password ", ctx.f); write_quoted(ctx.f, SAFEV_new_from_cstr_UNSAFE(p->password)); putc('\n', ctx.f); fflush(ctx.f); if (ferror(ctx.f)) { log_io_error(pd, &ctx); goto done; } if (read_line(&ctx) < 0) { log_io_error(pd, &ctx); goto done; } if (response_type(ctx.line_v) != RESP_OK) { log_bad_response(pd, &ctx, "to password"); goto done; } } LS_TimeDelta tmo = ls_double_to_TD(p->tmo, LS_TD_FOREVER); for (;;) { // write "currentsong\n" fputs("currentsong\n", ctx.f); fflush(ctx.f); if (ferror(ctx.f)) { log_io_error(pd, &ctx); goto done; } // until OK, append data to 'kv_song' if (loop_until_ok(pd, &ctx, &kv_song) < 0) goto done; // write "status\n" fputs("status\n", ctx.f); fflush(ctx.f); if (ferror(ctx.f)) { log_io_error(pd, &ctx); goto done; } // until OK, append data to 'kv_status' if (loop_until_ok(pd, &ctx, &kv_status) < 0) goto done; // make a call lua_State *L = funcs.call_begin(pd->userdata); lua_createtable(L, 0, 3); // L: table lua_pushstring(L, "update"); // L: table "update" lua_setfield(L, -2, "what"); // L: table kv_strarr_table_push(kv_song, L); // L: table table lua_setfield(L, -2, "song"); // L: table kv_strarr_table_push(kv_status, L); // L: table table lua_setfield(L, -2, "status"); // L: table funcs.call_end(pd->userdata); // clear the arrays ls_strarr_clear(&kv_status); ls_strarr_clear(&kv_song); // write the idle string ("idle <...list of events...>\n") fputs(p->idle_str.data, ctx.f); fflush(ctx.f); if (ferror(ctx.f)) { log_io_error(pd, &ctx); goto done; } // if we need to, report timeouts until we have data on fd if (!ls_TD_is_forever(tmo)) { for (;;) { int nfds = ls_wait_input_on_fd(fd, tmo); if (nfds < 0) { LS_ERRF(pd, "ls_wait_input_on_fd: %s", ls_tls_strerror(errno)); goto done; } else if (nfds == 0) { report_status(pd, funcs, "timeout"); } else { break; } } } // wait for an OK if (loop_until_ok(pd, &ctx, NULL) < 0) goto done; } done: if (ctx.f) { fclose(ctx.f); } ls_close(fd_to_close); line_reader_destroy(&ctx.LR); ls_strarr_destroy(kv_song); ls_strarr_destroy(kv_status); } static void do_enable_tcp_keepalive(LuastatusPluginData *pd, int fd) { int value = 1; if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &value, sizeof(value)) < 0) { LS_WARNF(pd, "setsockopt: SO_KEEPALIVE: %s", ls_tls_strerror(errno)); } } static void do_enable_tcp_nodelay(LuastatusPluginData *pd, int fd) { int value = 1; if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &value, sizeof(value)) < 0) { LS_WARNF(pd, "setsockopt: TCP_NODELAY: %s", ls_tls_strerror(errno)); } } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { Priv *p = pd->priv; LS_FifoDevice retry_fifo_dev = ls_fifo_device_new(); char portstr[8]; snprintf(portstr, sizeof(portstr), "%d", (int) p->port); while (1) { report_status(pd, funcs, "connecting"); int fd; if (p->hostname && p->hostname[0] == '/') { fd = unixdom_open(pd, p->hostname); if (fd < 0) { goto retry; } } else { fd = inetdom_open(pd, p->hostname, portstr, p->bind_addr, p->bind_addr_family); if (fd < 0) { goto retry; } if (p->enable_tcp_keepalive) { do_enable_tcp_keepalive(pd, fd); } do_enable_tcp_nodelay(pd, fd); } interact(pd, funcs, fd); retry: report_status(pd, funcs, "error"); LS_TimeDelta retry_tmo; if (!ls_double_to_TD_checked(p->retry_tmo, &retry_tmo)) { LS_FATALF(pd, "an error occurred; not retrying as requested"); goto error; } if (ls_fifo_device_open(&retry_fifo_dev, p->retry_fifo) < 0) { LS_WARNF(pd, "ls_fifo_device_open: %s: %s", p->retry_fifo, ls_tls_strerror(errno)); } if (ls_fifo_device_wait(&retry_fifo_dev, retry_tmo) < 0) { LS_FATALF(pd, "ls_fifo_device_wait: %s: %s", p->retry_fifo, ls_tls_strerror(errno)); goto error; } } error: ls_fifo_device_close(&retry_fifo_dev); } LuastatusPluginIface luastatus_plugin_iface_v1 = { .init = init, .run = run, .destroy = destroy, }; ================================================ FILE: plugins/mpd/safe_haven.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "safe_haven.h" #include #include #include #include "libls/ls_strarr.h" #define LIT(Literal_) SAFEV_new_from_literal(Literal_) bool is_good_greeting(SAFEV v) { return SAFEV_starts_with(v, LIT("OK MPD ")); } ResponseType response_type(SAFEV v) { if (SAFEV_equals(v, LIT("OK"))) { return RESP_OK; } if (SAFEV_starts_with(v, LIT("ACK "))) { return RESP_ACK; } return RESP_OTHER; } static inline void do_fwrite_str_view(FILE *f, SAFEV v) { size_t n = SAFEV_len(v); if (n) { fwrite(SAFEV_ptr_UNSAFE(v), 1, n, f); } } void write_quoted(FILE *f, SAFEV v) { fputc('"', f); for (;;) { size_t i = SAFEV_index_of(v, '"'); if (i == (size_t) -1) { break; } SAFEV cur_seg = SAFEV_subspan(v, 0, i); do_fwrite_str_view(f, cur_seg); fputs("\\\"", f); v = SAFEV_suffix(v, i + 1); } do_fwrite_str_view(f, v); fputc('"', f); } static inline void append_sv_to_strarr(LS_StringArray *sa, SAFEV v) { ls_strarr_append(sa, SAFEV_ptr_UNSAFE(v), SAFEV_len(v)); } void append_line_to_kv_strarr(LS_StringArray *sa, SAFEV line) { size_t i = SAFEV_index_of(line, ':'); if (i == (size_t) -1) { return; } if (SAFEV_at_or(line, i + 1, '\0') != ' ') { return; } SAFEV key = SAFEV_subspan(line, 0, i); append_sv_to_strarr(sa, key); SAFEV value = SAFEV_suffix(line, i + 2); append_sv_to_strarr(sa, value); } ================================================ FILE: plugins/mpd/safe_haven.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef safe_haven_h_ #define safe_haven_h_ #include #include #include "libls/ls_strarr.h" #include "libsafe/safev.h" typedef enum { RESP_OK, RESP_ACK, RESP_OTHER, } ResponseType; bool is_good_greeting(SAFEV v); ResponseType response_type(SAFEV v); void write_quoted(FILE *f, SAFEV v); // If /line/ is of form "key: value", appends /key/ and /value/ to /sa/. void append_line_to_kv_strarr(LS_StringArray *sa, SAFEV line); #endif ================================================ FILE: plugins/mpris/CMakeLists.txt ================================================ install (FILES mpris.lua DESTINATION ${LUA_PLUGINS_DIR}) luastatus_add_man_page (README.rst luastatus-plugin-mpris 7) ================================================ FILE: plugins/mpris/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-mpris .. :X-man-page-only: ###################### .. :X-man-page-only: .. :X-man-page-only: ####################################### .. :X-man-page-only: MPRIS media player plugin for luastatus .. :X-man-page-only: ####################################### .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This derived plugin interfaces with MPRIS-compatible media players via D-Bus to monitor playback status and track metadata. Functions ========= The following functions are provided: * ``widget(tbl)`` Constructs a ``widget`` table required by luastatus. ``tbl`` is a table with the following fields: **(required)** - ``player``: string The name of the MPRIS player (e.g., ``"clementine"``). It will be automatically prefixed with ``org.mpris.MediaPlayer2.`` when communicating over D-Bus. You can list active MPRIS players on your system by running the following command:: dbus-send \ --session --dest=org.freedesktop.DBus --type=method_call --print-reply \ /org/freedesktop/DBus org.freedesktop.DBus.ListNames \ | grep -F 'org.mpris.MediaPlayer2' - ``cb``: function The callback function that will be called with a table containing the current MPRIS player properties (such as ``Metadata``, ``PlaybackStatus``, ``Volume``, etc.). The table will be empty if the data cannot be retrieved or the player is unavailable. **(optional)** - ``forced_refresh_interval``: number The interval in seconds for manually querying the player properties. Defaults to 30. This acts as a safety fallback if D-Bus signals are missed or invalidated. - ``event`` The ``event`` entry of the resulting table (see ``luastatus`` documentation for the description of ``widget.event`` field). ================================================ FILE: plugins/mpris/mpris.lua ================================================ --[[ Copyright (C) 2026 luastatus developers This file is part of luastatus. luastatus is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. luastatus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with luastatus. If not, see . --]] local function unwrap1_into_table(x) assert(type(x) == 'table') assert(#x == 1) local res = x[1] assert(type(res) == 'table') return res end local function mix_into(dst, x, handle_metadata_key) for _, kv in ipairs(x) do local k, v = kv[1], kv[2] if handle_metadata_key and k == 'Metadata' then dst[k] = {} mix_into(dst[k], v, false) else dst[k] = v end end end local function print_warning(what) print(string.format('WARNING: luastatus: mpris plugin: %s', what)) end local function storage_requery(storage) storage.current_data = {} local is_ok, res_raw = luastatus.plugin.get_all_properties({ bus = 'session', flag_no_autostart = true, dest = 'org.mpris.MediaPlayer2.' .. storage.player, object_path = '/org/mpris/MediaPlayer2', interface = 'org.mpris.MediaPlayer2.Player', }) if not is_ok then print_warning(string.format('cannot get properties: %s', res_raw)) return end mix_into(storage.current_data, unwrap1_into_table(res_raw), true) end local function storage_handle_update(storage, parameters) local changed_props = parameters[2] local invalidated_props = parameters[3] if #invalidated_props > 0 then storage_requery(storage) return end mix_into(storage.current_data, changed_props, true) end local P = {} function P.widget(tbl) assert(tbl.player) local storage = { player = tbl.player, current_data = {}, } return { plugin = 'dbus', opts = { signals = { { bus = 'session', sender = 'org.mpris.MediaPlayer2.' .. tbl.player, interface = 'org.freedesktop.DBus.Properties', signal = 'PropertiesChanged', object_path = '/org/mpris/MediaPlayer2', arg0 = 'org.mpris.MediaPlayer2.Player', }, }, timeout = tbl.forced_refresh_interval or 30, report_when_ready = true, }, cb = function(t) if t.what == 'signal' then storage_handle_update(storage, t.parameters) else storage_requery(storage) end return tbl.cb(storage.current_data) end, event = tbl.event, } end return P ================================================ FILE: plugins/multiplex/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_plugin ( plugin-multiplex $ $ $ $ $ ${sources} ) configure_file ("config.in.h" "config.generated.h") target_compile_definitions (plugin-multiplex PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-multiplex LUA) target_include_directories (plugin-multiplex PUBLIC "${PROJECT_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}") # find pthreads set (CMAKE_THREAD_PREFER_PTHREAD TRUE) set (THREADS_PREFER_PTHREAD_FLAG TRUE) find_package (Threads REQUIRED) # link against dl and pthread target_link_libraries (plugin-multiplex PUBLIC ${CMAKE_DL_LIBS} Threads::Threads) luastatus_add_man_page (README.rst luastatus-plugin-multiplex 7) ================================================ FILE: plugins/multiplex/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-timer .. :X-man-page-only: ###################### .. :X-man-page-only: .. :X-man-page-only: ########################## .. :X-man-page-only: timer plugin for luastatus .. :X-man-page-only: ########################## .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This plugin is a "multiplexor": it allows you to spawn multiple "nested widgets" and receive information from all of them. It also can invoke the ``event`` functions of nested widgets. Options ======= The following options are supported: * ``data_sources`` (**required**): table This must be a dictionary (a table with string keys) of *data source specifications*, Each entry is treated as follows: the key is the identifier of the data source, and the value is the *data source specification*. A *data source specification* is a string with a Lua program which defines ``LL_data_source`` global variable. This specification is used to initialize a new **nested widget**. Just like with normal luastatus widgets, ``plugin`` and ``cb`` entries are required, ``opts`` entry is optional. These entries have the same meaning as with normal luastatus widgets. The ``event`` function of a data source can later be called by the "main" multiplexor widget that is being discussed. Here is an example of a *data source specification*:: [[ widget = { plugin = 'xtitle', opts = { extended_fmt = true, }, cb = function(t) return t.instance or '' end, } ]] Currently, there is a limit on the number of data sources: there can be at most 64 different data sources. For nested widget's ``cb`` functions, only string and nil return values are supported. * ``greet``: boolean Whether or not to call the callback with ``what="hello"`` before doing anything else. Defaults to false. ``cb`` argument =============== A table with ``what`` field. * If ``what`` is ``"hello"``, then the ``greet`` was enabled and the plugin is making this call before doing anything else. Defaults to false. * If ``what`` is ``"update"``, then some nested widget's plugin has produced a value. In this case, ``updates`` field is also present and is a table that contains all the updates (note that only updates are specified, not the full snapshot). Its keys are identifiers of data sources, and values are as follows: + if this nested widget's ``cb`` has generated a string value: a string; + if this nested widget's ``cb`` has generated a nil value: ``false``; + some error occurred: an integer with error code (see below). Error codes ----------- The following error codes can be reported (as of now): * 64: plugin's ``run()`` function for this nested widget has returned (due to a fatal error), there will be no events from it anymore; * 65: Lua error occurred in this nested widget's ``cb`` function; * 66: This nested widget's ``cb`` function returned something other than a string or nil. Functions ========= The following functions are provided: * ``luastatus.plugin.call_event(ident, content)`` Calls the ``event`` function of the data source with identifier ``ident`` (which must be a string) with string ``content``. On success, returns ``true``. On failure, returns ``false, err_msg`` (but see `Notes`_). Notes ===== If the nested widgets have not been spawned yet, ``call_event`` throws an error instead of returning ``false, err_msg``. This is only possible if the function is being invoked from an event handler. So, if you use this function from an event handler, you should consider this possibility. ================================================ FILE: plugins/multiplex/config.in.h ================================================ #ifndef luastatus_plugin_multiplex_config_h_ #define luastatus_plugin_multiplex_config_h_ #define LUASTATUS_PLUGINS_DIR "@PLUGINS_DIR@" #define LUASTATUS_LUA_PLUGINS_DIR "@LUA_PLUGINS_DIR@" #endif ================================================ FILE: plugins/multiplex/conq.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "conq.h" #include #include #include "libls/ls_alloc_utils.h" #include "libls/ls_panic.h" #include "libls/ls_string.h" struct Conq { pthread_mutex_t mtx; pthread_cond_t condvar; LS_String *slots; size_t nslots; char slot_states[CONQ_MAX_SLOTS]; CONQ_MASK new_data_mask; }; Conq *conq_create(size_t nslots) { LS_ASSERT(nslots <= CONQ_MAX_SLOTS); Conq *q = LS_XNEW(Conq, 1); LS_PTH_CHECK(pthread_mutex_init(&q->mtx, NULL)); LS_PTH_CHECK(pthread_cond_init(&q->condvar, NULL)); q->slots = LS_XNEW(LS_String, nslots); for (size_t i = 0; i < nslots; ++i) { q->slots[i] = ls_string_new_reserve(1024); q->slot_states[i] = CONQ_SLOT_STATE_EMPTY; } q->nslots = nslots; q->new_data_mask = 0; return q; } void conq_update_slot( Conq *q, size_t slot_idx, const char *buf, size_t nbuf, ConqSlotState state) { LS_ASSERT(slot_idx < q->nslots); LS_PTH_CHECK(pthread_mutex_lock(&q->mtx)); LS_String *old = &q->slots[slot_idx]; ConqSlotState old_state = q->slot_states[slot_idx]; if (ls_string_eq_b(*old, buf, nbuf) && old_state == state) { goto unlock; } ls_string_assign_b(old, buf, nbuf); q->slot_states[slot_idx] = state; q->new_data_mask |= ((CONQ_MASK) 1) << slot_idx; LS_PTH_CHECK(pthread_cond_signal(&q->condvar)); unlock: LS_PTH_CHECK(pthread_mutex_unlock(&q->mtx)); } CONQ_MASK conq_fetch_updates( Conq *q, LS_String *out, ConqSlotState *out_states) { LS_PTH_CHECK(pthread_mutex_lock(&q->mtx)); CONQ_MASK mask; while (!(mask = q->new_data_mask)) { LS_PTH_CHECK(pthread_cond_wait(&q->condvar, &q->mtx)); } for (size_t i = 0; i < q->nslots; ++i) { if ((mask >> i) & 1) { LS_String slot = q->slots[i]; ls_string_assign_b(&out[i], slot.data, slot.size); out_states[i] = q->slot_states[i]; } } q->new_data_mask = 0; LS_PTH_CHECK(pthread_mutex_unlock(&q->mtx)); return mask; } void conq_destroy(Conq *q) { LS_PTH_CHECK(pthread_mutex_destroy(&q->mtx)); LS_PTH_CHECK(pthread_cond_destroy(&q->condvar)); for (size_t i = 0; i < q->nslots; ++i) { ls_string_free(q->slots[i]); } free(q->slots); free(q); } ================================================ FILE: plugins/multiplex/conq.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include #include "libls/ls_string.h" // A concurrent queue. // // Instead of single value, it maintains a list of /LS_String/s, // each of which might get "updated" independently. // // The size of the list is limited to /CONQ_MAX_SLOTS/. typedef uint64_t CONQ_MASK; enum { CONQ_MAX_SLOTS = sizeof(CONQ_MASK) * 8 }; // These must be >= 0 and < 128. typedef enum { CONQ_SLOT_STATE_EMPTY, CONQ_SLOT_STATE_NIL, CONQ_SLOT_STATE_HAS_VALUE, CONQ_SLOT_STATE_ERROR_PLUGIN_DONE = 64, CONQ_SLOT_STATE_ERROR_LUA_ERR, CONQ_SLOT_STATE_ERROR_BAD_TYPE, } ConqSlotState; struct Conq; typedef struct Conq Conq; Conq *conq_create(size_t nslots); void conq_update_slot( Conq *q, size_t slot_idx, const char *buf, size_t nbuf, ConqSlotState state); CONQ_MASK conq_fetch_updates( Conq *q, LS_String *out, ConqSlotState *out_states); void conq_destroy(Conq *q); ================================================ FILE: plugins/multiplex/external_context.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include "include/plugin_data_v1.h" typedef LuastatusPluginData_v1 *ExternalContext; ================================================ FILE: plugins/multiplex/map_ref.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "map_ref.h" #include "external_context.h" #include "libls/ls_panic.h" #include "libls/ls_alloc_utils.h" #include #include typedef struct { pthread_mutex_t mtx; size_t nrefs; } Control; MapRef map_ref_new_empty(void) { return (MapRef) {0}; } static inline Control *load(MapRef ref) { LS_ASSERT(ref.pptr != NULL); return *ref.pptr; } static inline void store(MapRef ref, Control *C) { LS_ASSERT(ref.pptr != NULL); *ref.pptr = C; } void map_ref_init(MapRef *ref, ExternalContext ectx) { ref->pptr = ectx->map_get(ectx->userdata, "plugin-init-mtx"); Control *C = load(*ref); if (C) { ++C->nrefs; } else { C = LS_XNEW(Control, 1); LS_PTH_CHECK(pthread_mutex_init(&C->mtx, NULL)); C->nrefs = 1; store(*ref, C); } } void map_ref_lock_mtx(MapRef ref) { Control *C = load(ref); LS_PTH_CHECK(pthread_mutex_lock(&C->mtx)); } void map_ref_unlock_mtx(MapRef ref) { Control *C = load(ref); LS_PTH_CHECK(pthread_mutex_unlock(&C->mtx)); } void map_ref_destroy(MapRef ref) { if (!ref.pptr) { // Wasn't even initialized (with /map_ref_init()/). return; } Control *C = load(ref); // Since we currently own a reference, /C/ must be non-NULL and have a positive refcount. LS_ASSERT(C != NULL); LS_ASSERT(C->nrefs != 0); if (!--C->nrefs) { LS_PTH_CHECK(pthread_mutex_destroy(&C->mtx)); free(C); store(ref, NULL); } } ================================================ FILE: plugins/multiplex/map_ref.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include "external_context.h" typedef struct { void **pptr; } MapRef; MapRef map_ref_new_empty(void); void map_ref_init(MapRef *ref, ExternalContext ectx); void map_ref_lock_mtx(MapRef ref); void map_ref_unlock_mtx(MapRef ref); void map_ref_destroy(MapRef ref); ================================================ FILE: plugins/multiplex/multiplex.c ================================================ /* * Copyright (C) 2015-2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include "libls/ls_panic.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_string.h" #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "libmoonvisit/moonvisit.h" #include "wspec_list.h" #include "runner.h" #include "conq.h" #include "priv.h" #include "runtime_data.h" #include "map_ref.h" static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; LS_PTH_CHECK(pthread_mutex_destroy(&p->runners_mtx)); wspec_list_destroy(&p->wspecs); map_ref_destroy(p->map_ref); free(p); } static int visit_wspec(MoonVisit *mv, void *ud, int kpos, int vpos) { Priv *p = ud; if (moon_visit_checktype_at(mv, "key", kpos, LUA_TSTRING) < 0) { return -1; } if (moon_visit_checktype_at(mv, "value", vpos, LUA_TSTRING) < 0) { return -1; } const char *name = lua_tostring(mv->L, kpos); const char *code = lua_tostring(mv->L, vpos); wspec_list_add(&p->wspecs, name, code); return 0; } static bool check_wspecs(WspecList *x, char *errbuf, size_t nerrbuf) { size_t n = wspec_list_size(x); if (!n) { snprintf(errbuf, nerrbuf, "data_sources table is empty"); return false; } if (n > CONQ_MAX_SLOTS) { snprintf( errbuf, nerrbuf, "too many data sources (%zu, limit is %d)", n, (int) CONQ_MAX_SLOTS ); return false; } const char *dup = wspec_list_find_duplicates(x); if (dup) { snprintf(errbuf, nerrbuf, "data_sources table: duplicate key '%s'", dup); return false; } return true; } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .wspecs = wspec_list_new(), .greet = false, .runners = NULL, .map_ref = map_ref_new_empty(), }; LS_PTH_CHECK(pthread_mutex_init(&p->runners_mtx, NULL)); map_ref_init(&p->map_ref, pd); char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Parse data_sources if (moon_visit_table_f(&mv, -1, "data_sources", visit_wspec, p, false) < 0) { goto mverror; } if (!check_wspecs(&p->wspecs, errbuf, sizeof(errbuf))) { goto mverror; } // Parse greet if (moon_visit_bool(&mv, -1, "greet", &p->greet, true) < 0) { goto mverror; } return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); //error: destroy(pd); return LUASTATUS_ERR; } static inline const char *evt_result_to_str(RunnerEventResult val) { switch (val) { case EVT_RESULT_OK: return NULL; case EVT_RESULT_LUA_ERROR: return "lua-error-in-handler"; case EVT_RESULT_NO_HANDLER: return "no-handler"; } LS_MUST_BE_UNREACHABLE(); } static int l_call_event(lua_State *L) { Priv *p = lua_touserdata(L, lua_upvalueindex(1)); const char *data_src_name = luaL_checkstring(L, 1); int data_src_idx = wspec_list_find(&p->wspecs, data_src_name); if (data_src_idx < 0) { return luaL_error(L, "cannot find data source with name '%s'", data_src_name); } size_t ndata; const char *data = luaL_checklstring(L, 2, &ndata); Runner **R; LS_PTH_CHECK(pthread_mutex_lock(&p->runners_mtx)); if (p->runners) { R = &p->runners[data_src_idx]; } else { R = NULL; } LS_PTH_CHECK(pthread_mutex_unlock(&p->runners_mtx)); if (!R) { return luaL_error(L, "runners have not been spawned yet"); } const char *err; if (*R) { lua_State *borrowed_L = runner_event_begin(*R); lua_pushlstring(borrowed_L, data, ndata); RunnerEventResult res = runner_event_end(*R); err = evt_result_to_str(res); } else { err = "widget-failed-to-init"; } if (err) { lua_pushboolean(L, 0); lua_pushstring(L, err); return 2; } else { lua_pushboolean(L, 1); return 1; } } static void register_funcs(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv; // L: table lua_pushlightuserdata(L, p); // L: table ud lua_pushcclosure(L, l_call_event, 1); // L: table func lua_setfield(L, -2, "call_event"); // L: table } static void *my_thread_func(void *vud) { UniversalUserdata *u_ud = vud; LuastatusPluginData *pd = u_ud->ectx; Priv *p = pd->priv; Runner *R = p->runners[u_ud->i]; LS_ASSERT(R != NULL); runner_run(R); return NULL; } static void recv_handle_new_data( LuastatusPluginData *pd, Conq *cq, size_t i, lua_State *L) { if (lua_isnil(L, -1)) { conq_update_slot(cq, i, NULL, 0, CONQ_SLOT_STATE_NIL); } else if (lua_isstring(L, -1)) { size_t ns; const char *s = lua_tolstring(L, -1, &ns); conq_update_slot(cq, i, s, ns, CONQ_SLOT_STATE_HAS_VALUE); } else { Priv *p = pd->priv; const char *name = wspec_list_get_name(&p->wspecs, i); LS_WARNF( pd, "cb of widget [%s] returned %s value (expected string or nil)", name, luaL_typename(L, -1) ); conq_update_slot(cq, i, NULL, 0, CONQ_SLOT_STATE_ERROR_BAD_TYPE); } } static void recv_callback(void *vud, RunnerCallbackReason reason, lua_State *L) { UniversalUserdata *u_ud = vud; LuastatusPluginData *pd = u_ud->ectx; Priv *p = pd->priv; Conq *cq = p->rtdata.cq; size_t i = u_ud->i; switch (reason) { case REASON_NEW_DATA: recv_handle_new_data(pd, cq, i, L); return; case REASON_LUA_ERROR: conq_update_slot(cq, i, NULL, 0, CONQ_SLOT_STATE_ERROR_LUA_ERR); return; case REASON_PLUGIN_EXITED: conq_update_slot(cq, i, NULL, 0, CONQ_SLOT_STATE_ERROR_PLUGIN_DONE); return; } LS_MUST_BE_UNREACHABLE(); } static void make_call_simple( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, const char *what) { lua_State *L = funcs.call_begin(pd->userdata); lua_createtable(L, 0, 1); // L: table lua_pushstring(L, what); // L: table string lua_setfield(L, -2, "what"); // L: table funcs.call_end(pd->userdata); } static void push_update(lua_State *L, const LS_String *buf, ConqSlotState state) { switch (state) { case CONQ_SLOT_STATE_EMPTY: lua_pushnil(L); return; case CONQ_SLOT_STATE_NIL: lua_pushboolean(L, 0); return; case CONQ_SLOT_STATE_HAS_VALUE: lua_pushlstring(L, buf->data, buf->size); return; case CONQ_SLOT_STATE_ERROR_PLUGIN_DONE: case CONQ_SLOT_STATE_ERROR_LUA_ERR: case CONQ_SLOT_STATE_ERROR_BAD_TYPE: lua_pushinteger(L, (int) state); return; } LS_MUST_BE_UNREACHABLE(); } static void make_call_update( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, size_t n, CONQ_MASK mask, LS_String *bufs, ConqSlotState *states) { Priv *p = pd->priv; lua_State *L = funcs.call_begin(pd->userdata); lua_createtable(L, 0, 2); // L: table lua_pushstring(L, "update"); // L: table string lua_setfield(L, -2, "what"); // L: table lua_newtable(L); // L: table table for (size_t i = 0; i < n; ++i) { if ((mask >> i) & 1) { push_update(L, &bufs[i], states[i]); // L: table table value const char *key = wspec_list_get_name(&p->wspecs, i); lua_setfield(L, -2, key); // L: table table } } lua_setfield(L, -2, "updates"); // L: table funcs.call_end(pd->userdata); } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { Priv *p = pd->priv; size_t n = wspec_list_size(&p->wspecs); runtime_data_init(&p->rtdata, pd, n); map_ref_lock_mtx(p->map_ref); for (size_t i = 0; i < n; ++i) { const char *name = wspec_list_get_name(&p->wspecs, i); const char *code = wspec_list_get_code(&p->wspecs, i); Runner *R = runner_new( name, code, pd, recv_callback, &p->rtdata.u_uds[i] ); p->rtdata.runners[i] = R; wspec_list_get_rid_of_code(&p->wspecs, i); } map_ref_unlock_mtx(p->map_ref); LS_PTH_CHECK(pthread_mutex_lock(&p->runners_mtx)); p->runners = p->rtdata.runners; LS_PTH_CHECK(pthread_mutex_unlock(&p->runners_mtx)); if (p->greet) { make_call_simple(pd, funcs, "hello"); } for (size_t i = 0; i < n; ++i) { if (!p->rtdata.runners[i]) { continue; } pthread_t ignored_tid; int err_num = pthread_create( &ignored_tid, NULL, my_thread_func, &p->rtdata.u_uds[i] ); if (err_num != 0) { LS_ERRF(pd, "cannot create thread: %s", ls_tls_strerror(err_num)); } } Conq *cq = p->rtdata.cq; LS_String *bufs = p->rtdata.bufs; ConqSlotState *states = p->rtdata.states; for (;;) { CONQ_MASK mask = conq_fetch_updates(cq, bufs, states); make_call_update(pd, funcs, n, mask, bufs, states); } } LuastatusPluginIface luastatus_plugin_iface_v1 = { .init = init, .register_funcs = register_funcs, .run = run, .destroy = destroy, }; ================================================ FILE: plugins/multiplex/priv.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include #include "wspec_list.h" #include "runtime_data.h" #include "runner.h" #include "map_ref.h" typedef struct { WspecList wspecs; bool greet; pthread_mutex_t runners_mtx; Runner **runners; RuntimeData rtdata; MapRef map_ref; } Priv; ================================================ FILE: plugins/multiplex/runner.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "runner.h" #include #include #include #include #include #include #include #include #include #include #include #include "include/plugin_data_v1.h" #include "libls/ls_panic.h" #include "libls/ls_xallocf.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_getenv_r.h" #include "libls/ls_lua_compat.h" #include "librunshell/runshell.h" #include "libwidechar/libwidechar.h" #include "external_context.h" #include "config.generated.h" // Logging macros. #define FATALF(Runner_, ...) my_sayf(Runner_, LUASTATUS_LOG_FATAL, __VA_ARGS__) #define ERRF(Runner_, ...) my_sayf(Runner_, LUASTATUS_LOG_ERR, __VA_ARGS__) #define WARNF(Runner_, ...) my_sayf(Runner_, LUASTATUS_LOG_WARN, __VA_ARGS__) #define INFOF(Runner_, ...) my_sayf(Runner_, LUASTATUS_LOG_INFO, __VA_ARGS__) #define VERBOSEF(Runner_, ...) my_sayf(Runner_, LUASTATUS_LOG_VERBOSE, __VA_ARGS__) #define DEBUGF(Runner_, ...) my_sayf(Runner_, LUASTATUS_LOG_DEBUG, __VA_ARGS__) #define TRACEF(Runner_, ...) my_sayf(Runner_, LUASTATUS_LOG_TRACE, __VA_ARGS__) #define LOCK_L(W_) LS_PTH_CHECK(pthread_mutex_lock(&(W_)->L_mtx)) #define UNLOCK_L(W_) LS_PTH_CHECK(pthread_mutex_unlock(&(W_)->L_mtx)) enum { SAYF_BUF_SIZE = 1024 }; typedef struct { // The interface loaded from this plugin's .so file. LuastatusPluginIface_v1 iface; // An allocated zero-terminated string with plugin name, as specified in widget's // /widget.plugin/ string. char *name; // A handle returned from /dlopen/ for this plugin's .so file. void *dlhandle; } Plugin; typedef struct { // An initialized plugin. Plugin plugin; // /plugin/'s data for this widget. LuastatusPluginData_v1 data; // This widget's Lua interpreter instance. lua_State *L; // A mutex guarding /L/. pthread_mutex_t L_mtx; // Lua reference (in /L/'s registry) to this widget's /widget.cb/ function. int lref_cb; // Lua reference (in /L/'s registry) to this widget's /widget.event/ function (is // /LUA_REFNIL/ if the latter is /nil/). int lref_event; // An allocated widget's name. char *name; } Widget; struct Runner { ExternalContext ectx; Widget widget; RunnerCallback callback; void *callback_ud; }; // This function exists because /dlerror()/ may return /NULL/ even if /dlsym()/ returned /NULL/. static inline const char *safe_dlerror(void) { const char *err = dlerror(); return err ? err : "(no error, but the symbol is NULL)"; } static inline void safe_vsnprintf(char *buf, size_t nbuf, const char *fmt, va_list vl) { if (vsnprintf(buf, nbuf, fmt, vl) < 0) { buf[0] = '\0'; } } static void my_sayf(Runner *runner, int level, const char *fmt, ...) { char buf[SAYF_BUF_SIZE]; va_list vl; va_start(vl, fmt); safe_vsnprintf(buf, sizeof(buf), fmt, vl); va_end(vl); runner->ectx->sayf( runner->ectx->userdata, level, "%s", buf ); } static void external_sayf(void *userdata, int level, const char *fmt, ...) { Runner *runner = userdata; char buf[SAYF_BUF_SIZE]; va_list vl; va_start(vl, fmt); safe_vsnprintf(buf, sizeof(buf), fmt, vl); va_end(vl); runner->ectx->sayf( runner->ectx->userdata, level, "[%s] %s", runner->widget.name, buf ); } // Returns a pointer to the value of the entry with key /key/; or creates a new entry with the given // key and /NULL/ value, and returns a pointer to that value. static void **map_get(void *userdata, const char *key) { LS_ASSERT(key != NULL); Runner *runner = userdata; TRACEF(runner, "map_get(userdata=%p, key='%s')", userdata, key); return runner->ectx->map_get(runner->ectx->userdata, key); } static bool plugin_load(Runner *runner, Plugin *p, const char *filename, const char *name) { p->dlhandle = NULL; p->name = ls_xstrdup(name); DEBUGF(runner, "loading plugin from file '%s'", filename); (void) dlerror(); // clear last error if (!(p->dlhandle = dlopen(filename, RTLD_NOW | RTLD_LOCAL))) { ERRF(runner, "dlopen: %s: %s", filename, safe_dlerror()); goto error; } int *p_lua_ver = dlsym(p->dlhandle, "LUASTATUS_PLUGIN_LUA_VERSION_NUM"); if (!p_lua_ver) { ERRF(runner, "dlsym: LUASTATUS_PLUGIN_LUA_VERSION_NUM: %s", safe_dlerror()); goto error; } if (*p_lua_ver != LUA_VERSION_NUM) { ERRF(runner, "plugin '%s' was compiled with LUA_VERSION_NUM=%d and luastatus with %d", filename, *p_lua_ver, LUA_VERSION_NUM); goto error; } LuastatusPluginIface_v1 *p_iface = dlsym(p->dlhandle, "luastatus_plugin_iface_v1"); if (!p_iface) { ERRF(runner, "dlsym: luastatus_plugin_iface_v1: %s", safe_dlerror()); goto error; } p->iface = *p_iface; DEBUGF(runner, "plugin successfully loaded"); return true; error: if (p->dlhandle) { dlclose(p->dlhandle); } free(p->name); return false; } static bool plugin_load_by_name(Runner *runner, Plugin *p, const char *name) { if ((strchr(name, '/'))) { return plugin_load(runner, p, name, name); } else { char *filename = ls_xallocf("%s/plugin-%s.so", LUASTATUS_PLUGINS_DIR, name); bool r = plugin_load(runner, p, filename, name); free(filename); return r; } } static void plugin_unload(Plugin *p) { free(p->name); dlclose(p->dlhandle); } static lua_State *xnew_lua_state(void) { lua_State *L = luaL_newstate(); if (!L) { LS_PANIC("luaL_newstate() failed: out of memory?"); } return L; } // Returns a string representation of an error object located at the position /pos/ of /L/'s stack. static inline const char *get_lua_error_msg(lua_State *L, int pos) { const char *msg = lua_tostring(L, pos); return msg ? msg : "(error object cannot be converted to string)"; } // Checks a /lua_*/ call that returns a /LUA_*/ error code, performed on a Lua interpreter instance // /L/. /ret/ is the return value of the call. // // If /ret/ is /0/, returns /true/; otherwise, logs the error and returns /false/. static bool check_lua_call(Runner *runner, lua_State *L, int ret) { const char *prefix; switch (ret) { case 0: return true; case LUA_ERRRUN: case LUA_ERRSYNTAX: case LUA_ERRMEM: // Lua itself produces a meaningful error message in this case case LUA_ERRFILE: // ditto prefix = "(lua) "; break; case LUA_ERRERR: prefix = "(lua) error while running error handler: "; break; #ifdef LUA_ERRGCMM // Introduced in Lua 5.2 and removed in in Lua 5.4. case LUA_ERRGCMM: prefix = "(lua) error while running __gc metamethod: "; break; #endif default: prefix = "unknown Lua error code (please report!), message is: "; } // L: ? error ERRF(runner, "%s%s", prefix, get_lua_error_msg(L, -1)); lua_pop(L, 1); // L: ? return false; } // The Lua error handler that gets called whenever an error occurs inside a chunk called with // /do_lua_call/. // // Currently, it returns (to /check_lua_call/) the traceback of the error. static int l_error_handler(lua_State *L) { // L: error lua_getglobal(L, LUA_DBLIBNAME); // L: error debug lua_getfield(L, -1, "traceback"); // L: error debug traceback lua_pushstring(L, get_lua_error_msg(L, 1)); // L: error debug traceback msg lua_pushinteger(L, 2); // L: error debug traceback msg level lua_call(L, 2, 1); // L: error debug result return 1; } // Similar to /lua_call/, but expects an error handler to be at the bottom of /L/'s stack, runs the // chunk with that error handler, and logs the error message, if any. // // Returns /true/ on success, /false/ on failure. static inline bool do_lua_call(Runner *runner, lua_State *L, int nargs, int nresults) { return check_lua_call(runner, L, lua_pcall(L, nargs, nresults, 1)); } // Replacement for Lua's /os.exit()/: a simple /exit()/ used by Lua is not thread-safe in Linux. static int l_os_exit(lua_State *L) { int code = luaL_optinteger(L, 1, /*default value*/ EXIT_SUCCESS); fflush(stdout); fflush(stderr); _exit(code); } // Replacement for Lua's /os.getenv()/: a simple /getenv()/ used by Lua is not guaranteed by POSIX // to be thread-safe. static int l_os_getenv(lua_State *L) { const char *r = ls_getenv_r(luaL_checkstring(L, 1)); if (r) { lua_pushstring(L, r); } else { ls_lua_pushfail(L); } return 1; } // Replacement for Lua's /os.setlocale()/: this thing is inherently thread-unsafe. static int l_os_setlocale(lua_State *L) { ls_lua_pushfail(L); return 1; } // Implementation of /luastatus.require_plugin()/. Expects a single upvalue: an initially empty // table that will be used as a registry of loaded Lua plugins. static int l_require_plugin(lua_State *L) { const char *arg = luaL_checkstring(L, 1); if ((strchr(arg, '/'))) { return luaL_argerror(L, 1, "plugin name contains a slash"); } lua_pushvalue(L, lua_upvalueindex(1)); // L: ? table lua_getfield(L, -1, arg); // L: ? table value if (!lua_isnil(L, -1)) { return 1; } lua_pop(L, 1); // L: ? table char *filename = ls_xallocf("%s/%s.lua", LUASTATUS_LUA_PLUGINS_DIR, arg); int r = luaL_loadfile(L, filename); free(filename); if (r != 0) { return lua_error(L); } // L: ? table chunk lua_call(L, 0, 1); // L: ? table result lua_pushvalue(L, -1); // L: ? table result result lua_setfield(L, -3, arg); // L: ? table result return 1; } static void inject_libs_replacements(lua_State *L) { lua_getglobal(L, "os"); // L: ? os lua_pushcfunction(L, l_os_exit); // L: ? os l_os_exit lua_setfield(L, -2, "exit"); // L: ? os lua_pushcfunction(L, l_os_getenv); // L: ? os l_os_getenv lua_setfield(L, -2, "getenv"); // L: ? os lua_pushcfunction(L, l_os_setlocale); // L: ? os l_os_setlocale lua_setfield(L, -2, "setlocale"); // L: ? os bool is_lua51 = ls_lua_is_lua51(L); lua_pushcfunction( L, is_lua51 ? runshell_l_os_execute_lua51ver : runshell_l_os_execute); // L: ? os os_execute_func lua_setfield(L, -2, "execute"); // L: ? os lua_pop(L, 1); // L: ? } static void inject_luastatus_module(lua_State *L) { lua_createtable(L, 0, 4); // L: ? table // ========== require_plugin ========== lua_newtable(L); // L: ? table table lua_pushcclosure(L, l_require_plugin, 1); // L: ? table l_require_plugin lua_setfield(L, -2, "require_plugin"); // L: ? table // ========== execute ========== lua_pushcfunction(L, runshell_l_os_execute); // L: ? table cfunction lua_setfield(L, -2, "execute"); // L: ? table // ========== libwidechar ========== lua_newtable(L); // L: ? table table libwidechar_register_lua_funcs(L); // L: ? table table lua_setfield(L, -2, "libwidechar"); // L: ? table lua_setglobal(L, "luastatus"); // L: ? } // 1. Replaces some of the functions in the standard library with our thread-safe counterparts. // 2. Registers the /luastatus/ module (just creates a global table actually) except for the // /luastatus.plugin/ and /luastatus.barlib/ submodules (created later). static void inject_libs(lua_State *L) { inject_libs_replacements(L); inject_luastatus_module(L); } // Inspects the 'plugin' field of /w/'s /widget/ table; the /widget/ table is assumed to be on top // of /w.L/'s stack. The stack itself is not changed by this function. static bool widget_init_inspect_plugin(Runner *runner) { Widget *w = &runner->widget; lua_State *L = w->L; // L: ? widget lua_getfield(L, -1, "plugin"); // L: ? widget plugin if (!lua_isstring(L, -1)) { ERRF(runner, "'widget.plugin': expected string, found %s", luaL_typename(L, -1)); return false; } if (!plugin_load_by_name(runner, &w->plugin, lua_tostring(L, -1))) { ERRF(runner, "cannot load plugin '%s'", lua_tostring(L, -1)); return false; } lua_pop(L, 1); // L: ? widget return true; } // Inspects the 'cb' field of /w/'s /widget/ table; the /widget/ table is assumed to be on top // of /w.L/'s stack. The stack itself is not changed by this function. static bool widget_init_inspect_cb(Runner *runner) { Widget *w = &runner->widget; lua_State *L = w->L; // L: ? widget lua_getfield(L, -1, "cb"); // L: ? widget plugin if (!lua_isfunction(L, -1)) { ERRF(runner, "'widget.cb': expected function, found %s", luaL_typename(L, -1)); return false; } w->lref_cb = luaL_ref(L, LUA_REGISTRYINDEX); // L: ? widget return true; } // Inspects the 'event' field of /w/'s /widget/ table; the /widget/ table is assumed to be on top // of /w.L/'s stack. The stack itself is not changed by this function. static bool widget_init_inspect_event(Runner *runner) { Widget *w = &runner->widget; lua_State *L = w->L; // L: ? widget lua_getfield(L, -1, "event"); // L: ? widget event switch (lua_type(L, -1)) { case LUA_TNIL: case LUA_TFUNCTION: w->lref_event = luaL_ref(L, LUA_REGISTRYINDEX); // L: ? widget return true; default: ERRF(runner, "'widget.event': expected function or nil, found %s", luaL_typename(L, -1)); return false; } } // Inspects the 'opts' field of /w/'s /widget/ table; the /widget/ table is assumed to be on top // of /w.L/'s stack. // // Pushes either the value of 'opts', or a new empty table if it is absent, onto the stack. static bool widget_init_inspect_push_opts(Runner *runner) { Widget *w = &runner->widget; lua_State *L = w->L; lua_getfield(L, -1, "opts"); // L: ? widget opts switch (lua_type(L, -1)) { case LUA_TTABLE: return true; case LUA_TNIL: lua_pop(L, 1); // L: ? widget lua_newtable(L); // L: ? widget table return true; default: ERRF(runner, "'widget.opts': expected table or nil, found %s", luaL_typename(L, -1)); return false; } } static bool widget_init(Runner *runner, const char *name, const char *code) { Widget *w = &runner->widget; w->L = xnew_lua_state(); LS_PTH_CHECK(pthread_mutex_init(&w->L_mtx, NULL)); w->name = ls_xstrdup(name); bool plugin_loaded = false; DEBUGF(runner, "initializing widget [%s]", name); luaL_openlibs(w->L); // w->L: - inject_libs(w->L); // w->L: - lua_pushcfunction(w->L, l_error_handler); // w->L: l_error_handler DEBUGF(runner, "running widget [%s]", name); char *chunk_name = ls_xallocf("data source [%s]", name); bool loaded_ok = check_lua_call( runner, w->L, luaL_loadbuffer(w->L, code, strlen(code), chunk_name)); free(chunk_name); if (!loaded_ok) { goto error; } // w->L: l_error_handler chunk if (!do_lua_call(runner, w->L, 0, 0)) { goto error; } // w->L: l_error_handler lua_getglobal(w->L, "widget"); // w->L: l_error_handler widget if (!lua_istable(w->L, -1)) { ERRF(runner, "'widget': expected table, found %s", luaL_typename(w->L, -1)); goto error; } if (!widget_init_inspect_plugin(runner)) { goto error; } plugin_loaded = true; if (!widget_init_inspect_cb(runner) || !widget_init_inspect_event(runner) || !widget_init_inspect_push_opts(runner)) { goto error; } // w->L: l_error_handler widget opts w->data = (LuastatusPluginData_v1) { .userdata = runner, .sayf = external_sayf, .map_get = map_get, }; if (w->plugin.iface.init(&w->data, w->L) == LUASTATUS_ERR) { ERRF(runner, "plugin's init() failed"); goto error; } LS_ASSERT(lua_gettop(w->L) == 3); // w->L: l_error_handler widget opts lua_pop(w->L, 2); // w->L: l_error_handler DEBUGF(runner, "widget successfully initialized"); return true; error: lua_close(w->L); LS_PTH_CHECK(pthread_mutex_destroy(&w->L_mtx)); free(w->name); if (plugin_loaded) { plugin_unload(&w->plugin); } return false; } static void widget_destroy(Widget *w) { w->plugin.iface.destroy(&w->data); plugin_unload(&w->plugin); lua_close(w->L); LS_PTH_CHECK(pthread_mutex_destroy(&w->L_mtx)); free(w->name); } static void register_funcs(Runner *runner) { Widget *w = &runner->widget; lua_State *L = w->L; // L: ? lua_getglobal(L, "luastatus"); // L: ? luastatus if (!lua_istable(L, -1)) { WARNF( runner, "widget [%s]: 'luastatus' is not a table anymore, will not register " "barlib/plugin functions", w->name); goto done; } if (w->plugin.iface.register_funcs) { lua_newtable(L); // L: ? luastatus table int old_top = lua_gettop(L); (void) old_top; w->plugin.iface.register_funcs(&w->data, L); // L: ? luastatus table LS_ASSERT(lua_gettop(L) == old_top); lua_setfield(L, -2, "plugin"); // L: ? luastatus } done: lua_pop(L, 1); // L: ? } static lua_State *plugin_call_begin(void *userdata) { Runner *runner = userdata; TRACEF(runner, "plugin_call_begin(userdata=%p)", userdata); Widget *w = &runner->widget; LOCK_L(w); lua_State *L = w->L; LS_ASSERT(lua_gettop(L) == 1); // w->L: l_error_handler lua_rawgeti(L, LUA_REGISTRYINDEX, w->lref_cb); // w->L: l_error_handler cb return L; } static void plugin_call_end(void *userdata) { Runner *runner = userdata; TRACEF(runner, "plugin_call_end(userdata=%p)", userdata); Widget *w = &runner->widget; lua_State *L = w->L; LS_ASSERT(lua_gettop(L) == 3); // L: l_error_handler cb data bool r = do_lua_call(runner, L, 1, 1); if (r) { runner->callback(runner->callback_ud, REASON_NEW_DATA, L); } else { runner->callback(runner->callback_ud, REASON_LUA_ERROR, NULL); } lua_settop(L, 1); // L: l_error_handler UNLOCK_L(w); } static void plugin_call_cancel(void *userdata) { Runner *runner = userdata; TRACEF(runner, "plugin_call_cancel(userdata=%p)", userdata); Widget *w = &runner->widget; lua_settop(w->L, 1); // w->L: l_error_handler UNLOCK_L(w); } lua_State *runner_event_begin(Runner *runner) { Widget *w = &runner->widget; LOCK_L(w); lua_State *L = w->L; LS_ASSERT(lua_gettop(L) == 1); // L: l_error_handler lua_rawgeti(L, LUA_REGISTRYINDEX, w->lref_event); // L: l_error_handler event return L; } RunnerEventResult runner_event_end(Runner *runner) { RunnerEventResult ret; Widget *w = &runner->widget; lua_State *L = w->L; LS_ASSERT(lua_gettop(L) == 3); // L: l_error_handler event arg if (w->lref_event == LUA_REFNIL) { lua_pop(L, 2); // L: l_error_handler ret = EVT_RESULT_NO_HANDLER; } else { bool is_ok = do_lua_call(runner, L, 1, 0); // L: l_error_handler ret = is_ok ? EVT_RESULT_OK : EVT_RESULT_LUA_ERROR; } UNLOCK_L(w); return ret; } void runner_run(Runner *runner) { Widget *w = &runner->widget; DEBUGF(runner, "widget [%s] is now running", w->name); w->plugin.iface.run(&w->data, (LuastatusPluginRunFuncs_v1) { .call_begin = plugin_call_begin, .call_end = plugin_call_end, .call_cancel = plugin_call_cancel, }); WARNF(runner, "plugin's run() for widget [%s] has returned", w->name); runner->callback(runner->callback_ud, REASON_PLUGIN_EXITED, NULL); } Runner *runner_new( const char *name, const char *code, ExternalContext ectx, RunnerCallback callback, void *callback_ud) { Runner *runner = LS_XNEW(Runner, 1); *runner = (Runner) { .ectx = ectx, .callback = callback, .callback_ud = callback_ud, }; if (!widget_init(runner, name, code)) { free(runner); return NULL; } register_funcs(runner); return runner; } void runner_destroy(Runner *runner) { widget_destroy(&runner->widget); free(runner); } ================================================ FILE: plugins/multiplex/runner.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include "external_context.h" typedef enum { REASON_NEW_DATA, REASON_LUA_ERROR, REASON_PLUGIN_EXITED, } RunnerCallbackReason; typedef void (*RunnerCallback)( void *ud, RunnerCallbackReason reason, lua_State *L); typedef enum { EVT_RESULT_OK, EVT_RESULT_LUA_ERROR, EVT_RESULT_NO_HANDLER, } RunnerEventResult; struct Runner; typedef struct Runner Runner; Runner *runner_new( const char *name, const char *code, ExternalContext ectx, RunnerCallback callback, void *callback_ud); void runner_run(Runner *R); lua_State *runner_event_begin(Runner *R); RunnerEventResult runner_event_end(Runner *R); void runner_destroy(Runner *R); ================================================ FILE: plugins/multiplex/runtime_data.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "runtime_data.h" #include "conq.h" #include "external_context.h" #include "runner.h" #include "libls/ls_panic.h" #include "libls/ls_alloc_utils.h" void runtime_data_init(RuntimeData *x, ExternalContext ectx, size_t n) { LS_ASSERT(n != 0); x->runners = LS_XNEW(Runner *, n); for (size_t i = 0; i < n; ++i) { x->runners[i] = NULL; } x->cq = conq_create(n); x->bufs = LS_XNEW(LS_String, n); for (size_t i = 0; i < n; ++i) { x->bufs[i] = ls_string_new_reserve(512); } x->states = LS_XNEW(ConqSlotState, n); for (size_t i = 0; i < n; ++i) { x->states[i] = CONQ_SLOT_STATE_EMPTY; } x->u_uds = LS_XNEW(UniversalUserdata, n); for (size_t i = 0; i < n; ++i) { x->u_uds[i] = (UniversalUserdata) { .ectx = ectx, .i = i, }; } x->ectx = ectx; } ================================================ FILE: plugins/multiplex/runtime_data.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include "runner.h" #include "conq.h" #include "external_context.h" #include "libls/ls_string.h" typedef struct { ExternalContext ectx; size_t i; } UniversalUserdata; typedef struct { Runner **runners; Conq *cq; LS_String *bufs; ConqSlotState *states; UniversalUserdata *u_uds; ExternalContext ectx; } RuntimeData; void runtime_data_init(RuntimeData *x, ExternalContext ectx, size_t n); ================================================ FILE: plugins/multiplex/wspec_list.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "wspec_list.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_panic.h" #include #include WspecList wspec_list_new(void) { return (WspecList) {0}; } void wspec_list_add(WspecList *x, const char *name, const char *code) { LS_ASSERT(name != NULL); LS_ASSERT(code != NULL); Wspec new_elem = (Wspec) { .name = ls_xstrdup(name), .code = ls_xstrdup(code), }; if (x->size == x->capacity) { x->data = LS_M_X2REALLOC(x->data, &x->capacity); } x->data[x->size++] = new_elem; } const char *wspec_list_find_duplicates(WspecList *x) { size_t n = x->size; for (size_t i = 0; i < n; ++i) { for (size_t j = 0; j < i; ++j) { const char *s1 = x->data[i].name; const char *s2 = x->data[j].name; if (strcmp(s1, s2) == 0) { return s1; } } } return NULL; } int wspec_list_find(WspecList *x, const char *name) { LS_ASSERT(name != NULL); size_t n = x->size; for (size_t i = 0; i < n; ++i) { if (strcmp(x->data[i].name, name) == 0) { return i; } } return -1; } void wspec_list_get_rid_of_code(WspecList *x, size_t idx) { LS_ASSERT(idx < x->size); Wspec *elem = &x->data[idx]; free(elem->code); elem->code = NULL; } void wspec_list_destroy(WspecList *x) { size_t n = x->size; for (size_t i = 0; i < n; ++i) { Wspec *elem = &x->data[i]; free(elem->name); free(elem->code); } free(x->data); } ================================================ FILE: plugins/multiplex/wspec_list.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include "libls/ls_compdep.h" #include "libls/ls_panic.h" typedef struct { char *name; char *code; } Wspec; typedef struct { Wspec *data; size_t size; size_t capacity; } WspecList; WspecList wspec_list_new(void); LS_INHEADER int wspec_list_size(WspecList *x) { return x->size; } LS_INHEADER const char *wspec_list_get_name(WspecList *x, size_t idx) { LS_ASSERT(idx < x->size); return x->data[idx].name; } LS_INHEADER const char *wspec_list_get_code(WspecList *x, size_t idx) { LS_ASSERT(idx < x->size); return x->data[idx].code; } void wspec_list_add(WspecList *x, const char *name, const char *code); const char *wspec_list_find_duplicates(WspecList *x); int wspec_list_find(WspecList *x, const char *name); void wspec_list_get_rid_of_code(WspecList *x, size_t idx); void wspec_list_destroy(WspecList *x); ================================================ FILE: plugins/network-linux/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_plugin (plugin-network-linux $ $ ${sources}) target_compile_definitions (plugin-network-linux PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-network-linux LUA) target_include_directories (plugin-network-linux PUBLIC "${PROJECT_SOURCE_DIR}") find_package (PkgConfig REQUIRED) pkg_check_modules (NL_AND_CO REQUIRED libnl-3.0 libnl-genl-3.0) luastatus_target_build_with (plugin-network-linux NL_AND_CO) luastatus_add_man_page (README.rst luastatus-plugin-network-linux 7) ================================================ FILE: plugins/network-linux/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-network-linux .. :X-man-page-only: ############################## .. :X-man-page-only: .. :X-man-page-only: ############################################# .. :X-man-page-only: Network plugin for luastatus (Linux-specific) .. :X-man-page-only: ############################################# .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This plugin monitors network routing and link updates. It can report IP addresses used for outgoing connections by various network interfaces, information about a wireless connection, and the speed of an Ethernet connection. Options ======= The following options are supported: * ``ip``: boolean Whether to report IP addresses used for outgoing connections. Defaults to true. * ``wireless``: boolean Whether to report wireless connection info. Defaults to false. * ``ethernet``: boolean Whether to report Ethernet connection info. Defaults to false. * ``new_overall_fmt``: boolean Whether to use *new overall format* (see documentation below). Defaults to false. * ``new_ip_fmt``: boolean Whether to report IP (``ipv4`` and ``ipv6``) addresses as tables (as opposed to strings). Defaults to false. * ``timeout``: number If specified and not negative, requery information and call ``cb`` every ``timeout`` seconds. Note that this is done on any routing/link update anyway, so this is only useful if you want to show the "volatile" properties of a wireless connection such as signal level, bitrate, and frequency. * ``make_self_pipe``: boolean If true, the ``wake_up()`` (see the `Functions`_ section) function will be available. Defaults to false. ``cb`` argument =============== If the list of network interfaces cannot be fetched: * if new overall format is used, a table ``{what = "error"}``; * if old overall format is used, ``nil``. Otherwise (if there is no error): * if new overall format is used, a table ``{what = reason, data = tbl}``, where ``reason`` is a string (see `Fetching the "what"`_) and ``tbl`` is the main table; * if old overall format is used, the main table itself (but, in this case, this table also has a metatable; see `Fetching the "what"`_ section). The main table is a table where keys are network interface names (e.g. ``wlan0`` or ``wlp1s0``) and values are tables with the following entries (all are optional): * ``ipv4``, ``ipv6`` (only if the ``ip`` option is enabled) - If ``new_ip_fmt`` option was set to true, arrays (tables with numeric keys) of strings; each element corresponds to a local IPv4/IPv6 address of the interface. (In an extremely unlikely and probably logically impossible event that the number of addresses exceeds maximum Lua array length, the array will be truncated and the last element will be ``false``, not a string, to indicate truncation.) - Otherwise, the value behind ``ipv4``/``ipv6`` key is a string corresponding to *some* local IPv4/IPv6 address of the interface. * ``wireless``: table with following entries (only if the ``wireless`` option is enabled): - ``ssid``: string 802.11 network service set identifier (also known as the "network name"). - ``signal_dbm``: number Signal level, in dBm. Generally, -90 corresponds to the worst quality, and -20 to the best quality. - ``frequency``: number Radio frequency, in Hz. - ``bitrate``: number Bitrate, in units of 100 kbit/s. * ``ethernet``: table with following entries (only if the ``ethernet`` option is enabled): - ``speed``: number Interface speed, in Mbits/s. Fetching the "what" ================================ If new overall format is used, and the argument is a table ``t``, then the reason why the callback has been called is simply stored in ``t.what``. If old overall format is used, and the argument (the main table) is a table ``t``, then the reason why the callback has been called can be fetched as follows:: getmetatable(t).what This value is either: * ``"update"`` network routing/link update; * ``"timeout"``: timeout; * ``"self_pipe"``: the ``luastatus.plugin.wake_up()`` function has been called. For old overall format, the reason this shamanistic ritual with a metatable is needed is that the support for any data other than the list of networks has not been planned for in advance, and ``what`` is a valid network interface name. Functions ========= The following functions are provided: * ``luastatus.plugin.wake_up()`` Forces a call to ``cb``. Only available if the ``make_self_pipe`` option was set to ``true``; otherwise, it throws an error. ================================================ FILE: plugins/network-linux/ethernet_info.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #define _GNU_SOURCE #include "ethernet_info.h" #include #include #include #include #include #include #include #include #include "libls/ls_panic.h" uint32_t get_ethernet_speed(int sockfd, const char *iface) { LS_ASSERT(iface != NULL); struct ethtool_cmd ecmd = {.cmd = ETHTOOL_GSET}; struct ifreq ifr = {.ifr_data = (caddr_t) &ecmd}; size_t niface = strlen(iface); if (niface + 1 > sizeof(ifr.ifr_name)) { goto fail; } memcpy(ifr.ifr_name, iface, niface + 1); if (ioctl(sockfd, SIOCETHTOOL, &ifr) < 0) { goto fail; } uint32_t res = ethtool_cmd_speed(&ecmd); if (res == (uint32_t) SPEED_UNKNOWN) { goto fail; } return res; fail: return 0; } ================================================ FILE: plugins/network-linux/ethernet_info.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef ethernet_info_h_ #define ethernet_info_h_ #include uint32_t get_ethernet_speed(int sockfd, const char *iface); #endif ================================================ FILE: plugins/network-linux/iface_type.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "iface_type.h" #include #include #include #include #include "libls/ls_io_utils.h" #include "libls/ls_panic.h" bool is_wlan_iface(const char *iface) { LS_ASSERT(iface != NULL); FILE *f = NULL; int fd = -1; char *line = NULL; size_t nline = 0; bool ret = false; char path[256]; snprintf(path, sizeof(path), "/sys/class/net/%s/uevent", iface); if ((fd = open(path, O_RDONLY | O_CLOEXEC)) < 0) { goto done; } if (!(f = fdopen(fd, "r"))) { goto done; } fd = -1; while (getline(&line, &nline, f) > 0) { if (strcmp(line, "DEVTYPE=wlan\n") == 0) { ret = true; goto done; } } done: free(line); ls_close(fd); if (f) { fclose(f); } return ret; } ================================================ FILE: plugins/network-linux/iface_type.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef iface_type_h_ #define iface_type_h_ #include bool is_wlan_iface(const char *iface); #endif ================================================ FILE: plugins/network-linux/network.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "libmoonvisit/moonvisit.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_io_utils.h" #include "libls/ls_osdep.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_time_utils.h" #include "libls/ls_evloop_lfuncs.h" #include "libls/ls_lua_compat.h" #include "string_set.h" #include "wireless_info.h" #include "ethernet_info.h" #include "iface_type.h" typedef struct { bool report_ip; bool report_wireless; bool report_ethernet; bool new_ip_fmt; bool new_overall_fmt; double tmo; StringSet wlan_ifaces; int eth_sockfd; int pipefds[2]; } Priv; static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; string_set_destroy(p->wlan_ifaces); ls_close(p->eth_sockfd); ls_close(p->pipefds[0]); ls_close(p->pipefds[1]); free(p); } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .report_ip = true, .report_wireless = false, .report_ethernet = false, .new_ip_fmt = false, .new_overall_fmt = false, .tmo = -1, .wlan_ifaces = string_set_new(), .eth_sockfd = -1, .pipefds = {-1, -1}, }; char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Parse ip if (moon_visit_bool(&mv, -1, "ip", &p->report_ip, true) < 0) goto mverror; // Parse wireless if (moon_visit_bool(&mv, -1, "wireless", &p->report_wireless, true) < 0) goto mverror; // Parse ethernet if (moon_visit_bool(&mv, -1, "ethernet", &p->report_ethernet, true) < 0) goto mverror; // Parse new_ip_fmt if (moon_visit_bool(&mv, -1, "new_ip_fmt", &p->new_ip_fmt, true) < 0) goto mverror; // Parse new_overall_fmt if (moon_visit_bool(&mv, -1, "new_overall_fmt", &p->new_overall_fmt, true) < 0) goto mverror; // Parse timeout if (moon_visit_num(&mv, -1, "timeout", &p->tmo, true) < 0) goto mverror; // Parse make_self_pipe bool make_self_pipe = false; if (moon_visit_bool(&mv, -1, "make_self_pipe", &make_self_pipe, true) < 0) { goto mverror; } if (make_self_pipe) { LS_DEBUGF(pd, "making self-pipe"); if (ls_self_pipe_open(p->pipefds) < 0) { LS_FATALF(pd, "ls_self_pipe_open: %s", ls_tls_strerror(errno)); goto error; } } // Open eth_sockfd if needed. if (p->report_ethernet) { p->eth_sockfd = ls_cloexec_socket(AF_INET, SOCK_DGRAM, 0); if (p->eth_sockfd < 0) { LS_WARNF(pd, "ls_cloexec_socket: %s", ls_tls_strerror(errno)); } } return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); error: destroy(pd); return LUASTATUS_ERR; } static void register_funcs(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv; // L: table ls_self_pipe_push_luafunc(p->pipefds, L); // L: table func lua_setfield(L, -2, "wake_up"); // L: table } static void report_error(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { Priv *p = pd->priv; lua_State *L = funcs.call_begin(pd->userdata); // L: ? if (p->new_overall_fmt) { lua_createtable(L, 0, 1); // L: ? table lua_pushstring(L, "error"); // L: ? table str lua_setfield(L, -2, "what"); // L: ? table } else { lua_pushnil(L); // L: ? nil } funcs.call_end(pd->userdata); } static void inject_ip_info(lua_State *L, struct ifaddrs *addr, bool new_ip_fmt) { // L: ? ifacetbl if (!addr->ifa_addr) { return; } int family = addr->ifa_addr->sa_family; if (family != AF_INET && family != AF_INET6) { return; } char host[1025]; int r = getnameinfo( addr->ifa_addr, family == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6), host, sizeof(host), NULL, 0, NI_NUMERICHOST); if (r) { return; } const char *k = family == AF_INET ? "ipv4" : "ipv6"; if (new_ip_fmt) { lua_getfield(L, -1, k); // L: ? ifacetbl tbl if (lua_isnil(L, -1)) { lua_pop(L, 1); // L: ? ifacetbl lua_newtable(L); // L: ? ifacetbl tbl lua_pushstring(L, host); // L: ? ifacetbl tbl str lua_rawseti(L, -2, 1); // L: ? ifacetbl tbl lua_setfield(L, -2, k); // L: ? ifacetbl } else { size_t n = ls_lua_array_len(L, -1); // L: ? ifacetbl tbl if (n == (size_t) LS_LUA_MAXI) { lua_pushboolean(L, 0); // L: ? ifacetbl tbl false lua_rawseti(L, -2, n); // L: ? ifacetbl tbl } else { lua_pushstring(L, host); // L: ? ifacetbl tbl str lua_rawseti(L, -2, n + 1); // L: ? ifacetbl tbl } lua_pop(L, 1); // L: ? ifacetbl } } else { lua_pushstring(L, host); // L: ? ifacetbl str lua_setfield(L, -2, k); // L: ? ifacetbl } } static void inject_wireless_info(lua_State *L, struct ifaddrs *addr) { WirelessInfo info; if (!get_wireless_info(addr->ifa_name, &info)) { return; } // L: ? ifacetbl lua_createtable(L, 0, 4); // L: ? ifacetbl table if (info.flags & HAS_ESSID) { lua_pushstring(L, info.essid); // L: ? ifacetbl table str lua_setfield(L, -2, "ssid"); // L: ? ifacetbl table } if (info.flags & HAS_SIGNAL_DBM) { lua_pushnumber(L, info.signal_dbm); // L: ? ifacetbl table number lua_setfield(L, -2, "signal_dbm"); // L: ? ifacetbl table } if (info.flags & HAS_FREQUENCY) { lua_pushnumber(L, info.frequency); // L: ? ifacetbl table number lua_setfield(L, -2, "frequency"); // L: ? ifacetbl table } if (info.flags & HAS_BITRATE) { lua_pushnumber(L, info.bitrate); // L: ? ifacetbl table number lua_setfield(L, -2, "bitrate"); // L: ? ifacetbl table } lua_setfield(L, -2, "wireless"); // L: ? ifacetbl } static void inject_ethernet_info(lua_State *L, struct ifaddrs *addr, int sockfd) { if (sockfd < 0) { return; } uint32_t speed = get_ethernet_speed(sockfd, addr->ifa_name); if (!speed) { return; } // L: ? ifacetbl lua_createtable(L, 0, 1); // L: ? ifacetbl table lua_pushnumber(L, speed); // L: ? ifacetbl table number lua_setfield(L, -2, "speed"); // L: ? ifacetbl table lua_setfield(L, -2, "ethernet"); // L: ? ifacetbl } static void begin_call(Priv *p, lua_State *L) { // L: ? if (p->new_overall_fmt) { lua_createtable(L, 0, 2); // L: ? outer_table } } static void end_call(Priv *p, lua_State *L, const char *what) { if (p->new_overall_fmt) { // L: ? outer_table table lua_setfield(L, -2, "data"); // L: ? outer_table lua_pushstring(L, what); // L: ? outer_table what lua_setfield(L, -2, "what"); // L: ? outer_table } else { // L: ? table lua_createtable(L, 0, 1); // L: ? table mt lua_pushstring(L, what); // L: ? table mt what lua_setfield(L, -2, "what"); // L: ? table mt lua_setmetatable(L, -2); // L: ? table } } static void make_call( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, bool refresh, const char *what) { struct ifaddrs *ifaddr; if (getifaddrs(&ifaddr) < 0) { LS_ERRF(pd, "getifaddrs: %s", ls_tls_strerror(errno)); report_error(pd, funcs); return; } Priv *p = pd->priv; lua_State *L = funcs.call_begin(pd->userdata); // L: ? begin_call(p, L); // L: ?... lua_newtable(L); // L: ?... table if (refresh) { string_set_reset(&p->wlan_ifaces); } for (struct ifaddrs *cur = ifaddr; cur; cur = cur->ifa_next) { lua_getfield(L, -1, cur->ifa_name); // L: ?... table ifacetbl if (lua_isnil(L, -1)) { lua_pop(L, 1); // L: ?... table lua_newtable(L); // L: ?... table ifacetbl lua_pushvalue(L, -1); // L: ?... table ifacetbl ifacetbl lua_setfield(L, -3, cur->ifa_name); // L: ?... table ifacetbl if (p->report_wireless) { bool is_wlan; if (refresh) { is_wlan = is_wlan_iface(cur->ifa_name); if (is_wlan) { string_set_add(&p->wlan_ifaces, cur->ifa_name); } } else { is_wlan = string_set_contains(p->wlan_ifaces, cur->ifa_name); } if (is_wlan) { inject_wireless_info(L, cur); // L: ?... table ifacetbl } } if (p->report_ethernet) { inject_ethernet_info(L, cur, p->eth_sockfd); // L: ?... table ifacetbl } } if (p->report_ip) { inject_ip_info(L, cur, p->new_ip_fmt); // L: ?... table ifacetbl } lua_pop(L, 1); // L: ?... table } // L: ?... table end_call(p, L, what); // L: ? result if (refresh) { string_set_freeze(&p->wlan_ifaces); } freeifaddrs(ifaddr); funcs.call_end(pd->userdata); } static ssize_t my_recvmsg(int fd_netlink, struct msghdr *msg, LS_TimeDelta tmo, int fd_extra) { struct pollfd pfds[2] = { {.fd = fd_netlink, .events = POLLIN}, {.fd = fd_extra, .events = POLLIN}, }; int poll_rc = ls_poll(pfds, 2, tmo); if (poll_rc < 0) { // 'poll()' failed somewhy. return -1; } if (poll_rc == 0) { // Timeout reached. errno = EAGAIN; return -1; } if (pfds[1].revents & POLLIN) { // We have some input on /fd_extra/. char dummy; ssize_t nread = read(fd_extra, &dummy, 1); (void) nread; errno = 0; return -1; } // If 'recvmsg()' below fails with /EAGAIN/ or /EWOULDBLOCK/, we do exactly the right // thing: return -1 and set errno to /EAGAIN/ or /EWOULDBLOCK/, which signals a timeout. return recvmsg(fd_netlink, msg, 0); } static bool interpret_nl_msg(LuastatusPluginData *pd, char *msg_buf, size_t len) { for ( struct nlmsghdr *nh = (struct nlmsghdr *) msg_buf; NLMSG_OK(nh, len); nh = NLMSG_NEXT(nh, len)) { if (nh->nlmsg_type == NLMSG_DONE) { // end of multipart message break; } if (nh->nlmsg_type == NLMSG_ERROR) { if (nh->nlmsg_len < NLMSG_LENGTH(sizeof(struct nlmsgerr))) { LS_ERRF(pd, "netlink error message truncated"); return false; } struct nlmsgerr *e = NLMSG_DATA(nh); int errnum = e->error; if (errnum) { LS_ERRF(pd, "netlink error: %s", ls_tls_strerror(-errnum)); return false; } else { LS_WARNF(pd, "unexpected ACK - what's going on?"); continue; } } // we don't care about the message } return true; } static bool interact(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { Priv *p = pd->priv; // We allocate the buffer for netlink messages on the heap rather than on the stack, for two // reasons: // // 1. Alignment. The code in the netlink(7) man page seems to be wrong as it does not use // /__attribute__((aligned(...)))/, which is used, e.g., in the inotify(7) example. // // 2. Stack space is not free, and 8K is quite a large allocation for the stack. // // As for the buffer size, netlink(7) says "8192 to avoid message truncation on platforms with // page size > 4096". enum { NBUF = 8192 }; bool ret = false; char *buf = LS_XNEW(char, NBUF); int fd = -1; fd = ls_cloexec_socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (fd < 0) { LS_FATALF(pd, "socket: %s", ls_tls_strerror(errno)); goto error; } ls_make_nonblock(fd); struct sockaddr_nl sa = { .nl_family = AF_NETLINK, .nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR, }; if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0) { LS_FATALF(pd, "bind: %s", ls_tls_strerror(errno)); goto error; } LS_TimeDelta TD = ls_double_to_TD(p->tmo, LS_TD_FOREVER); make_call(pd, funcs, true, "update"); while (1) { struct iovec iov = {.iov_base = buf, .iov_len = NBUF}; struct msghdr msg = {.msg_iov = &iov, .msg_iovlen = 1}; ssize_t len = my_recvmsg(fd, &msg, TD, p->pipefds[0]); if (len < 0) { if (errno == 0) { make_call(pd, funcs, false, "self_pipe"); continue; } else if (errno == EINTR) { make_call(pd, funcs, true, "update"); continue; } else if (LS_IS_EAGAIN(errno)) { make_call(pd, funcs, false, "timeout"); continue; } else if (errno == ENOBUFS) { ret = true; LS_WARNF(pd, "ENOBUFS - kernel's socket buffer is full"); goto error; } else { LS_FATALF(pd, "my_recvmsg: %s", ls_tls_strerror(errno)); goto error; } } if (!interpret_nl_msg(pd, buf, len)) { ret = true; goto error; } make_call(pd, funcs, true, "update"); } error: free(buf); ls_close(fd); return ret; } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { while (interact(pd, funcs)) { // a non-fatal error occurred report_error(pd, funcs); ls_sleep_simple(5.0); LS_INFOF(pd, "resynchronizing"); } } LuastatusPluginIface_v1 luastatus_plugin_iface_v1 = { .init = init, .register_funcs = register_funcs, .run = run, .destroy = destroy, }; ================================================ FILE: plugins/network-linux/string_set.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "string_set.h" #include #include #include "libls/ls_alloc_utils.h" #include "libls/ls_compdep.h" #include "libls/ls_freemem.h" #include "libls/ls_panic.h" StringSet string_set_new(void) { return (StringSet) {NULL, 0, 0}; } void string_set_reset(StringSet *s) { for (size_t i = 0; i < s->size; ++i) { free(s->data[i]); } s->data = LS_M_FREEMEM(s->data, &s->size, &s->capacity); } void string_set_add(StringSet *s, const char *val) { LS_ASSERT(val != NULL); if (s->size == s->capacity) { s->data = LS_M_X2REALLOC(s->data, &s->capacity); } s->data[s->size++] = ls_xstrdup(val); } static int elem_cmp(const void *a, const void *b) { return strcmp(* (const char *const *) a, * (const char *const *) b); } void string_set_freeze(StringSet *s) { if (s->size) { qsort(s->data, s->size, sizeof(char *), elem_cmp); } } bool string_set_contains(StringSet s, const char *val) { LS_ASSERT(val != NULL); if (!s.size) { return false; } return bsearch(&val, s.data, s.size, sizeof(char *), elem_cmp) != NULL; } void string_set_destroy(StringSet s) { for (size_t i = 0; i < s.size; ++i) { free(s.data[i]); } free(s.data); } ================================================ FILE: plugins/network-linux/string_set.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef string_set_h_ #define string_set_h_ #include #include typedef struct { char **data; size_t size; size_t capacity; } StringSet; StringSet string_set_new(void); // Clears and unfreezes the set. void string_set_reset(StringSet *s); void string_set_add(StringSet *s, const char *val); void string_set_freeze(StringSet *s); bool string_set_contains(StringSet s, const char *val); void string_set_destroy(StringSet s); #endif ================================================ FILE: plugins/network-linux/wireless_info.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "wireless_info.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Adapted from i3status/src/print_wireless_info.c static void find_ssid(uint8_t *ies, uint32_t ies_len, uint8_t **ssid, uint32_t *ssid_len) { enum { WLAN_EID_SSID = 0 }; for (;;) { if (ies_len < 2) { goto not_found; } uint32_t cur_len = ies[1] + 2; if (cur_len > ies_len) { goto not_found; } if (ies[0] == WLAN_EID_SSID) { *ssid = ies + 2; *ssid_len = cur_len - 2; return; } ies_len -= cur_len; ies += cur_len; } not_found: *ssid = NULL; *ssid_len = 0; } static int gwi_sta_cb(struct nl_msg *msg, void *vud) { WirelessInfo *info = vud; struct nlattr *tb[NL80211_ATTR_MAX + 1]; struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg)); struct nlattr *sinfo[NL80211_STA_INFO_MAX + 1]; struct nlattr *rinfo[NL80211_RATE_INFO_MAX + 1]; struct nla_policy stats_policy[NL80211_STA_INFO_MAX + 1] = { [NL80211_STA_INFO_RX_BITRATE] = {.type = NLA_NESTED}, [NL80211_STA_INFO_SIGNAL] = {.type = NLA_U8}, }; struct nla_policy rate_policy[NL80211_RATE_INFO_MAX + 1] = { [NL80211_RATE_INFO_BITRATE] = {.type = NLA_U16}, }; if (nla_parse( tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0), genlmsg_attrlen(gnlh, 0), NULL) < 0) { return NL_SKIP; } if (!tb[NL80211_ATTR_STA_INFO]) return NL_SKIP; if (nla_parse_nested( sinfo, NL80211_STA_INFO_MAX, tb[NL80211_ATTR_STA_INFO], stats_policy) < 0) { return NL_SKIP; } if (sinfo[NL80211_STA_INFO_SIGNAL]) { info->flags |= HAS_SIGNAL_DBM; info->signal_dbm = (int8_t) nla_get_u8(sinfo[NL80211_STA_INFO_SIGNAL]); } if (!sinfo[NL80211_STA_INFO_RX_BITRATE]) return NL_SKIP; if (nla_parse_nested( rinfo, NL80211_RATE_INFO_MAX, sinfo[NL80211_STA_INFO_RX_BITRATE], rate_policy) < 0) { return NL_SKIP; } if (!rinfo[NL80211_RATE_INFO_BITRATE]) return NL_SKIP; info->flags |= HAS_BITRATE; info->bitrate = nla_get_u16(rinfo[NL80211_RATE_INFO_BITRATE]); return NL_SKIP; } static int gwi_scan_cb(struct nl_msg *msg, void *vud) { WirelessInfo *info = vud; struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg)); struct nlattr *tb[NL80211_ATTR_MAX + 1]; struct nlattr *bss[NL80211_BSS_MAX + 1]; struct nla_policy bss_policy[NL80211_BSS_MAX + 1] = { [NL80211_BSS_FREQUENCY] = {.type = NLA_U32}, [NL80211_BSS_BSSID] = {.type = NLA_UNSPEC}, [NL80211_BSS_INFORMATION_ELEMENTS] = {.type = NLA_UNSPEC}, [NL80211_BSS_STATUS] = {.type = NLA_U32}, }; if (nla_parse( tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0), genlmsg_attrlen(gnlh, 0), NULL) < 0) { return NL_SKIP; } if (!tb[NL80211_ATTR_BSS]) return NL_SKIP; if (nla_parse_nested( bss, NL80211_BSS_MAX, tb[NL80211_ATTR_BSS], bss_policy) < 0) { return NL_SKIP; } if (!bss[NL80211_BSS_STATUS]) return NL_SKIP; uint32_t status = nla_get_u32(bss[NL80211_BSS_STATUS]); if (status != NL80211_BSS_STATUS_ASSOCIATED && status != NL80211_BSS_STATUS_IBSS_JOINED) { return NL_SKIP; } if (!bss[NL80211_BSS_BSSID]) return NL_SKIP; memcpy(info->bssid, nla_data(bss[NL80211_BSS_BSSID]), ETH_ALEN); if (bss[NL80211_BSS_FREQUENCY]) { info->flags |= HAS_FREQUENCY; info->frequency = ((double) nla_get_u32(bss[NL80211_BSS_FREQUENCY])) * 1e6; } if (bss[NL80211_BSS_INFORMATION_ELEMENTS]) { uint8_t *ssid; uint32_t ssid_len; find_ssid( nla_data(bss[NL80211_BSS_INFORMATION_ELEMENTS]), nla_len(bss[NL80211_BSS_INFORMATION_ELEMENTS]), &ssid, &ssid_len); if (ssid && ssid_len) { info->flags |= HAS_ESSID; snprintf(info->essid, sizeof(info->essid), "%.*s", (int) ssid_len, ssid); } } return NL_SKIP; } bool get_wireless_info(const char *iface, WirelessInfo *info) { memset(info, 0, sizeof(WirelessInfo)); bool ok = false; struct nl_sock *sk = NULL; struct nl_msg *msg = NULL; int r; if (!(sk = nl_socket_alloc())) goto done; if (genl_connect(sk) != 0) goto done; if (nl_socket_modify_cb(sk, NL_CB_VALID, NL_CB_CUSTOM, gwi_scan_cb, info) < 0) goto done; int nl80211_id = genl_ctrl_resolve(sk, "nl80211"); if (nl80211_id < 0) goto done; unsigned ifidx = if_nametoindex(iface); if (ifidx == 0) goto done; if (!(msg = nlmsg_alloc())) goto done; if (!genlmsg_put( msg, NL_AUTO_PORT, NL_AUTO_SEQ, nl80211_id, 0, NLM_F_DUMP, NL80211_CMD_GET_SCAN, 0) ) { goto done; } if (nla_put_u32(msg, NL80211_ATTR_IFINDEX, ifidx) < 0) goto done; r = nl_send_sync(sk, msg); msg = NULL; if (r < 0) goto done; if (nl_socket_modify_cb(sk, NL_CB_VALID, NL_CB_CUSTOM, gwi_sta_cb, info) < 0) goto done; if (!(msg = nlmsg_alloc())) goto done; if (!genlmsg_put( msg, NL_AUTO_PORT, NL_AUTO_SEQ, nl80211_id, 0, NLM_F_DUMP, NL80211_CMD_GET_STATION, 0) ) { goto done; } if (nla_put_u32(msg, NL80211_ATTR_IFINDEX, ifidx) < 0) goto done; if (nla_put(msg, NL80211_ATTR_MAC, 6, info->bssid) < 0) goto done; r = nl_send_sync(sk, msg); msg = NULL; if (r < 0) goto done; ok = true; done: if (msg) nlmsg_free(msg); if (sk) nl_socket_free(sk); return ok; } ================================================ FILE: plugins/network-linux/wireless_info.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef wireless_info_h_ #define wireless_info_h_ #include #include #include enum { ESSID_MAX = 32 }; enum { HAS_ESSID = 1 << 0, HAS_SIGNAL_DBM = 1 << 1, HAS_BITRATE = 1 << 2, HAS_FREQUENCY = 1 << 3, }; typedef struct { int flags; char essid[ESSID_MAX + 1]; uint8_t bssid[ETH_ALEN]; int signal_dbm; unsigned bitrate; // in units of 100 kbit/s double frequency; } WirelessInfo; bool get_wireless_info(const char *iface, WirelessInfo *info); #endif ================================================ FILE: plugins/network-rate-linux/CMakeLists.txt ================================================ install (FILES network-rate-linux.lua DESTINATION ${LUA_PLUGINS_DIR}) luastatus_add_man_page (README.rst luastatus-plugin-network-rate-linux 7) ================================================ FILE: plugins/network-rate-linux/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-network-rate-linux .. :X-man-page-only: ################################### .. :X-man-page-only: .. :X-man-page-only: ################################################ .. :X-man-page-only: Linux-specific network rate plugin for luastatus .. :X-man-page-only: ################################################ .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This derived plugin periodically polls Linux ``procfs`` for the network receive/send rate (traffic usage per unit of time). Functions ========= The following functions are provided: * ``reader_make([iface_filter])`` Creates a *reader* that can be used to query network rates for network interfaces that match the filter. If specified, ``iface_filter`` must be a function that takes a string (interface name) and return ``true`` if the reader should should report network rates for this interface, ``false`` or ``nil`` otherwise. If ``iface_filter`` is not specified the reader will report network rates for all interfaces. * ``reader_read(reader, divisor[, in_array_form])`` Queries network rates using the given reader. ``reader`` must be a reader instance returned by ``reader_make``. For a given interface, we define a *datum* as a table of form with keys ``"R"`` and ``"S"``; the ``"R"``/``"S"`` fields contain the increase in the number of bytes received/sent, correspondingly, with this interface, since the last query with this reader, divided by ``divisor``. Normally, ``divisor`` should be the period with which you query the network rates with this reader; if not applicable, set ``divisor`` to 1 and interpret the values yourself. If ``in_array_form`` is ``true``, the result will be an array (a table with numeric keys) of ``{iface_name, datum}`` entries, in the same order as reported by the kernel. Otherwise (if ``in_array_form`` is not specified or ``false``), the result will be a dictionary (a table with string keys) with interface names as keys and datums as values. * ``widget(tbl)`` Constructs a ``widget`` table required by luastatus. ``tbl`` is a table with the following fields: **(required)** - ``cb``: a function The callback to call each ``period`` (see below) seconds with the network rate data. It is called with a single argument that is equivalent to ``reader_read`` return value (please refer to the description of that function above). **(optional)** Note: ``iface_filter``, ``iface_only`` and ``iface_except`` are mutually exclusive. If neither is specified, network rates for all interfaces will be reported. - ``iface_filter``: a function If specified, this must be a function that takes a string (interface name) and return ``true`` if we are interested in network rates for this interface, ``false`` or ``nil`` otherwise. - ``iface_only``/``iface_except``: a string or a table These options can be used to specify a whitelist/blacklist, correspondingly, for network interfaces that we are interested in. If it is a string, it will be interpreted as an interface name; only interface with this name will be whitelisted/blacklisted. If it is a table with numeric keys, it will be interpreted as a list of interface names that should be whitelisted/blacklisted. If it is a table with string keys, it will be interpreted as a lookup table: interface names that correspond to a truth-y (anything except ``false`` and ``nil``) value in this table will be whitelisted/blacklisted. If it is an empty table, no interfaces will be whitelisted/blacklisted. - ``in_array_form``: a boolean Please refer to the description of ``in_array_form`` argument to ``reader_read`` function above. Defaults to ``false``. - ``period``: a number The period, in seconds, with which the callback function will be called. This will also be a divisor (all rates will be divided by this value so that the rates be per ``period`` seconds instead of 1 second). Please refer to the description of ``divisor`` argument to ``reader_read`` function above for more information. Defaults to 1. - ``event`` The ``event`` entry of the resulting table (see ``luastatus`` documentation for the description of ``widget.event`` field). ================================================ FILE: plugins/network-rate-linux/network-rate-linux.lua ================================================ --[[ Copyright (C) 2015-2025 luastatus developers This file is part of luastatus. luastatus is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. luastatus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with luastatus. If not, see . --]] local P = {} local DEFAULT_PROCPATH = '/proc' local ACCEPT_ANY_IFACE_FILTER = function(_) return true end --[[ /proc/net/dev looks like this: Inter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed docker0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 wlp2s0: 39893402 38711 0 0 0 0 0 0 3676924 27205 0 0 0 0 0 0 lo: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 --]] local LINE_PATTERN = '^%s*(%S+):' .. ('%s+(%d+)' .. ('%s+%d+'):rep(7)):rep(2) .. '%s*$' function P.reader_new(iface_filter) return { iface_filter = iface_filter or ACCEPT_ANY_IFACE_FILTER, _procpath = DEFAULT_PROCPATH, last_recv = {}, last_sent = {}, } end local function parse_line(line, reader, divisor) local iface, recv_str, sent_str = line:match(LINE_PATTERN) if not (iface and recv_str and sent_str) then return nil, nil end if not reader.iface_filter(iface) then return nil, nil end local recv = assert(tonumber(recv_str)) local sent = assert(tonumber(sent_str)) local prev_recv, prev_sent = reader.last_recv[iface], reader.last_sent[iface] local datum = nil if prev_recv and prev_sent then local delta_recv = recv - prev_recv local delta_sent = sent - prev_sent if (delta_recv >= 0 and delta_sent >= 0) and (recv > 0 and sent > 0) then datum = {R = delta_recv / divisor, S = delta_sent / divisor} end end reader.last_recv[iface] = recv reader.last_sent[iface] = sent return datum, iface end local function do_with_file(f, callback) local is_ok, err = pcall(callback) f:close() if not is_ok then error(err) end end function P.reader_read(reader, divisor, in_array_form) local f = assert(io.open(reader._procpath .. '/net/dev', 'r')) local res = {} do_with_file(f, function() for line in f:lines() do if not line:find('|') then -- skip the "header" lines local datum, iface = parse_line(line, reader, divisor) if datum then if in_array_form then res[#res + 1] = {iface, datum} else res[iface] = datum end end end end end) return res end local function mkfilter(x, negate) local match_table = {} if type(x) == 'string' then match_table[x] = true elseif type(x) == 'table' then if type(next(x)) == 'number' then -- x is an "array" for _, v in ipairs(x) do match_table[v] = true end else -- x is a "dict" (or empty) for k, v in pairs(x) do if v then match_table[k] = true end end end else error('invalid iface_only/iface_except value (expected string or table)') end if negate then return function(iface) return not match_table[iface] end else return function(iface) return match_table[iface] end end end function P.widget(tbl) local iface_filter if tbl.iface_only then iface_filter = mkfilter(tbl.iface_only, false) elseif tbl.iface_except then iface_filter = mkfilter(tbl.iface_except, true) else iface_filter = tbl.iface_filter end local period = tbl.period or 1 local reader = P.reader_new(iface_filter) reader._procpath = tbl._procpath or DEFAULT_PROCPATH return { plugin = 'timer', opts = { period = period, }, cb = function(_) return tbl.cb(P.reader_read(reader, period, tbl.in_array_form)) end, event = tbl.event, } end return P ================================================ FILE: plugins/pipe/CMakeLists.txt ================================================ install (FILES pipe.lua DESTINATION ${LUA_PLUGINS_DIR}) luastatus_add_man_page (README.rst luastatus-plugin-pipe 7) ================================================ FILE: plugins/pipe/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-pipe .. :X-man-page-only: ##################### .. :X-man-page-only: .. :X-man-page-only: ########################################## .. :X-man-page-only: process output reader plugin for luastatus .. :X-man-page-only: ########################################## .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== **NOTE**: this is a legacy plugin. Please use ``pipev2`` plugin instead. This derived plugin monitors the output of a process and calls the callback function whenever it produces a line. Functions ========= The following functions are provided: * ``shell_escape(x)`` If ``x`` is a string, escapes it as a shell argument; if ``x`` is an array of strings, escapes them and then joins by space. * ``widget(tbl)`` Constructs a ``widget`` table required by luastatus. ``tbl`` is a table with the following fields: **(required)** - ``command``: string ``/bin/sh`` command to spawn. - ``cb``: function The callback that will be called with a line produced by the spawned process each time one is available. **(optional)** - ``on_eof``: function Callback to be called when the spawned process closes its stdout. Default is to simply hang to prevent busy-looping forever. - ``event`` The ``event`` entry of the resulting table (see ``luastatus`` documentation for the description of ``widget.event`` field). ================================================ FILE: plugins/pipe/pipe.lua ================================================ --[[ Copyright (C) 2015-2025 luastatus developers This file is part of luastatus. luastatus is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. luastatus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with luastatus. If not, see . --]] local P = {} function P.shell_escape(x) if type(x) == 'string' then if x:find('\0') then error('shell argument contains NUL character') end return "'" .. x:gsub("'", "'\\''") .. "'" elseif type(x) == 'table' then local t = {} for _, arg in ipairs(x) do t[#t + 1] = P.shell_escape(arg) end return table.concat(t, ' ') else error('argument type is neither string nor table') end end function P.widget(tbl) local f = assert(io.popen(tbl.command, 'r')) return { plugin = 'timer', opts = {period = 0}, cb = function() local r = f:read('*line') if r ~= nil then return tbl.cb(r) end if tbl.on_eof ~= nil then tbl.on_eof() else luastatus.plugin.push_period(3600) error('child process has closed its stdout') end end, event = tbl.event, } end return P ================================================ FILE: plugins/pipev2/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_plugin ( plugin-pipev2 $ $ ${sources} ) target_compile_definitions (plugin-pipev2 PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-pipev2 LUA) target_include_directories (plugin-pipev2 PUBLIC "${PROJECT_SOURCE_DIR}") # find pthreads set (CMAKE_THREAD_PREFER_PTHREAD TRUE) set (THREADS_PREFER_PTHREAD_FLAG TRUE) find_package (Threads REQUIRED) # link aginst pthread target_link_libraries (plugin-pipev2 PUBLIC Threads::Threads) luastatus_add_man_page (README.rst luastatus-plugin-pipev2 7) ================================================ FILE: plugins/pipev2/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-pipev2 .. :X-man-page-only: ####################### .. :X-man-page-only: .. :X-man-page-only: ############################## .. :X-man-page-only: pipe plugin for luastatus (v2) .. :X-man-page-only: ############################## .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This plugin spawns a process and then reads lines (or chunks separated by some other configured delimiter) from its stdout. It can optionally redirect the child process' stdin as well. This way, a widget is able to write data into its stdin, thus establishing bidirectional communication. It can also send signals to the child process. Options ======= The following options are supported: * ``argv``: array of string (**required**) Program name or path, followed by the arguments. * ``delimiter``: string Delimiter to use for breaking stream of bytes into chunks. Defaults to ``\n``, which means report text lines. If specified, must be exactly one byte long. * ``pipe_stdin``: boolean Whether or not to rediect the child process' stdin. If enabled, the widget will be able to write data into its stdin, thus establishing bidirectional communication. Defaults to ``false``. * ``greet``: boolean Whether or not to call the callback in the beginning with ``what="hello"``. * ``bye``: boolean Whether or not to call the callback in the end (after the process has died) with ``what="bye"``. ``cb`` argument =============== A table with ``what`` key: * If it's ``"hello"``, then the ``greet`` option was enabled, and the callback is called before any other calls. * If it's ``"line"``, then the child process has just produced a line (or, if a custom delimiter is used, a chunk separated by the delimiter). The ``line`` field of the table contains the line itself. * If it's ``"bye"``, then the ``bye`` option was enabled, and the child process has just died. Functions ========= This plugin provides the following functions: * ``luastatus.plugin.write_to_stdin(bytes)`` Writes ``bytes``, which must be a string, into the child process' stdin. Only available if ``pipe_stdin`` option was enabled. On success, it returns ``true``. On failure, it returns ``false, err_msg``, where ``err_msg`` is the error message, but see `Notes`_. * ``luastatus.plugin.kill([signal])`` Sends a signal to the child process. If ``signal`` is not passed or is ``nil``, SIGTERM is implied. ``signal`` can be either a string with the spelling of a signal (e.g. ``"SIGUSR1"``) or a signal number (e.g. 9). On success, it returns ``true``. On failure, it returns ``false, err_msg``, where ``err_msg`` is the error message, but see `Notes`_. For the list of supported signal spellings, see `Supported signal names`_. * ``luastatus.plugin.get_sigrt_bounds()`` Returns ``SIGRTMIN, SIGRTMAX``. Might be useful if you want to send real-time signals and don't want to depend on OS/architecture. Supported signal names ---------------------- All signals defined by POSIX.1-2008 (without extensions) are supported: SIGABRT, SIGALRM, SIGBUS, SIGCHLD, SIGCONT, SIGFPE, SIGHUP, SIGILL, SIGINT, SIGKILL, SIGPIPE, SIGQUIT, SIGSEGV, SIGSTOP, SIGTERM, SIGTSTP, SIGTTIN, SIGTTOU, SIGUSR1, SIGUSR2, SIGURG. Notes ----- If the child process has not been spawned yet, ``write_to_stdin`` and ``kill`` throw an error instead of returning ``false, err_msg``. This is only possible if the function is being invoked from an event handler. So, if you use this function from an event handler, you should consider this possibility. ================================================ FILE: plugins/pipev2/launch.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "launch.h" #include #include #include #include #include #include #include #include "libls/ls_panic.h" #include "libls/ls_osdep.h" static inline void unmake_cloexec(int fd) { int flags = fcntl(fd, F_GETFD); if (flags < 0) { LS_PANIC_WITH_ERRNUM("fcntl (F_GETFD) failed", errno); } flags &= ~FD_CLOEXEC; if (fcntl(fd, F_SETFD, flags) < 0) { LS_PANIC_WITH_ERRNUM("fcntl (F_SETFD) failed", errno); } } typedef struct { int fds[2]; int their_end; } Pipe; #define PIPE_INIT_EMPTY {.fds = {-1, -1}, .their_end = -1} static inline bool pipe_is_empty(Pipe *p) { return p->their_end < 0; } static inline int pipe_make(Pipe *p, int their_end) { if (ls_cloexec_pipe(p->fds) < 0) { return -1; } p->their_end = their_end; unmake_cloexec(p->fds[their_end]); return 0; } static inline int pipe_finalize(Pipe *p) { if (pipe_is_empty(p)) { return -1; } close(p->fds[p->their_end]); // The following line sets /our_end/ to: // * 1 if /p->their_end/ is 0; // * 0 if /p->their_end/ is 1. int our_end = 1 ^ p->their_end; return p->fds[our_end]; } static inline void pipe_destroy(Pipe *p) { if (pipe_is_empty(p)) { return; } close(p->fds[0]); close(p->fds[1]); } static inline void add_redir(posix_spawn_file_actions_t *fa, Pipe *p) { if (pipe_is_empty(p)) { return; } int fd_old = p->fds[p->their_end]; int fd_new = p->their_end; if (fd_old == fd_new) { return; } LS_PTH_CHECK(posix_spawn_file_actions_adddup2(fa, fd_old, fd_new)); LS_PTH_CHECK(posix_spawn_file_actions_addclose(fa, fd_old)); } extern char **environ; int launch(const LaunchArg *argv, bool redir_stdin, LaunchResult *res, LaunchError *err) { LS_ASSERT(argv != NULL); LS_ASSERT(argv[0] != NULL); Pipe p_stdin = PIPE_INIT_EMPTY; Pipe p_stdout = PIPE_INIT_EMPTY; if (redir_stdin) { if (pipe_make(&p_stdin, /*their_end=*/ 0) < 0) { err->where = "pipe"; err->errnum = errno; goto error; } } if (pipe_make(&p_stdout, /*their_end=*/ 1) < 0) { err->where = "pipe"; err->errnum = errno; goto error; } posix_spawn_file_actions_t fa; LS_PTH_CHECK(posix_spawn_file_actions_init(&fa)); add_redir(&fa, &p_stdin); add_redir(&fa, &p_stdout); pid_t pid; int rc = posix_spawnp( /*pid=*/ &pid, /*file=*/ argv[0], /*file_actions=*/ &fa, /*attrp=*/ NULL, /*argv=*/ argv, /*envp=*/ environ ); LS_PTH_CHECK(posix_spawn_file_actions_destroy(&fa)); if (rc != 0) { err->where = "posix_spawnp"; err->errnum = rc; goto error; } int fd_stdin = pipe_finalize(&p_stdin); int fd_stdout = pipe_finalize(&p_stdout); *res = (LaunchResult) { .pid = pid, .fd_stdin = fd_stdin, .fd_stdout = fd_stdout, }; return 0; error: pipe_destroy(&p_stdin); pipe_destroy(&p_stdout); return -1; } ================================================ FILE: plugins/pipev2/launch.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include typedef char *LaunchArg; typedef struct { pid_t pid; int fd_stdin; int fd_stdout; } LaunchResult; typedef struct { const char *where; int errnum; } LaunchError; int launch(const LaunchArg *argv, bool redir_stdin, LaunchResult *res, LaunchError *err); ================================================ FILE: plugins/pipev2/pipev2.c ================================================ /* * Copyright (C) 2015-2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include "libls/ls_alloc_utils.h" #include "libls/ls_panic.h" #include "libls/ls_io_utils.h" #include "libls/ls_tls_ebuf.h" #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "libmoonvisit/moonvisit.h" #include "launch.h" #include "utils.h" #include "sigdb.h" enum { PID_NOT_SPAWNED_YET = 0, PID_ALREADY_TERMINATED = -1, }; typedef struct { LaunchArg *data; size_t size; size_t capacity; } ArgsList; typedef struct { ArgsList argv; // The /getdelim()/ function requires the /int delim/ parameter to be "representable // as an unsigned char" (i.e. to be non-negative). // // Since the signedness of /char/ depends on the implementation, we store the // delimiter as an /unsigned char/. Then we can pass it to /getdelim()/ without cast. unsigned char delimiter; bool pipe_stdin; bool greet; bool bye; pthread_mutex_t child_mtx; int child_stdin_fd; pid_t child_pid; } Priv; static void args_list_add(ArgsList *x, const char *s) { char *new_elem = s ? ls_xstrdup(s) : NULL; if (x->size == x->capacity) { x->data = LS_M_X2REALLOC(x->data, &x->capacity); } x->data[x->size++] = new_elem; } static void args_list_destroy(ArgsList *x) { for (size_t i = 0; i < x->size; ++i) { free(x->data[i]); } free(x->data); } static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; args_list_destroy(&p->argv); LS_PTH_CHECK(pthread_mutex_destroy(&p->child_mtx)); ls_close(p->child_stdin_fd); free(p); } static int visit_argv_elem(MoonVisit *mv, void *ud, int kpos, int vpos) { (void) kpos; mv->where = "'argv' element"; Priv *p = ud; if (moon_visit_checktype_at(mv, NULL, vpos, LUA_TSTRING) < 0) { return -1; } const char *s = lua_tostring(mv->L, vpos); args_list_add(&p->argv, s); return 0; } static int visit_delimiter(MoonVisit *mv, void *ud, const char *s, size_t ns) { Priv *p = ud; if (ns != 1) { moon_visit_err(mv, "length of delimiter: expected 1, found %zu", ns); return -1; } p->delimiter = s[0]; return 0; } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .argv = {0}, .delimiter = '\n', .pipe_stdin = false, .greet = false, .bye = false, .child_stdin_fd = -1, .child_pid = PID_NOT_SPAWNED_YET, }; LS_PTH_CHECK(pthread_mutex_init(&p->child_mtx, NULL)); char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Parse argv if (moon_visit_table_f(&mv, -1, "argv", visit_argv_elem, p, false) < 0) { goto mverror; } if (!p->argv.size) { snprintf(errbuf, sizeof(errbuf), "'argv' array is empty"); goto error; } // Parse delimiter if (moon_visit_str_f(&mv, -1, "delimiter", visit_delimiter, p, true) < 0) { goto mverror; } // Parse pipe_stdin if (moon_visit_bool(&mv, -1, "pipe_stdin", &p->pipe_stdin, true) < 0) { goto mverror; } // Parse greet if (moon_visit_bool(&mv, -1, "greet", &p->greet, true) < 0) { goto mverror; } // Parse bye if (moon_visit_bool(&mv, -1, "bye", &p->bye, true) < 0) { goto mverror; } return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); error: destroy(pd); return LUASTATUS_ERR; } static int l_write_to_stdin(lua_State *L) { Priv *p = lua_touserdata(L, lua_upvalueindex(1)); if (!p->pipe_stdin) { return luaL_error(L, "'pipe_stdin' option was not enabled"); } size_t ndata; const char *data = luaL_checklstring(L, 1, &ndata); LS_PTH_CHECK(pthread_mutex_lock(&p->child_mtx)); int fd = p->child_stdin_fd; LS_PTH_CHECK(pthread_mutex_unlock(&p->child_mtx)); if (fd < 0) { return luaL_error(L, "process have not been created yet"); } if (utils_full_write(fd, data, ndata) >= 0) { lua_pushboolean(L, 1); return 1; } else { const char *err_descr = ls_tls_strerror(errno); lua_pushboolean(L, 0); lua_pushstring(L, err_descr); return 2; } } static int fetch_sig_num(lua_State *L) { if (lua_isnoneornil(L, 1)) { return SIGTERM; } if (lua_isnumber(L, 1)) { int res = lua_tointeger(L, 1); if (res < 0) { return luaL_argerror(L, 1, "number is negative or out of range"); } return res; } else if (lua_isstring(L, 1)) { const char *sig_name = lua_tostring(L, 1); int res = sigdb_lookup_num_by_name(sig_name); if (res < 0) { return luaL_argerror(L, 1, "unknown signal name"); } return res; } else { return luaL_error( L, "expected number of string as argument, found %s", luaL_typename(L, 1)); } } static int l_kill(lua_State *L) { Priv *p = lua_touserdata(L, lua_upvalueindex(1)); int sig_num = fetch_sig_num(L); // If /is_ok/ == 1: killed successfully. // If /is_ok/ == 0: error, error number in /err_num/. // If /is_ok/ == -1: process have not been created yet. int is_ok; int err_num; LS_PTH_CHECK(pthread_mutex_lock(&p->child_mtx)); pid_t pid = p->child_pid; if (pid > 0) { if (kill(pid, sig_num) < 0) { is_ok = 0; } else { is_ok = 1; } err_num = errno; } else { if (pid == PID_NOT_SPAWNED_YET) { is_ok = -1; } else { is_ok = 0; } err_num = ESRCH; } LS_PTH_CHECK(pthread_mutex_unlock(&p->child_mtx)); if (is_ok < 0) { return luaL_error(L, "process have not been created yet"); } if (is_ok) { lua_pushboolean(L, 1); return 1; } else { const char *err_descr = ls_tls_strerror(err_num); lua_pushboolean(L, 0); lua_pushstring(L, err_descr); return 2; } } static int l_get_sigrt_bounds(lua_State *L) { lua_pushinteger(L, SIGRTMIN); lua_pushinteger(L, SIGRTMAX); return 2; } static void register_funcs(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv; // L: table lua_pushlightuserdata(L, p); // L: table ud lua_pushcclosure(L, l_write_to_stdin, 1); // L: table func lua_setfield(L, -2, "write_to_stdin"); // L: table lua_pushlightuserdata(L, p); // L: table ud lua_pushcclosure(L, l_kill, 1); // L: table func lua_setfield(L, -2, "kill"); // L: table lua_pushcfunction(L, l_get_sigrt_bounds); // L: table func lua_setfield(L, -2, "get_sigrt_bounds"); // L: table } static void report_reason_of_death( LuastatusPluginData *pd, int wait_rc, int wait_status, int wait_errno) { if (wait_rc < 0) { LS_INFOF(pd, "waitpid: %s", ls_tls_strerror(wait_errno)); } else { if (WIFEXITED(wait_status)) { int exit_code = WEXITSTATUS(wait_status); LS_INFOF(pd, "child process exited with code %d", exit_code); } else if (WIFSIGNALED(wait_status)) { int term_sig = WTERMSIG(wait_status); LS_INFOF(pd, "child process was killed with signal %d", term_sig); } else { LS_INFOF(pd, "child process terminated in an unexpected way (%d)", wait_status); } } } static void do_cleanup_leftover(LuastatusPluginData *pd, LaunchResult leftover) { LS_INFOF(pd, "killing the spawned process with SIGKILL and waiting for it to terminate"); (void) kill(leftover.pid, SIGKILL); int wait_status = 0; int wait_rc = utils_waitpid(leftover.pid, &wait_status); int wait_errno = errno; report_reason_of_death(pd, wait_rc, wait_status, wait_errno); ls_close(leftover.fd_stdin); ls_close(leftover.fd_stdout); } static bool do_spawn(LuastatusPluginData *pd, FILE **out_f) { Priv *p = pd->priv; args_list_add(&p->argv, NULL); LaunchResult res; LaunchError err; if (launch(p->argv.data, p->pipe_stdin, &res, &err) < 0) { LS_FATALF(pd, "cannot spawn process: %s: %s", err.where, ls_tls_strerror(err.errnum)); return false; } FILE *f = fdopen(res.fd_stdout, "r"); if (!f) { LS_FATALF(pd, "fdopen: %s", ls_tls_strerror(errno)); do_cleanup_leftover(pd, res); return false; } *out_f = f; LS_PTH_CHECK(pthread_mutex_lock(&p->child_mtx)); p->child_stdin_fd = res.fd_stdin; p->child_pid = res.pid; LS_PTH_CHECK(pthread_mutex_unlock(&p->child_mtx)); return true; } static void do_wait(LuastatusPluginData *pd) { Priv *p = pd->priv; LS_PTH_CHECK(pthread_mutex_lock(&p->child_mtx)); int wait_status = 0; int wait_rc = utils_waitpid(p->child_pid, &wait_status); int wait_errno = errno; p->child_pid = PID_ALREADY_TERMINATED; LS_PTH_CHECK(pthread_mutex_unlock(&p->child_mtx)); report_reason_of_death(pd, wait_rc, wait_status, wait_errno); } static void make_call_simple( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, const char *what) { lua_State *L = funcs.call_begin(pd->userdata); // L: ? lua_createtable(L, 0, 1); // L: ? table lua_pushstring(L, what); // L: ? table str lua_setfield(L, -2, "what"); // L: ? table funcs.call_end(pd->userdata); } static void make_call_line( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, const char *line, size_t nline) { lua_State *L = funcs.call_begin(pd->userdata); // L: ? lua_createtable(L, 0, 2); // L: ? table lua_pushstring(L, "line"); // L: ? table str lua_setfield(L, -2, "what"); // L: ? table lua_pushlstring(L, line, nline); // L: ? table str lua_setfield(L, -2, "line"); // L: ? table funcs.call_end(pd->userdata); } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { Priv *p = pd->priv; FILE *f; if (!do_spawn(pd, &f)) { return; } if (p->greet) { make_call_simple(pd, funcs, "hello"); } char *buf = NULL; size_t nbuf = 512; for (;;) { ssize_t r = getdelim(&buf, &nbuf, p->delimiter, f); if (r <= 0) { break; } make_call_line(pd, funcs, buf, r - 1); } if (feof(f)) { LS_ERRF(pd, "child process closed its stdout"); } else { LS_ERRF(pd, "read error: %s", ls_tls_strerror(errno)); } fclose(f); do_wait(pd); if (p->bye) { make_call_simple(pd, funcs, "bye"); for (;;) { pause(); } } } LuastatusPluginIface luastatus_plugin_iface_v1 = { .init = init, .register_funcs = register_funcs, .run = run, .destroy = destroy, }; ================================================ FILE: plugins/pipev2/sigdb.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "sigdb.h" #include #include #include #include "libls/ls_panic.h" typedef struct { const char *sig_name; int sig_num; } SigSpec; static const SigSpec SIG_SPECS[] = { {"SIGABRT", SIGABRT}, {"SIGALRM", SIGALRM}, {"SIGBUS", SIGBUS}, {"SIGCHLD", SIGCHLD}, {"SIGCONT", SIGCONT}, {"SIGFPE", SIGFPE}, {"SIGHUP", SIGHUP}, {"SIGILL", SIGILL}, {"SIGINT", SIGINT}, {"SIGKILL", SIGKILL}, {"SIGPIPE", SIGPIPE}, {"SIGQUIT", SIGQUIT}, {"SIGSEGV", SIGSEGV}, {"SIGSTOP", SIGSTOP}, {"SIGTERM", SIGTERM}, {"SIGTSTP", SIGTSTP}, {"SIGTTIN", SIGTTIN}, {"SIGTTOU", SIGTTOU}, {"SIGUSR1", SIGUSR1}, {"SIGUSR2", SIGUSR2}, {"SIGURG", SIGURG}, {0}, }; int sigdb_lookup_num_by_name(const char *sig_name) { LS_ASSERT(sig_name != NULL); for (const SigSpec *p = SIG_SPECS; p->sig_name; ++p) { if (strcmp(sig_name, p->sig_name) == 0) { return p->sig_num; } } return -1; } ================================================ FILE: plugins/pipev2/sigdb.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once int sigdb_lookup_num_by_name(const char *sig_name); ================================================ FILE: plugins/pipev2/utils.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "utils.h" #include #include #include #include #include #include "libls/ls_panic.h" int utils_full_write(int fd, const char *data, size_t ndata) { size_t nwritten = 0; while (nwritten < ndata) { ssize_t w = write(fd, data + nwritten, ndata - nwritten); if (w < 0) { if (errno == EINTR) { continue; } return -1; } nwritten += w; } return 0; } int utils_waitpid(pid_t pid, int *out_status) { LS_ASSERT(pid > 0); pid_t r; while ((r = waitpid(pid, out_status, 0)) < 0 && errno == EINTR) { // do nothing } return r < 0 ? -1 : 0; } ================================================ FILE: plugins/pipev2/utils.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include int utils_full_write(int fd, const char *data, size_t ndata); int utils_waitpid(pid_t pid, int *out_status); ================================================ FILE: plugins/pulse/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_plugin (plugin-pulse $ $ ${sources}) target_compile_definitions (plugin-pulse PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-pulse LUA) target_include_directories (plugin-pulse PUBLIC "${PROJECT_SOURCE_DIR}") find_package (PkgConfig REQUIRED) pkg_check_modules (PULSE REQUIRED libpulse) luastatus_target_build_with (plugin-pulse PULSE) luastatus_add_man_page (README.rst luastatus-plugin-pulse 7) ================================================ FILE: plugins/pulse/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-pulse .. :X-man-page-only: ###################### .. :X-man-page-only: .. :X-man-page-only: ############################### .. :X-man-page-only: PulseAudio plugin for luastatus .. :X-man-page-only: ############################### .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This plugin monitors the volume and mute status of a PulseAudio sink. Options ======= The following options are supported: * ``sink``: string Sink name; default is ``"@DEFAULT_SINK@"``. * ``make_self_pipe``: boolean If ``true``, the ``wake_up()`` function (see `Functions`_) will be available. ``cb`` argument =============== * If the ``make_self_pipe`` option is set to ``true``, and the callback is invoked because of the call to ``wake_up()``, then the argument is ``nil``; * otherwise, the argument is a table with the following entries: - ``cur``: integer Current volume level. - ``norm``: integer "Normal" (corresponding to 100%) volume level. - ``mute``: boolean Whether the sink is mute. - ``index``: integer Index of the default sink. - ``name``: string (**optional**, may be ``nil``) Name of the default sink. - ``desc``: string (**optional**, may be ``nil``) Description of the default sink. - ``port``: table with following entries, or ``nil`` if there is no active port: - ``name``: string (**optional**, may be ``nil``) Name of the default sink's active port. - ``desc``: string (**optional**, may be ``nil``) Description of the default sink's active port. - ``type``: string Type of the default sink's active port. Possible values are: + ``"unknown"`` + ``"aux"`` + ``"speaker"`` + ``"headphones"`` + ``"line"`` + ``"mic"`` + ``"headset"`` + ``"handset"`` + ``"earpiece"`` + ``"spdif"`` + ``"hdmi"`` + ``"tv"`` + ``"radio"`` + ``"video"`` + ``"usb"`` + ``"bluetooth"`` + ``"portable"`` + ``"handsfree"`` + ``"car"`` + ``"hifi"`` + ``"phone"`` + ``"network"`` + ``"analog"`` Functions ========= The following functions are provided: * ``luastatus.plugin.wake_up()`` Forces a call to ``cb`` (with ``nil`` argument). Only available if the ``make_self_pipe`` option was set to ``true``; otherwise, it throws an error. ================================================ FILE: plugins/pulse/pulse.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include #include #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "libmoonvisit/moonvisit.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_evloop_lfuncs.h" #include "libls/ls_time_utils.h" #include "libls/ls_io_utils.h" #include "libls/ls_panic.h" #ifdef PA_CHECK_VERSION # define MY_CHECK_VERSION(A_, B_, C_) PA_CHECK_VERSION(A_, B_, C_) #else # define MY_CHECK_VERSION(A_, B_, C_) 0 #endif // Note: some parts of this file are stolen from i3status' src/pulse.c. // This is fine since the BSD 3-Clause licence, under which it is licenced, is compatible with // LGPL-3.0. typedef struct { char *sink_name; int pipefds[2]; } Priv; static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; free(p->sink_name); ls_close(p->pipefds[0]); ls_close(p->pipefds[1]); free(p); } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .sink_name = NULL, .pipefds = {-1, -1}, }; char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Parse sink if (moon_visit_str(&mv, -1, "sink", &p->sink_name, NULL, true) < 0) goto mverror; if (!p->sink_name) p->sink_name = ls_xstrdup("@DEFAULT_SINK@"); // Parse make_self_pipe bool mkpipe = false; if (moon_visit_bool(&mv, -1, "make_self_pipe", &mkpipe, true) < 0) goto mverror; if (mkpipe) { if (ls_self_pipe_open(p->pipefds) < 0) { LS_FATALF(pd, "ls_self_pipe_open: %s", ls_tls_strerror(errno)); goto error; } } return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); error: destroy(pd); return LUASTATUS_ERR; } static void register_funcs(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv; // L: table ls_self_pipe_push_luafunc(p->pipefds, L); // L: table func lua_setfield(L, -2, "wake_up"); // L: table } typedef struct { LuastatusPluginData *pd; LuastatusPluginRunFuncs funcs; pa_mainloop *ml; uint32_t sink_idx; } UserData; static void self_pipe_cb( pa_mainloop_api *api, pa_io_event *e, int fd, pa_io_event_flags_t events, void *vud) { (void) api; (void) e; (void) events; char c; ssize_t unused = read(fd, &c, 1); (void) unused; UserData *ud = vud; lua_State *L = ud->funcs.call_begin(ud->pd->userdata); lua_pushnil(L); ud->funcs.call_end(ud->pd->userdata); } #if MY_CHECK_VERSION(14, 0, 0) static void push_port_type(lua_State *L, int type) { switch (type) { default: lua_pushstring(L, "unknown"); break; case PA_DEVICE_PORT_TYPE_UNKNOWN: lua_pushstring(L, "unknown"); break; case PA_DEVICE_PORT_TYPE_AUX: lua_pushstring(L, "aux"); break; case PA_DEVICE_PORT_TYPE_SPEAKER: lua_pushstring(L, "speaker"); break; case PA_DEVICE_PORT_TYPE_HEADPHONES: lua_pushstring(L, "headphones"); break; case PA_DEVICE_PORT_TYPE_LINE: lua_pushstring(L, "line"); break; case PA_DEVICE_PORT_TYPE_MIC: lua_pushstring(L, "mic"); break; case PA_DEVICE_PORT_TYPE_HEADSET: lua_pushstring(L, "headset"); break; case PA_DEVICE_PORT_TYPE_HANDSET: lua_pushstring(L, "handset"); break; case PA_DEVICE_PORT_TYPE_EARPIECE: lua_pushstring(L, "earpiece"); break; case PA_DEVICE_PORT_TYPE_SPDIF: lua_pushstring(L, "spdif"); break; case PA_DEVICE_PORT_TYPE_HDMI: lua_pushstring(L, "hdmi"); break; case PA_DEVICE_PORT_TYPE_TV: lua_pushstring(L, "tv"); break; case PA_DEVICE_PORT_TYPE_RADIO: lua_pushstring(L, "radio"); break; case PA_DEVICE_PORT_TYPE_VIDEO: lua_pushstring(L, "video"); break; case PA_DEVICE_PORT_TYPE_USB: lua_pushstring(L, "usb"); break; case PA_DEVICE_PORT_TYPE_BLUETOOTH: lua_pushstring(L, "bluetooth"); break; case PA_DEVICE_PORT_TYPE_PORTABLE: lua_pushstring(L, "portable"); break; case PA_DEVICE_PORT_TYPE_HANDSFREE: lua_pushstring(L, "handsfree"); break; case PA_DEVICE_PORT_TYPE_CAR: lua_pushstring(L, "car"); break; case PA_DEVICE_PORT_TYPE_HIFI: lua_pushstring(L, "hifi"); break; case PA_DEVICE_PORT_TYPE_PHONE: lua_pushstring(L, "phone"); break; case PA_DEVICE_PORT_TYPE_NETWORK: lua_pushstring(L, "network"); break; case PA_DEVICE_PORT_TYPE_ANALOG: lua_pushstring(L, "analog"); break; } } #endif static inline void push_str_or_nil(lua_State *L, const char *s) { if (s) { lua_pushstring(L, s); } else { lua_pushnil(L); } } static void store_volume_from_sink_cb( pa_context *c, const pa_sink_info *info, int eol, void *vud) { UserData *ud = vud; if (eol < 0) { if (pa_context_errno(c) == PA_ERR_NOENTITY) { return; } LS_ERRF(ud->pd, "PulseAudio error: %s", pa_strerror(pa_context_errno(c))); } else if (eol == 0) { if (info->index == ud->sink_idx) { lua_State *L = ud->funcs.call_begin(ud->pd->userdata); // L: ? lua_createtable(L, 0, 7); // L: ? table lua_pushinteger(L, pa_cvolume_avg(&info->volume)); // L: ? table integer lua_setfield(L, -2, "cur"); // L: ? table lua_pushinteger(L, PA_VOLUME_NORM); // L: ? table integer lua_setfield(L, -2, "norm"); // L: ? table lua_pushboolean(L, !!info->mute); // L: ? table boolean lua_setfield(L, -2, "mute"); // L: ? table lua_pushinteger(L, info->index); // L: ? table integer lua_setfield(L, -2, "index"); // L: ? table push_str_or_nil(L, info->name); // L: ? table string lua_setfield(L, -2, "name"); // L: ? table push_str_or_nil(L, info->description); // L: ? table string lua_setfield(L, -2, "desc"); // L: ? table #if MY_CHECK_VERSION(0, 9, 16) if (!info->active_port) { lua_pushnil(L); // L: ? table nil } else { lua_createtable(L, 0, 3); // L: ? table table push_str_or_nil(L, info->active_port->name); // L: ? table table string lua_setfield(L, -2, "name"); // L: ? table table push_str_or_nil(L, info->active_port->description); // L: ? table table string lua_setfield(L, -2, "desc"); // L: ? table table # if MY_CHECK_VERSION(14, 0, 0) push_port_type(L, info->active_port->type); // L: ? table table string lua_setfield(L, -2, "type"); // L: ? table table # endif } lua_setfield(L, -2, "port"); // L: ? table #endif ud->funcs.call_end(ud->pd->userdata); } } } static void store_sink_cb( pa_context *c, const pa_sink_info *info, int eol, void *vud) { UserData *ud = vud; if (info) { if (ud->sink_idx != info->index) { // sink has changed? ud->sink_idx = info->index; store_volume_from_sink_cb(c, info, eol, vud); } } } static void update_sink(pa_context *c, void *vud) { UserData *ud = vud; Priv *p = ud->pd->priv; pa_operation *o = pa_context_get_sink_info_by_name(c, p->sink_name, store_sink_cb, vud); if (o) { pa_operation_unref(o); } else { LS_ERRF(ud->pd, "pa_context_get_sink_info_by_name: %s", pa_strerror(pa_context_errno(c))); } } static void subscribe_cb( pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *vud) { UserData *ud = vud; if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) != PA_SUBSCRIPTION_EVENT_CHANGE) { return; } pa_subscription_event_type_t facility = t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; switch (facility) { case PA_SUBSCRIPTION_EVENT_SERVER: // server change event, see if the sink has changed update_sink(c, vud); break; case PA_SUBSCRIPTION_EVENT_SINK: { pa_operation *o = pa_context_get_sink_info_by_index( c, idx, store_volume_from_sink_cb, vud); if (o) { pa_operation_unref(o); } else { LS_ERRF(ud->pd, "pa_context_get_sink_info_by_index: %s", pa_strerror(pa_context_errno(c))); } } break; default: break; } } static void context_state_cb(pa_context *c, void *vud) { UserData *ud = vud; switch (pa_context_get_state(c)) { case PA_CONTEXT_UNCONNECTED: case PA_CONTEXT_CONNECTING: case PA_CONTEXT_AUTHORIZING: case PA_CONTEXT_SETTING_NAME: case PA_CONTEXT_TERMINATED: default: break; case PA_CONTEXT_READY: { pa_context_set_subscribe_callback(c, subscribe_cb, vud); update_sink(c, vud); pa_operation *o = pa_context_subscribe( c, PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SERVER, NULL, NULL); if (o) { pa_operation_unref(o); } else { LS_ERRF(ud->pd, "pa_context_subscribe: %s", pa_strerror(pa_context_errno(c))); } } break; case PA_CONTEXT_FAILED: // server disconnected us pa_mainloop_quit(ud->ml, 1); break; } } static bool iteration(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { bool ret = false; Priv *p = pd->priv; UserData ud = {.pd = pd, .funcs = funcs, .sink_idx = UINT32_MAX}; pa_mainloop_api *api = NULL; pa_context *ctx = NULL; pa_io_event *pipe_ev = NULL; if (!(ud.ml = pa_mainloop_new())) { LS_FATALF(pd, "pa_mainloop_new() failed"); goto error; } if (!(api = pa_mainloop_get_api(ud.ml))) { LS_FATALF(pd, "pa_mainloop_get_api() failed"); goto error; } pa_proplist *proplist = pa_proplist_new(); pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, "luastatus-plugin-pulse"); pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, "io.github.shdown.luastatus"); pa_proplist_sets(proplist, PA_PROP_APPLICATION_VERSION, "0.0.1"); ctx = pa_context_new_with_proplist(api, "luastatus-plugin-pulse", proplist); pa_proplist_free(proplist); if (!ctx) { LS_FATALF(pd, "pa_context_new_with_proplist() failed"); goto error; } pa_context_set_state_callback(ctx, context_state_cb, &ud); if (pa_context_connect(ctx, NULL, PA_CONTEXT_NOFAIL | PA_CONTEXT_NOAUTOSPAWN, NULL) < 0) { LS_FATALF(pd, "pa_context_connect: %s", pa_strerror(pa_context_errno(ctx))); goto error; } if (p->pipefds[0] >= 0) { pipe_ev = api->io_new(api, p->pipefds[0], PA_IO_EVENT_INPUT, self_pipe_cb, &ud); if (!pipe_ev) { LS_FATALF(pd, "io_new() failed"); goto error; } } ret = true; int ignored; if (pa_mainloop_run(ud.ml, &ignored) < 0) { LS_FATALF(pd, "pa_mainloop_run: %s", pa_strerror(pa_context_errno(ctx))); goto error; } error: if (pipe_ev) { LS_ASSERT(api != NULL); api->io_free(pipe_ev); } if (ctx) { pa_context_unref(ctx); } if (ud.ml) { pa_mainloop_free(ud.ml); } return ret; } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { for (;;) { if (!iteration(pd, funcs)) { ls_sleep_simple(5.0); } } } LuastatusPluginIface luastatus_plugin_iface_v1 = { .init = init, .register_funcs = register_funcs, .run = run, .destroy = destroy, }; ================================================ FILE: plugins/systemd-unit/CMakeLists.txt ================================================ install (FILES systemd-unit.lua DESTINATION ${LUA_PLUGINS_DIR}) luastatus_add_man_page (README.rst luastatus-plugin-systemd-unit 7) ================================================ FILE: plugins/systemd-unit/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-systemd-unit .. :X-man-page-only: ############################# .. :X-man-page-only: .. :X-man-page-only: ################################################## .. :X-man-page-only: Systemd unit state monitoring plugin for luastatus .. :X-man-page-only: ################################################## .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This derived plugin monitors the state of a systemd unit. Functions ========= The following functions are provided: * ``subscribe()`` Subscribes to the systemd D-Bus manager to receive unit state change notifications. * ``get_unit_path_by_unit_name(unit_name)`` Returns the D-Bus object path corresponding to the specified systemd unit name. * ``get_state_by_unit_path(unit_path)`` Returns the ``ActiveState`` property of the unit located at the given D-Bus object path. * ``get_state_by_unit_name(unit_name)`` Returns the current ``ActiveState`` propety for the specified systemd unit name. This is just a combination of ``get_state_by_unit_path`` and ``get_unit_path_by_unit_name``. * ``widget(tbl)`` Constructs a ``widget`` table required by luastatus. ``tbl`` must be a table with the following fields: **(required)** - ``unit_name``: string The name of the systemd unit to monitor (e.g., ``"tor.service"``). - ``cb``: function The callback function that will be called with the current unit state as a string. **(optional)** - ``no_throw``: boolean If set to ``true``, errors encountered while fetching the unit state will be caught, a warning will be printed to the console, and ``cb`` will be called with ``nil`` instead of propagating the error. Defaults to ``false``. - ``forced_refresh_interval``: number The timeout interval in seconds for forced state checks when D-Bus signals are not received. Defaults to ``30``. - ``event`` The ``event`` entry of the resulting table (see ``luastatus`` documentation for the description of ``widget.event`` field). ================================================ FILE: plugins/systemd-unit/systemd-unit.lua ================================================ --[[ Copyright (C) 2026 luastatus developers This file is part of luastatus. luastatus is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. luastatus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with luastatus. If not, see . --]] local function unwrap1_into_str(x) assert(type(x) == 'table') assert(#x == 1) local res = x[1] assert(type(res) == 'string') return res end local P = {} function P.subscribe() local is_ok, res = luastatus.plugin.call_method_str({ bus = 'system', dest = 'org.freedesktop.systemd1', object_path = '/org/freedesktop/systemd1', interface = 'org.freedesktop.systemd1.Manager', method = 'Subscribe', -- no "arg_str" parameter }) assert(is_ok, res) end function P.get_unit_path_by_unit_name(unit_name) local is_ok, res = luastatus.plugin.call_method_str({ bus = 'system', dest = 'org.freedesktop.systemd1', object_path = '/org/freedesktop/systemd1', interface = 'org.freedesktop.systemd1.Manager', method = 'GetUnit', arg_str = unit_name, }) assert(is_ok, res) return unwrap1_into_str(res) end function P.get_state_by_unit_path(unit_path) local is_ok, res = luastatus.plugin.get_property({ bus = 'system', dest = 'org.freedesktop.systemd1', object_path = unit_path, interface = 'org.freedesktop.systemd1.Unit', property_name = 'ActiveState', }) assert(is_ok, res) return unwrap1_into_str(res) end function P.get_state_by_unit_name(unit_name) local unit_path = P.get_unit_path_by_unit_name(unit_name) return P.get_state_by_unit_path(unit_path) end local function print_warning(unit_name, err) print(string.format( 'WARNING: luastatus: systemd-unit plugin: unable to get state of unit "%s": %s', unit_name, tostring(err) )) end local is_subscribed = false function P.widget(tbl) assert(type(tbl.unit_name) == 'string') return { plugin = 'dbus', opts = { signals = { { bus = 'system', interface = 'org.freedesktop.DBus.Properties', signal = 'PropertiesChanged', arg0 = 'org.freedesktop.systemd1.Unit', }, { bus = 'system', interface = 'org.freedesktop.systemd1.Manager', signal = 'UnitFilesChanged', arg0 = 'org.freedesktop.systemd1.Unit', }, }, timeout = tbl.forced_refresh_interval or 30, report_when_ready = true, }, cb = function(_) if not is_subscribed then P.subscribe() is_subscribed = true end if tbl.no_throw then local is_ok, res_or_err = pcall(P.get_state_by_unit_name, tbl.unit_name) if is_ok then return tbl.cb(res_or_err) else print_warning(tbl.unit_name, res_or_err) return tbl.cb(nil) end else return tbl.cb(P.get_state_by_unit_name(tbl.unit_name)) end end, } end return P ================================================ FILE: plugins/temperature-linux/CMakeLists.txt ================================================ install (FILES temperature-linux.lua DESTINATION ${LUA_PLUGINS_DIR}) luastatus_add_man_page (README.rst luastatus-plugin-temperature-linux 7) ================================================ FILE: plugins/temperature-linux/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-temperature-linux .. :X-man-page-only: ################################## .. :X-man-page-only: .. :X-man-page-only: ###################################################### .. :X-man-page-only: Linux-specific temperature sensor plugin for luastatus .. :X-man-page-only: ###################################################### .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This derived plugin periodically polls Linux ``sysfs`` for the current readings of various temperature sensors. Functions ========= The following functions are provided: * ``get_temps(data)`` Returns either an array or nil. If the result is nil, no data is currently available (the most likely reason for this is that the set of temperature sensors has changed; if so, the subsequent calls will return non-nil). Each array entry corresponds to a subsequent temperature sensor; it is a table with the following fields: * ``kind`` (string): kind of this temperature sensor; either ``"thermal"`` (``/sys/class/thermal/thermal_zone*`` sensors) or ``"hwmon"`` (``/sys/class/hwmon/*`` sensors). * ``name`` (string): name of this temperature sensor; it is either: - for ``"thermal"`` kind: ``"thermal_zone${i}"``, where ``${i}`` is the index of the thermal zone, or; - for ``"hwmon"`` kind: the contents of ``/sys/class/hwmon/hwmon${i}/name`` file, where ``${i}`` is the index of hwmon sensor. * ``path`` (string): full path to the sensor readings file. * ``value`` (number): current temperature, in Celsius degrees. The array is sorted first by kind (``"thermal"`` entries go first), then by sensor index (``${i}`` above). In order to use this function, you are expected to maintain a table ``data``, initially empty, and pass it to ``get_temps`` each time. The only things the caller is allowed to do with this table (either before the first call or after a call) are: - set ``please_reload`` field to ``true``; if ``please_reload`` field is true when this function is called, the function will forcefully reload all information and reset ``please_reload`` field to ``nil``. - set ``filter_func`` field to a function that takes two string arguments (kind and name), and returns a boolean indicating whether this sensor should be monitored. If not set, all sensors will be monitored. * ``widget(tbl[, data])`` Constructs a ``widget`` table required by luastatus. If ``data`` is specified, it must be an empty table; you can set ``please_reload`` field in this table to force a full reload. On each reported event, before a call to ``tbl.cb``, this field is reset to ``nil``. It is technically possible to set ``filter_func`` field of the ``data`` table to a filter function (see above), but it is recommended to set it via ``tbl.filter_func`` field (instead of ``data.filter_func``). ``tbl`` is a table with the following fields: **(required)** - ``cb``: a function The callback function that will be called with current readings of temperature sensors, or with ``nil``. The argument is the same as the return value of ``get_temps`` (see above for description of ``get_temps`` function for more information). **(optional)** - ``filter_func``: function This must be a function that takes two string arguments (kind and name; see the description of the ``get_temps`` function above for what they mean), and returns a boolean indicating whether this sensor should be monitored. If not specified, all sensors will be monitored. - ``timer_opts``: table Options for the underlying ``timer`` plugin. - ``event`` The ``event`` entry of the resulting table (see ``luastatus`` documentation for the description of ``widget.event`` field). ================================================ FILE: plugins/temperature-linux/temperature-linux.lua ================================================ -- ====================================================== -- ================= sensor_list stuff ================= -- ====================================================== local function sensor_list_new(kind) return {kind = kind, entries = nil} end local function sensor_list_is_null(SL) return SL.entries == nil end local function sensor_list_nullify(SL) SL.entries = nil end local function do_with_file(f, callback) local is_ok, err = pcall(callback) f:close() if not is_ok then error(err) end end local function sensor_list_realize(SL, filter_func, cmd) local f = assert(io.popen(cmd)) local entries = {} do_with_file(f, function() for line in f:lines() do local name, sort_by, path = line:match('^(%S+)\t(%S+)\t(%S+)$') if name then if (not filter_func) or filter_func(SL.kind, name) then entries[#entries + 1] = {name = name, sort_by = sort_by, path = path} end end end end) -- numeric sort local function extract_first_number(s) return tonumber(s:match('[0-9]+') or '-1') end table.sort(entries, function(a, b) return extract_first_number(a.sort_by) < extract_first_number(b.sort_by) end) SL.entries = entries end local function sensor_list_fetch_data(SL, into) assert(SL.entries) for _, entry in ipairs(SL.entries) do local f = io.open(entry.path, 'r') if not f then return false end local value = f:read('*number') f:close() if not value then return false end value = value / 1000 into[#into + 1] = { kind = SL.kind, name = entry.name, path = entry.path, value = value, } end return true end -- ====================================================== -- ================= data stuff ======================== -- ====================================================== local function escape_or_return_default(custom, default) if not custom then return default end if custom:find('\0') then error('custom path contains NUL character') end return string.format([['%s']], custom:gsub([[']], [['\'']])) end local function data_realize_sensor_lists(data) sensor_list_nullify(data.SL_thermal) sensor_list_nullify(data.SL_hwmon) local thermal_path_escaped = escape_or_return_default(data._thermal_path, '/sys/class/thermal') local hwmon_path_escaped = escape_or_return_default(data._hwmon_path, '/sys/class/hwmon') sensor_list_realize(data.SL_thermal, data.filter_func, [[ D=]] .. thermal_path_escaped .. [[; cd -- "$D" || exit $? for dir in thermal_zone*; do [ -e "$dir"/temp ] || continue printf "%s\t%s\t%s\n" "$dir" "$dir" "$D/$dir/temp" done ]]) sensor_list_realize(data.SL_hwmon, data.filter_func, [[ D=]] .. hwmon_path_escaped .. [[; cd -- "$D" || exit $? for dir in *; do [ -e "$dir"/name ] || continue IFS= read -r monitor_name < "$dir"/name || continue for f in "$dir"/temp*_input; do [ -e "$f" ] || continue printf "%s\t%s\t%s\n" "$monitor_name" "${f##*/}" "$D/$f" done done ]]) end local function data_is_ready(data) if sensor_list_is_null(data.SL_thermal) then return false end if sensor_list_is_null(data.SL_hwmon) then return false end return true end local function data_fetch_temps(data) local r = {} if not sensor_list_fetch_data(data.SL_thermal, r) then return nil end if not sensor_list_fetch_data(data.SL_hwmon, r) then return nil end return r end -- ====================================================== -- ================= plugin interface =================== -- ====================================================== local P = {} function P.get_temps(data) if not data.SL_thermal then data.SL_thermal = sensor_list_new('thermal') data.SL_hwmon = sensor_list_new('hwmon') end if data.please_reload or (not data_is_ready(data)) then data_realize_sensor_lists(data) data.please_reload = nil end local r = data_fetch_temps(data) if not r then data_realize_sensor_lists(data) return nil end return r end function P.widget(tbl, data) data = data or {} if tbl.filter_func then data.filter_func = tbl.filter_func end return { plugin = 'timer', opts = tbl.timer_opts, cb = function() return tbl.cb(P.get_temps(data)) end, event = tbl.event, } end return P ================================================ FILE: plugins/timer/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_plugin ( plugin-timer $ $ $ ${sources} ) target_compile_definitions (plugin-timer PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-timer LUA) target_include_directories (plugin-timer PUBLIC "${PROJECT_SOURCE_DIR}") luastatus_add_man_page (README.rst luastatus-plugin-timer 7) ================================================ FILE: plugins/timer/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-timer .. :X-man-page-only: ###################### .. :X-man-page-only: .. :X-man-page-only: ########################## .. :X-man-page-only: timer plugin for luastatus .. :X-man-page-only: ########################## .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This plugin is a simple timer, plus a wake-up FIFO can be specified. Options ======= The following options are supported: * ``period``: number A number of seconds to sleep before calling ``cb`` again. May be fractional. Defaults to 1. * ``fifo``: string Path to an existent FIFO. The plugin does not create FIFO itself. To force a wake-up, ``touch(1)`` the FIFO, that is, open it for writing and then close. * ``make_self_pipe``: boolean If true, the ``wake_up()`` (see the `Functions`_ section) function will be available. Defaults to false. ``cb`` argument =============== A string describing why the function is called: * if it is ``"hello"``, the function is called for the first time; * if it is ``"timeout"``, the function hasn't been called for ``period`` seconds; * if it is ``"fifo"``, the FIFO has been touched; * if it is ``"self_pipe"``, the ``luastatus.plugin.wake_up()`` function has been called. Functions ========= The following functions are provided: * ``luastatus.plugin.push_period(seconds)`` Changes the timer period for one iteration. * ``luastatus.plugin.wake_up()`` Forces a call to ``cb``. Only available if the ``make_self_pipe`` option was set to ``true``; otherwise, it throws an error. The following functions are provided as a part of "procalive" function set. These functions are available in plugins, including this one, that can be used to watch the state of some process(es): * ``is_ok, err_msg = luastatus.plugin.access(path)`` Checks if a given path exists, as if with ``access(path, F_OK)``. If it does exist, returns ``true, nil``. If it does not, returns ``false, nil``. If an error occurs, returns ``false, err_msg``. * ``file_type, err_msg = luastatus.plugin.stat(path)`` Tries to get the type of the file at the given path. On success returns either of: ``"regular"``, ``"dir"`` (directory), ``"chardev"`` (character device), ``"blockdev"`` (block device), ``"fifo"``, ``"symlink"``, ``"socket"``, ``"other"``. On failure returns ``nil, err_msg``. * ``arr, err_msg = luastatus.plugin.glob(pattern)`` Performs glob expansion of ``pattern``. A glob is a wildcard pattern like ``/tmp/*.txt`` that can be applied as a filter to a list of existing file names. Supported expansions are ``*``, ``?`` and ``[...]``. Please refer to ``glob(7)`` for more information on wildcard patterns. Note also that the globbing is performed with ``GLOB_MARK`` flag, so that in output, directories have trailing slash appended to their name. Returns ``arr, nil`` on success, where ``arr`` is an array of strings; these are existing file names that matched the given pattern. The order is arbitrary. On failure, returns ``nil, err_msg``. * ``is_alive = is_process_alive(pid)`` Checks if a process with PID ``pid`` is currently alive. ``pid`` must be either a number or a string. Returns a boolean that indicates whether the process is alive. ================================================ FILE: plugins/timer/timer.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "libmoonvisit/moonvisit.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_fifo_device.h" #include "libls/ls_evloop_lfuncs.h" #include "libls/ls_io_utils.h" #include "libls/ls_time_utils.h" #include "libprocalive/procalive_lfuncs.h" typedef struct { double period; char *fifo; LS_PushedTimeout pushed_tmo; int self_pipe[2]; } Priv; static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; free(p->fifo); ls_pushed_timeout_destroy(&p->pushed_tmo); ls_close(p->self_pipe[0]); ls_close(p->self_pipe[1]); free(p); } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .period = 1.0, .fifo = NULL, .self_pipe = {-1, -1}, }; ls_pushed_timeout_init(&p->pushed_tmo); char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Parse period if (moon_visit_num(&mv, -1, "period", &p->period, true) < 0) { goto mverror; } if (!ls_double_is_good_time_delta(p->period)) { LS_FATALF(pd, "period is invalid"); goto error; } // Parse fifo if (moon_visit_str(&mv, -1, "fifo", &p->fifo, NULL, true) < 0) { goto mverror; } // Parse make_self_pipe bool mkpipe = false; if (moon_visit_bool(&mv, -1, "make_self_pipe", &mkpipe, true) < 0) { goto mverror; } if (mkpipe) { if (ls_self_pipe_open(p->self_pipe) < 0) { LS_FATALF(pd, "ls_self_pipe_open: %s", ls_tls_strerror(errno)); goto error; } } return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); error: destroy(pd); return LUASTATUS_ERR; } static void register_funcs(LuastatusPluginData *pd, lua_State *L) { // L: table procalive_lfuncs_register_all(L); // L: table Priv *p = pd->priv; ls_pushed_timeout_push_luafunc(&p->pushed_tmo, L); // L: table func lua_setfield(L, -2, "push_period"); // L: table ls_self_pipe_push_luafunc(p->self_pipe, L); // L: table func lua_setfield(L, -2, "wake_up"); // L: table } static inline void make_call( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, const char *what) { lua_State *L = funcs.call_begin(pd->userdata); lua_pushstring(L, what); funcs.call_end(pd->userdata); } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { Priv *p = pd->priv; LS_FifoDevice dev = ls_fifo_device_new(); LS_TimeDelta default_tmo = ls_double_to_TD_or_die(p->period); make_call(pd, funcs, "hello"); for (;;) { if (ls_fifo_device_open(&dev, p->fifo) < 0) { LS_WARNF(pd, "ls_fifo_device_open: %s: %s", p->fifo, ls_tls_strerror(errno)); } LS_TimeDelta TD = ls_pushed_timeout_fetch(&p->pushed_tmo, default_tmo); struct pollfd pfds[2] = { {.fd = dev.fd, .events = POLLIN}, {.fd = p->self_pipe[0], .events = POLLIN}, }; int r = ls_poll(pfds, 2, TD); if (r < 0) { LS_FATALF(pd, "ls_poll: %s", ls_tls_strerror(errno)); goto error; } else if (r == 0) { make_call(pd, funcs, "timeout"); } else { if (pfds[0].revents) { make_call(pd, funcs, "fifo"); ls_fifo_device_reset(&dev); } else if (pfds[1].revents) { char dummy; ssize_t nread = read(p->self_pipe[0], &dummy, 1); (void) nread; make_call(pd, funcs, "self_pipe"); } } } error: ls_fifo_device_close(&dev); } LuastatusPluginIface luastatus_plugin_iface_v1 = { .init = init, .register_funcs = register_funcs, .run = run, .destroy = destroy, }; ================================================ FILE: plugins/udev/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_plugin (plugin-udev $ $ ${sources}) target_compile_definitions (plugin-udev PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-udev LUA) target_include_directories (plugin-udev PUBLIC "${PROJECT_SOURCE_DIR}") find_package (PkgConfig REQUIRED) pkg_check_modules (UDEV REQUIRED libudev) luastatus_target_build_with (plugin-udev UDEV) luastatus_add_man_page (README.rst luastatus-plugin-udev 7) ================================================ FILE: plugins/udev/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-udev .. :X-man-page-only: ##################### .. :X-man-page-only: .. :X-man-page-only: ######################### .. :X-man-page-only: udev plugin for luastatus .. :X-man-page-only: ######################### .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This plugin monitors udev events. Options ======= The following options are supported: * ``subsystem``: string Subsystem to monitor events from. Optional. * ``devtype``: string Only report events with this device type. Optional. * ``tag``: string Only report events with this tag. Optional. * ``kernel_events``: boolean Monitor kernel uevents, not udev ones. Defaults to false. * ``greet``: boolean Whether or not to call ``cb`` with ``what="hello"`` as soon as the widget starts. Defaults to false. * ``timeout``: number If specified and not negative, this plugin calls ``cb`` with ``what="timeout"`` if no event has occurred in ``timeout`` seconds. ``cb`` argument =============== A table with ``what`` entry: * if it is ``"hello"``, the function is being called for the first time (only if the ``greet`` option was set to ``true``); * if it is ``"timeout"``, the function has not been called for the number of seconds specified as the ``timeout`` option; * if it is ``"event"``, a udev event has occurred; in this case, the table has the following additional entries (all are optional strings): - ``syspath``; - ``sysname``; - ``sysnum``; - ``devpath``; - ``devnode``; - ``devtype``; - ``subsystem``; - ``driver``; - ``action``. Functions ========= The following functions are provided: * ``luastatus.plugin.push_timeout(seconds)`` Changes the timeout for one iteration. ================================================ FILE: plugins/udev/udev.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "libmoonvisit/moonvisit.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_io_utils.h" #include "libls/ls_evloop_lfuncs.h" #include "libls/ls_time_utils.h" typedef struct { char *subsystem; char *devtype; char *tag; bool kernel_ev; bool greet; double tmo; LS_PushedTimeout pushed_tmo; } Priv; static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; free(p->subsystem); free(p->devtype); free(p->tag); ls_pushed_timeout_destroy(&p->pushed_tmo); free(p); } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .subsystem = NULL, .devtype = NULL, .tag = NULL, .kernel_ev = false, .greet = false, .tmo = -1, }; ls_pushed_timeout_init(&p->pushed_tmo); char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Parse subsystem if (moon_visit_str(&mv, -1, "subsystem", &p->subsystem, NULL, true) < 0) goto mverror; // Parse devtype if (moon_visit_str(&mv, -1, "devtype", &p->devtype, NULL, true) < 0) goto mverror; // Parse tag if (moon_visit_str(&mv, -1, "tag", &p->tag, NULL, true) < 0) goto mverror; // Parse kernel_events if (moon_visit_bool(&mv, -1, "kernel_events", &p->kernel_ev, true) < 0) goto mverror; // Parse timeout if (moon_visit_num(&mv, -1, "timeout", &p->tmo, true) < 0) goto mverror; // Parse greet if (moon_visit_bool(&mv, -1, "greet", &p->greet, true) < 0) goto mverror; return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); //error: destroy(pd); return LUASTATUS_ERR; } static void register_funcs(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv; // L: table ls_pushed_timeout_push_luafunc(&p->pushed_tmo, L); // L: table func lua_setfield(L, -2, "push_timeout"); // L: table } static inline void report_status( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, const char *status) { lua_State *L = funcs.call_begin(pd->userdata); lua_createtable(L, 0, 1); // L: table lua_pushstring(L, status); // L: table string lua_setfield(L, -2, "what"); // L: table funcs.call_end(pd->userdata); } static void report_event( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, struct udev_device *dev) { typedef struct { const char *key; const char * (*func)(struct udev_device *); } Property; static const Property proplist[] = { {"syspath", udev_device_get_syspath}, {"sysname", udev_device_get_sysname}, {"sysnum", udev_device_get_sysnum}, {"devpath", udev_device_get_devpath}, {"devnode", udev_device_get_devnode}, {"devtype", udev_device_get_devtype}, {"subsystem", udev_device_get_subsystem}, {"driver", udev_device_get_driver}, {"action", udev_device_get_action}, {0}, }; lua_State *L = funcs.call_begin(pd->userdata); lua_createtable(L, 0, 4); // L: table lua_pushstring(L, "event"); // L: table string lua_setfield(L, -2, "what"); // L: table for (const Property *p = proplist; p->key; ++p) { const char *val = p->func(dev); if (val) { lua_pushstring(L, val); // L: table value lua_setfield(L, -2, p->key); // L: table } } funcs.call_end(pd->userdata); } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { Priv *p = pd->priv; struct udev *udev = udev_new(); if (!udev) { LS_FATALF(pd, "udev_new() failed"); return; } struct udev_monitor *mon = udev_monitor_new_from_netlink( udev, p->kernel_ev ? "kernel" : "udev"); if (!mon) { LS_FATALF(pd, "udev_monitor_new_from_netlink() failed"); goto error; } int rc; if (p->subsystem) { // udev_monitor_filter_add_match_subsystem_devtype() allows the second argument // to be NULL, but not the first one. rc = udev_monitor_filter_add_match_subsystem_devtype(mon, p->subsystem, p->devtype); if (rc < 0) { LS_FATALF(pd, "udev_monitor_filter_add_match_subsystem_devtype() failed (%d)", rc); goto error; } } if (p->tag) { rc = udev_monitor_filter_add_match_tag(mon, p->tag); if (rc < 0) { LS_FATALF(pd, "udev_monitor_filter_add_match_tag() failed (%d)", rc); goto error; } } rc = udev_monitor_enable_receiving(mon); if (rc < 0) { LS_FATALF(pd, "udev_monitor_enable_receiving() failed (%d)", rc); goto error; } int fd = udev_monitor_get_fd(mon); if (fd < 0) { LS_FATALF(pd, "udev_monitor_get_fd() failed (%d)", fd); goto error; } if (p->greet) { report_status(pd, funcs, "hello"); } LS_TimeDelta default_tmo = ls_double_to_TD(p->tmo, LS_TD_FOREVER); while (1) { LS_TimeDelta tmo = ls_pushed_timeout_fetch(&p->pushed_tmo, default_tmo); int r = ls_wait_input_on_fd(fd, tmo); if (r < 0) { LS_FATALF(pd, "ls_wait_input_on_fd: %s", ls_tls_strerror(errno)); goto error; } else if (r == 0) { report_status(pd, funcs, "timeout"); } else { struct udev_device *dev = udev_monitor_receive_device(mon); if (!dev) { // what the...? continue; } report_event(pd, funcs, dev); udev_device_unref(dev); } } error: if (mon) { udev_monitor_unref(mon); } udev_unref(udev); } LuastatusPluginIface luastatus_plugin_iface_v1 = { .init = init, .register_funcs = register_funcs, .run = run, .destroy = destroy, }; ================================================ FILE: plugins/unixsock/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_plugin (plugin-unixsock $ $ ${sources}) include (CheckSymbolExists) set (CMAKE_REQUIRED_DEFINITIONS "-D_GNU_SOURCE") check_symbol_exists (accept4 "sys/socket.h" HAVE_GNU_ACCEPT4) configure_file ("probes.in.h" "probes.generated.h") target_compile_definitions (plugin-unixsock PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-unixsock LUA) target_include_directories (plugin-unixsock PUBLIC "${PROJECT_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}") luastatus_add_man_page (README.rst luastatus-plugin-unixsock 7) ================================================ FILE: plugins/unixsock/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-unixsock .. :X-man-page-only: ######################### .. :X-man-page-only: .. :X-man-page-only: ####################################### .. :X-man-page-only: UNIX domain socket plugin for luastatus .. :X-man-page-only: ####################################### .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This plugin acts as a UNIX domain socket server (STREAM, not DGRAM or SEQPACKET), accepting incoming connections and making a call when a client produces a line. In order to have it make a call, connect to the UNIX socket and send a line to it. From a command line, this can be done with, e.g.:: echo some_data | socat stdio UNIX-CONNECT:/tmp/my-unix-socket (This is an experimental plugin; it is not enabled by default.) Options ======= The following options are supported: * ``path``: string Filesystem path to the UNIX domain socket. This option is required. * ``try_unlink``: boolean Whether or not to try to unlink (remove) the socket file before creating a server. Defaults to true. * ``timeout``: number If specified and not negative, this plugin calls ``cb`` with ``what="timeout"`` if no client has produced a line in ``timeout`` seconds. * ``greet``: boolean Whether or not to call ``cb`` with ``what="hello"`` as soon as the plugin starts. Defaults to false. * ``max_concur_conns``: number Specifies the maximum number of concurrent connections: once the number of connected clients reaches this value, others will be forced to wait in a queue. Please do not use this option unless you know what you are doing: too many concurrent connections will eat up file descriptors from the whole luastatus process. Defaults to 5, which is more than enough if the clients are not buggy. ``cb`` argument =============== A table with ``what`` entry: * if it is ``"hello"``, the function is being called for the first time (only if the ``greet`` option was set to ``true``); * if it is ``"timeout"``, the function has not been called for the number of seconds specified as the ``timeout`` option; * if it is ``"line"``, a client has produced a line; in this case, the table also has ``line`` entry with string value. Functions ========= The following functions are provided: * ``luastatus.plugin.push_timeout(seconds)`` Changes the timeout for one iteration. ================================================ FILE: plugins/unixsock/cloexec_accept.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #define _GNU_SOURCE #include "cloexec_accept.h" #include "probes.generated.h" #include "libls/ls_io_utils.h" #include #include int cloexec_accept(int sockfd) { #if HAVE_GNU_ACCEPT4 return accept4(sockfd, NULL, NULL, SOCK_CLOEXEC); #else int fd = accept(sockfd, NULL, NULL); if (fd >= 0) { ls_make_cloexec(fd); } return fd; #endif } ================================================ FILE: plugins/unixsock/cloexec_accept.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef cloexec_accept_h_ #define cloexec_accept_h_ int cloexec_accept(int sockfd); #endif ================================================ FILE: plugins/unixsock/probes.in.h ================================================ #ifndef luastatus_plugin_unixsock_probes_h_ #define luastatus_plugin_unixsock_probes_h_ #cmakedefine01 HAVE_GNU_ACCEPT4 #endif ================================================ FILE: plugins/unixsock/server.c ================================================ /* * Copyright (C) 2015-2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "server.h" #include #include #include #include #include #include #include #include "libls/ls_alloc_utils.h" #include "libls/ls_panic.h" #include "libls/ls_io_utils.h" #include "libls/ls_time_utils.h" #include "libls/ls_string.h" #include "cloexec_accept.h" enum { NCHUNK = 1024 }; typedef struct { int fd; LS_String buf; } Client; struct Server { int srv_fd; size_t max_clients; size_t nclients; struct pollfd *pfds; Client *clients; }; Server *server_new( int fd, size_t max_clients) { LS_ASSERT(fd >= 0); LS_ASSERT(max_clients <= SERVER_MAX_CLIENTS_LIMIT); ls_make_nonblock(fd); Server *S = LS_XNEW(Server, 1); *S = (Server) { .srv_fd = fd, .max_clients = max_clients, .nclients = 0, .pfds = LS_XNEW(struct pollfd, 1), .clients = NULL, }; return S; } static inline bool is_full(Server *S) { LS_ASSERT(S->nclients <= S->max_clients); return S->nclients == S->max_clients; } int server_poll( Server *S, LS_TimeDelta timeout, struct pollfd **out, size_t *out_n, bool *out_can_accept) { struct pollfd *pfds = S->pfds; pfds[0] = (struct pollfd) { .fd = is_full(S) ? -1 : S->srv_fd, .events = POLLIN, }; int rc = ls_poll(pfds, S->nclients + 1, timeout); if (rc < 0) { return -1; } *out = pfds + 1; *out_n = S->nclients; *out_can_accept = (pfds[0].revents != 0); return rc == 0 ? 0 : 1; } static inline bool is_dropped(Client *c) { return c->fd < 0; } static void drop_client(Client *c) { LS_ASSERT(!is_dropped(c)); close(c->fd); c->fd = -1; ls_string_free(c->buf); } static inline bool find_newline(Client *c, size_t num_last_read) { LS_ASSERT(num_last_read != 0); LS_ASSERT(num_last_read <= c->buf.size); const char *new_chunk = c->buf.data + c->buf.size - num_last_read; return memchr(new_chunk, '\n', num_last_read) != NULL; } int server_read_from_client( Server *S, size_t idx) { LS_ASSERT(idx < S->nclients); Client *c = &S->clients[idx]; struct pollfd *pfd = &S->pfds[idx + 1]; LS_ASSERT(!is_dropped(c)); if (!pfd->revents) { return 0; } ls_string_ensure_avail(&c->buf, NCHUNK); ssize_t r = read(c->fd, c->buf.data + c->buf.size, NCHUNK); if (r < 0) { if (LS_IS_EAGAIN(errno)) { return 0; } return -1; } else if (r == 0) { errno = 0; return -1; } c->buf.size += r; if (find_newline(c, r)) { return 1; } return 0; } const char *server_get_full_line( Server *S, size_t idx, size_t *out_len) { LS_ASSERT(idx < S->nclients); Client *c = &S->clients[idx]; const char *data = c->buf.data; size_t ndata = c->buf.size; LS_ASSERT(ndata != 0); const char *newline = memchr(data, '\n', ndata); LS_ASSERT(newline != NULL); *out_len = newline - data; return data; } void server_drop_client( Server *S, size_t idx) { LS_ASSERT(idx < S->nclients); Client *c = &S->clients[idx]; drop_client(c); } static void set_nclients(Server *S, size_t new_n) { if (S->nclients == new_n) { return; } S->clients = LS_M_XREALLOC(S->clients, new_n); S->pfds = LS_M_XREALLOC(S->pfds, new_n + 1); S->nclients = new_n; } static void update_pfds(Server *S) { for (size_t i = 0; i < S->nclients; ++i) { Client *c = &S->clients[i]; LS_ASSERT(!is_dropped(c)); S->pfds[i + 1] = (struct pollfd) { .fd = c->fd, .events = POLLIN, }; } } static void add_client(Server *S, int fd) { set_nclients(S, S->nclients + 1); S->clients[S->nclients - 1] = (Client) { .fd = fd, .buf = ls_string_new_reserve(NCHUNK), }; update_pfds(S); } int server_accept_new_client(Server *S) { LS_ASSERT(S->pfds[0].revents != 0); int client_fd = cloexec_accept(S->srv_fd); if (client_fd < 0) { if (LS_IS_EAGAIN(errno) || errno == ECONNABORTED) { return 0; } return -1; } ls_make_nonblock(client_fd); add_client(S, client_fd); return 1; } void server_compact(Server *S) { size_t j = 0; for (size_t i = 0; i < S->nclients; ++i) { if (!is_dropped(&S->clients[i])) { S->clients[j] = S->clients[i]; ++j; } } set_nclients(S, j); update_pfds(S); } void server_destroy(Server *S) { close(S->srv_fd); for (size_t i = 0; i < S->nclients; ++i) { if (!is_dropped(&S->clients[i])) { drop_client(&S->clients[i]); } } free(S->clients); free(S->pfds); free(S); } ================================================ FILE: plugins/unixsock/server.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef server_h_ #define server_h_ #include #include #include #include "libls/ls_time_utils.h" enum { SERVER_MAX_CLIENTS_LIMIT = 1024 * 1024 * 1024, }; struct Server; typedef struct Server Server; Server *server_new( int fd, size_t max_clients); int server_poll( Server *S, LS_TimeDelta timeout, struct pollfd **out, size_t *out_n, bool *out_can_accept); int server_read_from_client( Server *S, size_t idx); const char *server_get_full_line( Server *S, size_t idx, size_t *out_len); void server_drop_client( Server *S, size_t idx); // After this call, pollfd's returned from /server_poll/ are invalidated; it is // invalid to use them anymore. int server_accept_new_client(Server *S); void server_compact(Server *S); void server_destroy(Server *S); #endif ================================================ FILE: plugins/unixsock/unixsock.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "libmoonvisit/moonvisit.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_evloop_lfuncs.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_time_utils.h" #include "libls/ls_io_utils.h" #include "libls/ls_osdep.h" #include "server.h" typedef struct { char *path; bool try_unlink; bool greet; uint64_t max_clients; double tmo; LS_TimeDelta tmo_as_TD; LS_PushedTimeout pushed_tmo; } Priv; static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; free(p->path); ls_pushed_timeout_destroy(&p->pushed_tmo); free(p); } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .path = NULL, .try_unlink = true, .greet = false, .max_clients = 5, .tmo = -1, }; ls_pushed_timeout_init(&p->pushed_tmo); char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Parse path if (moon_visit_str(&mv, -1, "path", &p->path, NULL, false) < 0) goto mverror; if (p->path[0] == '\0') { LS_FATALF(pd, "path is empty"); goto error; } // Parse try_unlink if (moon_visit_bool(&mv, -1, "try_unlink", &p->try_unlink, true) < 0) goto mverror; // Parse greet if (moon_visit_bool(&mv, -1, "greet", &p->greet, true) < 0) goto mverror; // Parse max_concur_conns if (moon_visit_uint(&mv, -1, "max_concur_conns", &p->max_clients, true) < 0) { goto mverror; } if (!p->max_clients) { LS_FATALF(pd, "max_concur_conns is zero"); goto error; } if (p->max_clients > SERVER_MAX_CLIENTS_LIMIT) { LS_FATALF(pd, "max_concur_conns is too big (limit is %d)", (int) SERVER_MAX_CLIENTS_LIMIT); goto error; } // Parse timeout if (moon_visit_num(&mv, -1, "timeout", &p->tmo, true) < 0) { goto mverror; } p->tmo_as_TD = ls_double_to_TD(p->tmo, LS_TD_FOREVER); return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); error: destroy(pd); return LUASTATUS_ERR; } static void register_funcs(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv; // L: table ls_pushed_timeout_push_luafunc(&p->pushed_tmo, L); // L: table func lua_setfield(L, -2, "push_timeout"); // L: table } static inline LS_TimeStamp new_deadline(Priv *p) { LS_TimeDelta tmo = ls_pushed_timeout_fetch(&p->pushed_tmo, p->tmo_as_TD); if (ls_TD_is_forever(tmo)) { return LS_TS_BAD; } return ls_TS_plus_TD(ls_now(), tmo); } static inline LS_TimeDelta get_time_until_TS(LS_TimeStamp TS) { if (ls_TS_is_bad(TS)) { return LS_TD_FOREVER; } return ls_TS_minus_TS_nonneg(TS, ls_now()); } static void report_status( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, const char *what) { lua_State *L = funcs.call_begin(pd->userdata); lua_createtable(L, 0, 1); // L: table lua_pushstring(L, what); // L: table what lua_setfield(L, -2, "what"); // L: table funcs.call_end(pd->userdata); } static void report_line( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, const char *line, size_t nline) { lua_State *L = funcs.call_begin(pd->userdata); lua_createtable(L, 0, 2); // L: table lua_pushstring(L, "line"); // L: table str lua_setfield(L, -2, "what"); // L: table lua_pushlstring(L, line, nline); // L: table str lua_setfield(L, -2, "line"); // L: table funcs.call_end(pd->userdata); } static int mk_server(LuastatusPluginData *pd) { Priv *p = pd->priv; int fd = ls_cloexec_socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) { LS_FATALF(pd, "ls_cloexec_socket: %s", ls_tls_strerror(errno)); goto error; } struct sockaddr_un saun = {.sun_family = AF_UNIX}; if (p->try_unlink) { if (unlink(p->path) < 0 && errno != ENOENT) { LS_WARNF(pd, "unlink: %s: %s", p->path, ls_tls_strerror(errno)); } } size_t npath = strlen(p->path); if (npath + 1 > sizeof(saun.sun_path)) { LS_FATALF(pd, "socket path is too long"); goto error; } memcpy(saun.sun_path, p->path, npath + 1); if (bind(fd, (struct sockaddr *) &saun, sizeof(saun)) < 0) { LS_FATALF(pd, "bind: %s", ls_tls_strerror(errno)); goto error; } if (listen(fd, SOMAXCONN) < 0) { LS_FATALF(pd, "listen: %s", ls_tls_strerror(errno)); goto error; } return fd; error: ls_close(fd); return -1; } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { Priv *p = pd->priv; Server *S = NULL; int srv_fd = mk_server(pd); if (srv_fd < 0) { goto error; } S = server_new(srv_fd, p->max_clients); LS_TimeStamp deadline = new_deadline(p); if (p->greet) { report_status(pd, funcs, "hello"); deadline = new_deadline(p); } for (;;) { LS_TimeDelta tmo = get_time_until_TS(deadline); struct pollfd *pfds; size_t pfds_num; bool can_accept; int poll_rc = server_poll(S, tmo, &pfds, &pfds_num, &can_accept); if (poll_rc < 0) { LS_FATALF(pd, "poll: %s", ls_tls_strerror(errno)); goto error; } if (poll_rc == 0) { report_status(pd, funcs, "timeout"); deadline = new_deadline(p); } for (size_t i = 0; i < pfds_num; ++i) { if (!pfds[i].revents) { continue; } int read_rc = server_read_from_client(S, i); if (read_rc < 0) { if (errno == 0) { LS_DEBUGF(pd, "clent disconnected before sending a full line"); } else { LS_WARNF(pd, "read: %s", ls_tls_strerror(errno)); } server_drop_client(S, i); continue; } else if (read_rc > 0) { size_t nline; const char *line = server_get_full_line(S, i, &nline); report_line(pd, funcs, line, nline); server_drop_client(S, i); deadline = new_deadline(p); } } if (can_accept) { int accept_rc = server_accept_new_client(S); if (accept_rc < 0) { LS_FATALF(pd, "accept: %s", ls_tls_strerror(errno)); goto error; } else if (accept_rc > 0) { LS_DEBUGF(pd, "accepted a new client"); } } server_compact(S); } error: ls_close(srv_fd); if (S) { server_destroy(S); } } LuastatusPluginIface luastatus_plugin_iface_v1 = { .init = init, .register_funcs = register_funcs, .run = run, .destroy = destroy, }; ================================================ FILE: plugins/web/CMakeLists.txt ================================================ file (GLOB sources "*.c" "mod_json/*.c" "mod_urlencode/*.c") luastatus_add_plugin ( plugin-web $ $ $ ${sources} ) target_compile_definitions (plugin-web PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-web LUA) target_include_directories (plugin-web PUBLIC "${PROJECT_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}") find_library (MATH_LIBRARY m) if (MATH_LIBRARY) target_link_libraries (plugin-web PUBLIC ${MATH_LIBRARY}) endif () find_package (PkgConfig REQUIRED) pkg_check_modules (CURL_STUFF REQUIRED libcurl) luastatus_target_build_with (plugin-web CURL_STUFF) pkg_check_modules (CJSON_STUFF libcjson) if (CJSON_STUFF_FOUND) luastatus_target_build_with (plugin-web CJSON_STUFF) else () message (WARNING "libcjson was not found by pkg-config; will try to simply link with -lcjson") target_link_libraries (plugin-web PUBLIC -lcjson) endif () set (CJSON_FOUND_BY_PKG_CONFIG "${CJSON_STUFF_FOUND}") configure_file ("json_config.in.h" "json_config.generated.h") luastatus_add_man_page (README.rst luastatus-plugin-web 7) ================================================ FILE: plugins/web/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-web .. :X-man-page-only: #################### .. :X-man-page-only: .. :X-man-page-only: ######################## .. :X-man-page-only: Web plugin for luastatus .. :X-man-page-only: ######################## .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This plugin performs HTTP requests using libcurl. It is designed around a coroutine-based "planner" mechanism that allows sequencing requests, sleeps, and callback updates. It also provides utility functions for JSON encoding/decoding and URL encoding/decoding. Options ======= The following options are supported at plugin initialization: * ``planner``: function (**required**) A Lua function that acts as a coroutine planner. It should yield tables describing the next action (see `Planner Actions`_). The most common planner would be make-request-and-sleep:: function() while true do coroutine.yield({what = 'request', params = {...}}) coroutine.yield({what = 'sleep', period = 5.0}) end end * ``with_headers_global``: boolean Whether to include HTTP response headers in all response callbacks. Defaults to false. * ``debug_global``: boolean Whether to enable verbose libcurl debug logging globally. Defaults to false. * ``make_self_pipe``: boolean If true, the ``wake_up()`` (see the `Functions`_ section) function will be available. Defaults to false. Planner Actions =============== The ``planner`` function should yield a table with an ``action`` key. The following actions are supported: * ``{action = "request", params = {...}}`` Performs an HTTP request. The ``params`` table contains request-specific options (see `Request Options`_). Upon completion, the plugin calls ``cb`` with the response data. * ``{action = "sleep", period = }`` Sleeps for ``period`` seconds. Upon completion, the coroutine is resumed with a boolean indicating whether it was woken up via ``wake_up()`` (true) or timed out (false). Note that this action doesn't call ``cb``. * ``{action = "call_cb", what = }`` Immediately calls ``cb`` with a table containing only the ``what`` field, set to the specified string. Request Options =============== The ``params`` table in a ``request`` action supports the following options: * ``url``: string (**required**) The URL to request. * ``headers``: table An array of strings representing HTTP headers to send. * ``timeout``: number Maximum time, in seconds, for the request. Zero means no timeout (this is the default). * ``max_file_size``: integer Maximum file size to download. Zero means no limit (this is the default). Must be less than 2 Gb. * ``auto_referer``: boolean Automatically set the Referer header when following Location headers. * ``custom_request``: string Custom HTTP request method (e.g. DELETE). * ``follow_location``: boolean Follow HTTP redirects. * ``interface``: string Local interface to use for the connection. * ``max_redirs``: integer Maximum number of redirects to follow. The value of -1 means unlimited number of redirects. The default depends on libcurl version; either unlimited or 30. * ``tcp_keepalive``: boolean Enable TCP keepalive. * ``proxy``, ``proxy_username``, ``proxy_password``: strings Proxy configuration. * ``post_fields``: string Data to send in a POST (or POST-like) request. * ``with_headers``: boolean Whether to include HTTP response headers. If either this option or the global ``with_headers_global`` option is enabled, headers are included. * ``debug``: boolean Whether to enable verbose logging for this specific request. The logic is the same as with ``with_headers`` (see above). cb argument =========== The callback function ``cb`` is called with a table argument. The structure depends on the action completed. Note that the ``sleep`` action doesn't call ``cb``. * For ``request`` actions (HTTP requests has been done, regardless of status): A table ``{what = "response", status = , body = , headers = }``. * For ``request`` actions (HTTP requests has **not** been done): A table ``{what = "response", status = 0, body = "", headers = {}, error = }``. * For ``call_cb`` actions: A table ``{what = }``, where ``string`` is the value provided in the action. Functions ========= The following functions are provided in the plugin table: * ``luastatus.plugin.wake_up()`` Writes to the self-pipe. If the plugin is currently executing a ``sleep`` action, this interrupts the sleep and resumes the coroutine (``coroutine.yield`` then returns true). It doesn't directly call ``cb``. Waking up in the middle of a request is not supported; if a request is in flight, the wake-up will happen only after the request is done. Only available if the ``make_self_pipe`` option was set to ``true``; otherwise, it throws an error. * ``luastatus.plugin.time_now()`` Returns the current timestamp, as a floating-point number in seconds. If the system supports it, uses monotonic clock; otherwise, uses wall-clock. It may be helpful in case of a sleep interrupted by a wake-up. * ``luastatus.plugin.get_supported_opts()`` Returns a table listing all supported request options (keys are option names, values are ``true``). * ``luastatus.plugin.json_decode(input, mark_arrays_vs_dicts, mark_nulls)`` Decodes a JSON string into a Lua table. See the `JSON Decoding`_ section for details on the parameters. * ``luastatus.plugin.json_encode_str(str)`` Encodes a string into a JSON-safe string. Please note that it does **not** enclose the output in double quotes. * ``luastatus.plugin.json_encode_num(num)`` Converts a number into a JSON-conforming string representation. * ``luastatus.plugin.urlencode(str[, plus_notation])`` URL-encodes a string. ``plus_notation`` is an optional boolean parameter; if set to true, spaces will be replaced with ``+``, not ``%20``. ``plus_notation`` defaults to false. * ``luastatus.plugin.urldecode(str)`` URL-decodes a string. Returns nil on failure. JSON Decoding ============= The ``json_decode`` function provides options to handle nuances between JSON and Lua data structures: * ``mark_arrays_vs_dicts``: boolean Lua tables are used for both JSON arrays and objects (dicts), making them indistinguishable by default. If this parameter is ``true``, the decoded tables will have a metatable set: + JSON arrays will have a metatable with ``is_array = true``. + JSON objects will have a metatable with ``is_dict = true``. This allows you to distinguish them using ``getmetatable(
).is_array`` or ``is_dict``. * ``mark_nulls``: boolean In JSON, ``null`` is a valid value distinct from a missing key. In Lua, assigning ``nil`` to a table key removes the key. If this parameter is ``true``, JSON ``null`` values are decoded as a special light userdata object instead of Lua ``nil``. This allows you to distinguish between a key that is missing and a key that is explicitly set to ``null``. You can check for this special value using ``type() == "userdata"``. Return value ------------ On success, returns the decoded Lua table. On failure, returns ``nil, err_msg``. Limitations ----------- The maximum nesting depth that the current implementation supports is 100. If this limit is exceeded, the function fails and returns ``nil, "depth limit exceeded"``. ================================================ FILE: plugins/web/compat_lua_resume.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "compat_lua_resume.h" #include #if LUA_VERSION_NUM >= 504 static int do_resume(lua_State *L, lua_State *from, int nargs) { int unused; return lua_resume(L, from, nargs, &unused); } #elif LUA_VERSION_NUM >= 502 static int do_resume(lua_State *L, lua_State *from, int nargs) { return lua_resume(L, from, nargs); } #else static int do_resume(lua_State *L, lua_State *from, int nargs) { (void) from; return lua_resume(L, nargs); } #endif int compat_lua_resume(lua_State *L, lua_State *from, int nargs, int *nresults) { int old_top = lua_gettop(L); int rc = do_resume(L, from, nargs); if (rc == LUA_YIELD || rc == 0) { *nresults = lua_gettop(L) - (old_top - (nargs + 1)); } return rc; } ================================================ FILE: plugins/web/compat_lua_resume.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include int compat_lua_resume(lua_State *L, lua_State *from, int nargs, int *nresults); ================================================ FILE: plugins/web/fuzz_esc_json/.gitignore ================================================ harness findings ================================================ FILE: plugins/web/fuzz_esc_json/build.sh ================================================ #!/bin/sh if [ -z "$CC" ]; then echo >&2 "You must set the 'CC' environment variable." echo >&2 "Hint: you probably want to set 'CC' to 'some-directory/afl-gcc'." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. $CC -Wall -Wextra -O3 -fsanitize=undefined -std=c99 -D_POSIX_C_SOURCE=200809L \ -I"$luastatus_root" \ ./harness.c \ ../mod_json/json_encode_str.c \ "$luastatus_root"/libls/ls_string.c \ "$luastatus_root"/libls/ls_alloc_utils.c \ "$luastatus_root"/libls/ls_panic.c \ "$luastatus_root"/libls/ls_cstring_utils.c \ "$luastatus_root"/libsafe/*.c \ -o harness ================================================ FILE: plugins/web/fuzz_esc_json/clear.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" rm -rf ./findings ================================================ FILE: plugins/web/fuzz_esc_json/fuzz.sh ================================================ #!/bin/sh set -e if [ -z "$XXX_AFL_DIR" ]; then echo >&2 "You must set the 'XXX_AFL_DIR' environment variable." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" mkdir -p ./findings export UBSAN_OPTIONS=halt_on_error=1 export AFL_EXIT_WHEN_DONE=1 # We also set AFL_NO_ARITH=1 because it's a text-based format. # This potentially speeds up fuzzing. export AFL_NO_ARITH=1 "$XXX_AFL_DIR"/afl-fuzz -i testcases -o findings -t 5 ./harness @@ ================================================ FILE: plugins/web/fuzz_esc_json/gen_testcases.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases \ --a=1:'\"/' \ --a-range=h:1:0-31 \ --b=1:abc \ --b-range=h:1:127-255 \ --a-is-important \ --length=5-20 \ --num-files=10 \ --random-seed=123 ================================================ FILE: plugins/web/fuzz_esc_json/harness.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include "libls/ls_string.h" #include "libsafe/safev.h" #include "fuzz_utils/fuzz_utils.h" #include "../mod_json/json_encode_str.h" static void append_to_ls_string(void *ud, SAFEV v) { LS_String *dst = ud; ls_string_append_b(dst, SAFEV_ptr_UNSAFE(v), SAFEV_len(v)); } int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "USAGE: harness INPUT_FILE\n"); return 2; } int fd_in = open(argv[1], O_RDONLY | O_CLOEXEC); if (fd_in < 0) { perror(argv[1]); abort(); } FuzzInput input = fuzz_input_new_prealloc(1024); if (fuzz_input_read(fd_in, &input) < 0) { perror("read"); abort(); } LS_String res = ls_string_new_from_s("escape result = "); json_encode_str(append_to_ls_string, &res, SAFEV_new_UNSAFE(input.data, input.size)); fuzz_utils_used(res.data, res.size); fuzz_input_free(input); ls_string_free(res); close(fd_in); return 0; } ================================================ FILE: plugins/web/fuzz_esc_json/testcases/testcase_000 ================================================ \"//\ ================================================ FILE: plugins/web/fuzz_esc_json/testcases/testcase_001 ================================================ ""\ ================================================ FILE: plugins/web/fuzz_esc_json/testcases/testcase_003 ================================================ /\ ================================================ FILE: plugins/web/fuzz_esc_json/testcases/testcase_004 ================================================ "/cb\"ac"b\ ================================================ FILE: plugins/web/fuzz_esc_json/testcases/testcase_006 ================================================ ba a߫" ================================================ FILE: plugins/web/fuzz_esc_json/testcases/testcase_007 ================================================ c"bcc ================================================ FILE: plugins/web/fuzz_esc_json/testcases/testcase_008 ================================================ cbcȂbab\ ================================================ FILE: plugins/web/fuzz_esc_json/testcases/testcase_009 ================================================ cbƐbccb ================================================ FILE: plugins/web/fuzz_urldecode/.gitignore ================================================ /harness /findings ================================================ FILE: plugins/web/fuzz_urldecode/build.sh ================================================ #!/bin/sh if [ -z "$CC" ]; then echo >&2 "You must set the 'CC' environment variable." echo >&2 "Hint: you probably want to set 'CC' to 'some-directory/afl-gcc'." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. $CC -Wall -Wextra -O3 -fsanitize=undefined -std=c99 -D_POSIX_C_SOURCE=200809L \ -I"$luastatus_root" \ ./harness.c \ ../mod_urlencode/urldecode.c \ "$luastatus_root"/libsafe/*.c \ "$luastatus_root"/libls/ls_panic.c \ "$luastatus_root"/libls/ls_cstring_utils.c \ -o harness ================================================ FILE: plugins/web/fuzz_urldecode/fuzz.sh ================================================ #!/bin/sh set -e if [ -z "$XXX_AFL_DIR" ]; then echo >&2 "You must set the 'XXX_AFL_DIR' environment variable." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" mkdir -p ./findings export UBSAN_OPTIONS=halt_on_error=1 export AFL_EXIT_WHEN_DONE=1 "$XXX_AFL_DIR"/afl-fuzz $extra_opts -i testcases -o findings -t 5 ./harness @@ ================================================ FILE: plugins/web/fuzz_urldecode/gen_testcases.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases \ --a=1:x \ --b=1:x \ --mut-substrings="|%2f|%F2|%64|%bA|%2X|%XY" \ --length=10 \ --num-files=10 \ --random-seed=123 \ --extra-testcase='extra1:%' \ --extra-testcase='extra2:%%' \ --extra-testcase='extra3:%%%' ================================================ FILE: plugins/web/fuzz_urldecode/harness.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include "libls/ls_panic.h" #include "libsafe/safev.h" #include "libsafe/mut_safev.h" #include "fuzz_utils/fuzz_utils.h" #include "../mod_urlencode/urldecode.h" int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "USAGE: harness INPUT_FILE\n"); return 2; } int fd_in = open(argv[1], O_RDONLY | O_CLOEXEC); if (fd_in < 0) { perror(argv[1]); abort(); } FuzzInput input = fuzz_input_new_prealloc(1024); if (fuzz_input_read(fd_in, &input) < 0) { perror("read"); abort(); } SAFEV src = SAFEV_new_UNSAFE(input.data, input.size); size_t n = urldecode_size(src); char *ptr = calloc(n, 1); if (!ptr && n) { perror("calloc"); abort(); } MUT_SAFEV dst = MUT_SAFEV_new_UNSAFE(ptr, n); char is_ok = urldecode(src, dst); fuzz_utils_used(&is_ok, 1); fuzz_utils_used(ptr, n); free(ptr); fuzz_input_free(input); close(fd_in); return 0; } ================================================ FILE: plugins/web/fuzz_urldecode/testcases/testcase_000 ================================================ xxxx%2fxxxxxx ================================================ FILE: plugins/web/fuzz_urldecode/testcases/testcase_001 ================================================ xxxxxxx%F2xxx ================================================ FILE: plugins/web/fuzz_urldecode/testcases/testcase_002 ================================================ xxxxxxxxxx%64 ================================================ FILE: plugins/web/fuzz_urldecode/testcases/testcase_003 ================================================ xxxxx%bAxxxxx ================================================ FILE: plugins/web/fuzz_urldecode/testcases/testcase_004 ================================================ xxxxxxxx%2Xxx ================================================ FILE: plugins/web/fuzz_urldecode/testcases/testcase_005 ================================================ xxxxxxxxx%XYx ================================================ FILE: plugins/web/fuzz_urldecode/testcases/testcase_006 ================================================ xxxxxxxx%2fxx ================================================ FILE: plugins/web/fuzz_urldecode/testcases/testcase_007 ================================================ xx%F2xxxxxxxx ================================================ FILE: plugins/web/fuzz_urldecode/testcases/testcase_008 ================================================ xxxxxxxxx%64x ================================================ FILE: plugins/web/fuzz_urldecode/testcases/testcase_009 ================================================ xxx%bAxxxxxxx ================================================ FILE: plugins/web/fuzz_urldecode/testcases/testcase_extra1 ================================================ % ================================================ FILE: plugins/web/fuzz_urldecode/testcases/testcase_extra2 ================================================ %% ================================================ FILE: plugins/web/fuzz_urldecode/testcases/testcase_extra3 ================================================ %%% ================================================ FILE: plugins/web/fuzz_urlencode/.gitignore ================================================ /harness /findings ================================================ FILE: plugins/web/fuzz_urlencode/build.sh ================================================ #!/bin/sh if [ -z "$CC" ]; then echo >&2 "You must set the 'CC' environment variable." echo >&2 "Hint: you probably want to set 'CC' to 'some-directory/afl-gcc'." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. $CC -Wall -Wextra -O3 -fsanitize=undefined -std=c99 -D_POSIX_C_SOURCE=200809L \ -I"$luastatus_root" \ ./harness.c \ ../mod_urlencode/urlencode.c \ "$luastatus_root"/libsafe/*.c \ "$luastatus_root"/libls/ls_panic.c \ "$luastatus_root"/libls/ls_cstring_utils.c \ -o harness ================================================ FILE: plugins/web/fuzz_urlencode/fuzz.sh ================================================ #!/bin/sh set -e if [ -z "$XXX_AFL_DIR" ]; then echo >&2 "You must set the 'XXX_AFL_DIR' environment variable." exit 1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" case "$1" in 0|1) plus_encoding=$1 ;; *) printf '%s\n' "USAGE: $0 {0 | 1}" >&2 exit 2 ;; esac mkdir -p ./findings_"$plus_encoding" export UBSAN_OPTIONS=halt_on_error=1 export AFL_EXIT_WHEN_DONE=1 "$XXX_AFL_DIR"/afl-fuzz $extra_opts -i testcases -o findings_"$plus_encoding" -t 5 ./harness @@ "$plus_encoding" ================================================ FILE: plugins/web/fuzz_urlencode/fuzz_all.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" for plus_encoding in 0 1; do ./fuzz.sh "$plus_encoding" done ================================================ FILE: plugins/web/fuzz_urlencode/gen_testcases.sh ================================================ #!/bin/sh set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" luastatus_root=../../.. "$luastatus_root"/fuzz_utils/gen_testcases/gen_testcases.py \ ./testcases \ --a=1:x% \ --b=1:' ' \ --length=10 \ --num-files=10 \ --random-seed=123 ================================================ FILE: plugins/web/fuzz_urlencode/harness.c ================================================ /* * Copyright (C) 2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include "libls/ls_panic.h" #include "libsafe/safev.h" #include "libsafe/mut_safev.h" #include "fuzz_utils/fuzz_utils.h" #include "../mod_urlencode/urlencode.h" int main(int argc, char **argv) { if (argc != 3) { fprintf(stderr, "USAGE: harness INPUT_FILE PLUS_NOTATION_FLAG\n"); return 2; } int fd_in = open(argv[1], O_RDONLY | O_CLOEXEC); if (fd_in < 0) { perror(argv[1]); abort(); } bool plus_notation; if (strcmp(argv[2], "0") == 0) { plus_notation = false; } else if (strcmp(argv[2], "1") == 0) { plus_notation = true; } else { fprintf(stderr, "Invalid PLUS_NOTATION_FLAG (expected either 0 or 1).\n"); return 2; } FuzzInput input = fuzz_input_new_prealloc(1024); if (fuzz_input_read(fd_in, &input) < 0) { perror("read"); abort(); } SAFEV src = SAFEV_new_UNSAFE(input.data, input.size); size_t n = urlencode_size(src, plus_notation); char *ptr = malloc(n); if (!ptr && n) { perror("malloc"); abort(); } MUT_SAFEV dst = MUT_SAFEV_new_UNSAFE(ptr, n); urlencode(src, dst, plus_notation); fuzz_utils_used(ptr, n); free(ptr); fuzz_input_free(input); close(fd_in); return 0; } ================================================ FILE: plugins/web/fuzz_urlencode/testcases/testcase_000 ================================================ x%x%%%xxxx ================================================ FILE: plugins/web/fuzz_urlencode/testcases/testcase_001 ================================================ %% %xxx%%x ================================================ FILE: plugins/web/fuzz_urlencode/testcases/testcase_002 ================================================ % xx%%xx% ================================================ FILE: plugins/web/fuzz_urlencode/testcases/testcase_003 ================================================ x%%% % x x ================================================ FILE: plugins/web/fuzz_urlencode/testcases/testcase_004 ================================================ % % x x%x ================================================ FILE: plugins/web/fuzz_urlencode/testcases/testcase_005 ================================================ x xx x ================================================ FILE: plugins/web/fuzz_urlencode/testcases/testcase_006 ================================================ %x % ================================================ FILE: plugins/web/fuzz_urlencode/testcases/testcase_007 ================================================ % % ================================================ FILE: plugins/web/fuzz_urlencode/testcases/testcase_008 ================================================ x ================================================ FILE: plugins/web/fuzz_urlencode/testcases/testcase_009 ================================================ ================================================ FILE: plugins/web/get_min_curl_ver.sh ================================================ #!/usr/bin/env bash set -e set -o pipefail shopt -s failglob cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" sources=( ./*.[ch] ) gen_symbol_list() { grep -woE 'curl_[A-Za-z0-9_]+' "${sources[@]}" --binary-files=without-match --no-filename \ || return $? grep -woE 'CURLOPT_[A-Za-z0-9_]+' "${sources[@]}" --binary-files=without-match --no-filename \ || return $? } gen_symbol_list | sort -u | while IFS= read -r sym; do echo >&2 "Querying '${sym}'..." { lynx -dump "https://curl.se/libcurl/c/${sym}.html" . */ #include "make_request.h" #include #include "libls/ls_string.h" #include "libls/ls_strarr.h" #include "libls/ls_panic.h" #include "include/plugin_data_v1.h" #include "include/sayf_macros.h" #include "set_error.h" #define CANNOT_FAIL(expr) \ do { \ if ((expr) != CURLE_OK) { \ LS_PANIC("CANNOT_FAIL() failed"); \ } \ } while (0) static size_t callback_resp(char *buf, size_t char_sz, size_t nbuf, void *ud) { (void) char_sz; LS_String *body = ud; ls_string_append_b(body, buf, nbuf); return nbuf; } static size_t callback_header(char *buf, size_t char_sz, size_t nbuf, void *ud) { (void) char_sz; LS_StringArray *headers = ud; ls_strarr_append(headers, buf, nbuf); return nbuf; } static const char *get_debug_prefix(curl_infotype type) { switch (type) { case CURLINFO_TEXT: return "Text"; case CURLINFO_HEADER_OUT: return "Send header"; case CURLINFO_DATA_OUT: return "Send data"; case CURLINFO_HEADER_IN: return "Recv header"; case CURLINFO_DATA_IN: return "Recv data"; default: return NULL; } } static int callback_debug(CURL *C, curl_infotype type, char *buf, size_t nbuf, void *ud) { (void) C; LuastatusPluginData *pd = ud; const char *prefix = get_debug_prefix(type); if (prefix) { enum { MAX_PREVIEW = 1024 * 8 }; int truncated_len = nbuf < MAX_PREVIEW ? nbuf : MAX_PREVIEW; LS_INFOF(pd, " %s: %.*s", prefix, truncated_len, buf); } return 0; } bool make_request( LuastatusPluginData *pd, int req_flags, CURL *C, Response *out, char **out_errmsg) { *out = (Response) { .status = 0, .headers = ls_strarr_new(), .body = ls_string_new_reserve(1024), }; CANNOT_FAIL(curl_easy_setopt(C, CURLOPT_WRITEFUNCTION, callback_resp)); CANNOT_FAIL(curl_easy_setopt(C, CURLOPT_WRITEDATA, (void *) &out->body)); if (req_flags & REQ_FLAG_NEEDS_HEADERS) { CANNOT_FAIL(curl_easy_setopt(C, CURLOPT_HEADERFUNCTION, callback_header)); CANNOT_FAIL(curl_easy_setopt(C, CURLOPT_HEADERDATA, (void *) &out->headers)); } if (req_flags & REQ_FLAG_DEBUG) { CANNOT_FAIL(curl_easy_setopt(C, CURLOPT_VERBOSE, 1L)); CANNOT_FAIL(curl_easy_setopt(C, CURLOPT_DEBUGFUNCTION, callback_debug)); CANNOT_FAIL(curl_easy_setopt(C, CURLOPT_DEBUGDATA, (void *) pd)); } CURLcode rc = curl_easy_perform(C); if (rc != CURLE_OK) { set_curl_error(out_errmsg, rc); return false; } CANNOT_FAIL(curl_easy_getinfo(C, CURLINFO_RESPONSE_CODE, &out->status)); return true; } ================================================ FILE: plugins/web/make_request.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include #include "libls/ls_string.h" #include "libls/ls_strarr.h" #include "include/plugin_data_v1.h" enum { REQ_FLAG_NEEDS_HEADERS = 1 << 0, REQ_FLAG_DEBUG = 1 << 1, }; typedef struct { long status; LS_StringArray headers; LS_String body; } Response; bool make_request( LuastatusPluginData *pd, int req_flags, CURL *C, Response *out, char **out_errmsg); ================================================ FILE: plugins/web/mod_json/json_decode.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "json_decode.h" #include #include #include #include #include #include // It is actually located in ${CMAKE_CURRENT_BINARY_DIR}. // CMakeLists.txt adds it to the "include directories" list. #include "json_config.generated.h" #if CJSON_FOUND_BY_PKG_CONFIG # include #else # include #endif #include "libls/ls_panic.h" typedef struct { int recur_lim; int lref_mt_array; int lref_mt_dict; bool mark_nulls; const char *err_descr; } Params; static int mt_new(lua_State *L, const char *field) { // L: ? lua_createtable(L, 0, 1); // L: ? mt lua_pushboolean(L, 1); // L: ? mt boolean lua_setfield(L, -2, field); // L: ? mt return luaL_ref(L, LUA_REGISTRYINDEX); // L: ? } static void mt_set(lua_State *L, int lref) { // L: ? mt if (lref == LUA_REFNIL) { return; } lua_rawgeti(L, LUA_REGISTRYINDEX, lref); // L: ? table mt lua_setmetatable(L, -2); // L: ? table } static void mt_unref(lua_State *L, int lref) { if (lref == LUA_REFNIL) { return; } luaL_unref(L, LUA_REGISTRYINDEX, lref); } // Forward declaration static bool convert(lua_State *L, cJSON *j, Params *params); static bool convert_array(lua_State *L, cJSON *j, Params *params) { int n = cJSON_GetArraySize(j); lua_createtable(L, n, 0); // L: table mt_set(L, params->lref_mt_array); unsigned i = 1; for (cJSON *item = j->child; item; item = item->next) { if (!convert(L, item, params)) { return false; } // L: table value lua_rawseti(L, -2, i); // L: table ++i; } return true; } static bool convert_dict(lua_State *L, cJSON *j, Params *params) { // /cJSON_GetArraySize()/ it works for dicts too int n = cJSON_GetArraySize(j); lua_createtable(L, 0, n); // L: table mt_set(L, params->lref_mt_dict); for (cJSON *item = j->child; item; item = item->next) { if (!convert(L, item, params)) { return false; } // L: table value lua_setfield(L, -2, item->string); // L: table } return true; } static bool convert(lua_State *L, cJSON *j, Params *params) { if (!params->recur_lim--) { params->err_descr = "depth limit exceeded"; return false; } if (!lua_checkstack(L, 10)) { params->err_descr = "too many elements on Lua stack"; return false; } if (cJSON_IsNull(j)) { if (params->mark_nulls) { lua_pushlightuserdata(L, NULL); } else { lua_pushnil(L); } return true; } else if (cJSON_IsTrue(j)) { lua_pushboolean(L, 1); return true; } else if (cJSON_IsFalse(j)) { lua_pushboolean(L, 0); return true; } else if (cJSON_IsNumber(j)) { lua_pushnumber(L, j->valuedouble); return true; } else if (cJSON_IsString(j)) { lua_pushstring(L, j->valuestring); return true; } else if (cJSON_IsArray(j)) { return convert_array(L, j, params); } else if (cJSON_IsObject(j)) { return convert_dict(L, j, params); } else { LS_MUST_BE_UNREACHABLE(); } } bool json_decode(lua_State *L, const char *input, int max_depth, int flags, char *errbuf, size_t nerrbuf) { LS_ASSERT(input != NULL); if (!lua_checkstack(L, max_depth)) { snprintf(errbuf, nerrbuf, "Lua failed to allocate stack of size %d", max_depth); return false; } if (strlen(input) > (INT_MAX - 16)) { snprintf(errbuf, nerrbuf, "JSON payload is too large"); return false; } // /cJSON_GetErrorPtr/ is not thread-safe, so we use /cJSON_ParseWithOpts/. const char *err_ptr; cJSON *j = cJSON_ParseWithOpts(input, &err_ptr, /*require_null_terminate=*/ 1); if (!j) { snprintf(errbuf, nerrbuf, "JSON parse error at byte %d", (int) (err_ptr - input)); return false; } Params params = { .recur_lim = max_depth, .lref_mt_array = LUA_REFNIL, .lref_mt_dict = LUA_REFNIL, .mark_nulls = false, .err_descr = NULL, }; if (flags & JSON_DEC_MARK_ARRAYS_VS_DICT) { params.lref_mt_array = mt_new(L, "is_array"); params.lref_mt_dict = mt_new(L, "is_dict"); } if (flags & JSON_DEC_MARK_NULLS) { params.mark_nulls = true; } bool is_ok = convert(L, j, ¶ms); mt_unref(L, params.lref_mt_array); mt_unref(L, params.lref_mt_dict); if (!is_ok) { snprintf(errbuf, nerrbuf, "%s", params.err_descr); } cJSON_Delete(j); return is_ok; } ================================================ FILE: plugins/web/mod_json/json_decode.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include #include enum { JSON_DEC_MARK_ARRAYS_VS_DICT = 1 << 0, JSON_DEC_MARK_NULLS = 1 << 1, }; bool json_decode(lua_State *L, const char *input, int max_depth, int flags, char *errbuf, size_t nerrbuf); ================================================ FILE: plugins/web/mod_json/json_encode_num.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "json_encode_num.h" #include #include #include "libls/ls_xallocf.h" char *json_encode_num(double d) { if (!isfinite(d)) { return NULL; } return ls_xallocf("%.20f", d); } ================================================ FILE: plugins/web/mod_json/json_encode_num.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once char *json_encode_num(double d); ================================================ FILE: plugins/web/mod_json/json_encode_str.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "json_encode_str.h" #include #include "libsafe/safev.h" #include "libsafe/mut_safev.h" static const SAFEV HEX_CHARS = SAFEV_STATIC_INIT_FROM_LITERAL("0123456789ABCDEF"); void json_encode_str( void (*append)(void *ud, SAFEV segment), void *append_ud, SAFEV v) { char esc_arr[] = {'\\', 'u', '0', '0', '#', '#'}; MUT_SAFEV esc = MUT_SAFEV_new_UNSAFE(esc_arr, sizeof(esc_arr)); size_t n = SAFEV_len(v); size_t prev = 0; for (size_t i = 0; i < n; ++i) { unsigned char c = SAFEV_at(v, i); if (c < 32 || c == '\\' || c == '"' || c == '/') { append(append_ud, SAFEV_subspan(v, prev, i)); MUT_SAFEV_set_at(esc, 4, SAFEV_at(HEX_CHARS, c / 16)); MUT_SAFEV_set_at(esc, 5, SAFEV_at(HEX_CHARS, c % 16)); append(append_ud, MUT_SAFEV_TO_SAFEV(esc)); prev = i + 1; } } append(append_ud, SAFEV_subspan(v, prev, n)); } ================================================ FILE: plugins/web/mod_json/json_encode_str.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include "libsafe/safev.h" void json_encode_str( void (*append)(void *ud, SAFEV segment), void *append_ud, SAFEV v); ================================================ FILE: plugins/web/mod_json/mod_json.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "mod_json.h" #include #include #include #include #include "libsafe/safev.h" #include "json_decode.h" #include "json_encode_str.h" #include "json_encode_num.h" static inline bool getbool(lua_State *L, int arg) { if (lua_isnoneornil(L, arg)) { return false; } luaL_checktype(L, arg, LUA_TBOOLEAN); return lua_toboolean(L, arg); } static int l_json_decode(lua_State *L) { enum { MAX_DEPTH = 100 }; const char *input = luaL_checkstring(L, 1); bool mark_arrays_vs_dicts = getbool(L, 2); bool mark_nulls = getbool(L, 3); int flags = 0; if (mark_arrays_vs_dicts) { flags |= JSON_DEC_MARK_ARRAYS_VS_DICT; } if (mark_nulls) { flags |= JSON_DEC_MARK_NULLS; } char errbuf[256]; bool is_ok = json_decode(L, input, MAX_DEPTH, flags, errbuf, sizeof(errbuf)); if (is_ok) { return 1; } else { lua_settop(L, 0); lua_pushnil(L); lua_pushstring(L, errbuf); return 2; } } static void append_to_lua_buf_callback(void *ud, SAFEV v) { luaL_Buffer *b = ud; luaL_addlstring(b, SAFEV_ptr_UNSAFE(v), SAFEV_len(v)); } static int l_json_encode_str(lua_State *L) { size_t ns; const char *s = luaL_checklstring(L, -1, &ns); luaL_Buffer b; luaL_buffinit(L, &b); json_encode_str(append_to_lua_buf_callback, &b, SAFEV_new_UNSAFE(s, ns)); luaL_pushresult(&b); // L: result return 1; } static int l_json_encode_num(lua_State *L) { double d = luaL_checknumber(L, 1); char *res = json_encode_num(d); if (res) { lua_pushstring(L, res); } else { lua_pushnil(L); } free(res); return 1; } void mod_json_register_funcs(lua_State *L) { #define REG(func, name) (lua_pushcfunction(L, (func)), lua_setfield((L), -2, (name))) REG(l_json_decode, "json_decode"); REG(l_json_encode_str, "json_encode_str"); REG(l_json_encode_num, "json_encode_num"); #undef REG } ================================================ FILE: plugins/web/mod_json/mod_json.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include void mod_json_register_funcs(lua_State *L); ================================================ FILE: plugins/web/mod_urlencode/mod_urlencode.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "mod_urlencode.h" #include #include #include #include #include "libsafe/safev.h" #include "libsafe/mut_safev.h" #include "urlencode.h" #include "urldecode.h" #include "libls/ls_alloc_utils.h" static inline bool getbool(lua_State *L, int arg) { if (lua_isnoneornil(L, arg)) { return false; } luaL_checktype(L, arg, LUA_TBOOLEAN); return lua_toboolean(L, arg); } static int l_urlencode(lua_State *L) { size_t ns; const char *s = luaL_checklstring(L, 1, &ns); bool plus_notation = getbool(L, 2); SAFEV src = SAFEV_new_UNSAFE(s, ns); size_t nres = urlencode_size(src, plus_notation); if (nres == (size_t) -1) { ls_oom(); } char *buf = LS_XNEW(char, nres); urlencode(src, MUT_SAFEV_new_UNSAFE(buf, nres), plus_notation); lua_pushlstring(L, buf, nres); free(buf); return 1; } static int l_urldecode(lua_State *L) { size_t ns; const char *s = luaL_checklstring(L, 1, &ns); SAFEV src = SAFEV_new_UNSAFE(s, ns); size_t nres = urldecode_size(src); char *buf = LS_XNEW(char, nres); if (!urldecode(src, MUT_SAFEV_new_UNSAFE(buf, nres))) { lua_pushnil(L); goto done; } lua_pushlstring(L, buf, nres); done: free(buf); return 1; } void mod_urlencode_register_funcs(lua_State *L) { #define REG(func, name) (lua_pushcfunction(L, (func)), lua_setfield((L), -2, (name))) REG(l_urlencode, "urlencode"); REG(l_urldecode, "urldecode"); #undef REG } ================================================ FILE: plugins/web/mod_urlencode/mod_urlencode.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include void mod_urlencode_register_funcs(lua_State *L); ================================================ FILE: plugins/web/mod_urlencode/urldecode.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "urldecode.h" #include #include #include "libls/ls_panic.h" size_t urldecode_size(SAFEV src) { size_t n = SAFEV_len(src); size_t res = 0; size_t i = 0; while (i < n) { if (SAFEV_at(src, i) == '%') { i += 3; } else { ++i; } ++res; } return res; } static int parse_hex_digit(char c) { if ('0' <= c && c <= '9') { return c - '0'; } if ('a' <= c && c <= 'f') { return c - 'a' + 10; } if ('A' <= c && c <= 'F') { return c - 'A' + 10; } return -1; } bool urldecode(SAFEV src, MUT_SAFEV dst) { size_t n = SAFEV_len(src); size_t i = 0; // position in /src/ size_t j = 0; // position in /dst/ while (i < n) { char c = SAFEV_at(src, i); char new_c; if (c == '%') { if (i + 2 >= n) { return false; } int hi = parse_hex_digit(SAFEV_at(src, i + 1)); int lo = parse_hex_digit(SAFEV_at(src, i + 2)); if (hi < 0 || lo < 0) { return false; } new_c = (hi << 4) | lo; i += 3; } else { new_c = (c == '+') ? ' ' : c; ++i; } MUT_SAFEV_set_at(dst, j, new_c); ++j; } size_t expected_result_size = SAFEV_len(MUT_SAFEV_TO_SAFEV(dst)); LS_ASSERT(j == expected_result_size); return true; } ================================================ FILE: plugins/web/mod_urlencode/urldecode.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include #include "libsafe/safev.h" #include "libsafe/mut_safev.h" size_t urldecode_size(SAFEV src); bool urldecode(SAFEV src, MUT_SAFEV dst); ================================================ FILE: plugins/web/mod_urlencode/urlencode.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "urlencode.h" #include #include #include "libls/ls_panic.h" static inline bool should_be_escaped(char c) { if ('A' <= c && c <= 'Z') { return false; } if ('a' <= c && c <= 'z') { return false; } if ('0' <= c && c <= '9') { return false; } switch (c) { case '-': // minus case '_': // underscore case '.': case '~': return false; default: return true; } } size_t urlencode_size(SAFEV src, bool plus_notation) { size_t n = SAFEV_len(src); size_t res = 0; for (size_t i = 0; i < n; ++i) { char c = SAFEV_at(src, i); size_t summand; if ((plus_notation && c == ' ') || !should_be_escaped(c)) { summand = 1; } else { summand = 3; } res += summand; if (res < summand) { // overflow return -1; } } return res; } static const SAFEV HEX_CHARS = SAFEV_STATIC_INIT_FROM_LITERAL("0123456789ABCDEF"); void urlencode(SAFEV src, MUT_SAFEV dst, bool plus_notation) { size_t n = SAFEV_len(src); size_t j = 0; // position in /dst/ for (size_t i = 0; i < n; ++i) { unsigned char c = SAFEV_at(src, i); if (plus_notation && c == ' ') { MUT_SAFEV_set_at(dst, j, '+'); ++j; } else if (should_be_escaped(c)) { MUT_SAFEV_set_at(dst, j + 0, '%'); MUT_SAFEV_set_at(dst, j + 1, SAFEV_at(HEX_CHARS, c / 16)); MUT_SAFEV_set_at(dst, j + 2, SAFEV_at(HEX_CHARS, c % 16)); j += 3; } else { MUT_SAFEV_set_at(dst, j, c); ++j; } } size_t expected_result_size = SAFEV_len(MUT_SAFEV_TO_SAFEV(dst)); LS_ASSERT(j == expected_result_size); } ================================================ FILE: plugins/web/mod_urlencode/urlencode.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include #include "libsafe/safev.h" #include "libsafe/mut_safev.h" size_t urlencode_size(SAFEV src, bool plus_notation); void urlencode(SAFEV src, MUT_SAFEV dst, bool plus_notation); ================================================ FILE: plugins/web/next_request_params.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include typedef struct { CURL *C; struct curl_slist *headers; int local_req_flags; } NextRequestParams; ================================================ FILE: plugins/web/opts.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "opts.h" #include #include #include #include #include "libls/ls_algo.h" #include "libls/ls_lua_compat.h" #include "libls/ls_panic.h" #include "libls/ls_time_utils.h" #include "set_error.h" #include "next_request_params.h" static bool apply_str(NextRequestParams *dst, lua_State *L, CURLoption which, char **out_errmsg) { // L: ? something if (!lua_isstring(L, -1)) { set_type_error(out_errmsg, L, -1, LUA_TSTRING, ""); return false; } const char *s = lua_tostring(L, -1); CURLcode rc = curl_easy_setopt(dst->C, which, (char *) s); if (rc != CURLE_OK) { set_curl_error(out_errmsg, rc); return false; } return true; } static bool apply_bool(NextRequestParams *dst, lua_State *L, CURLoption which, char **out_errmsg) { // L: ? something if (!lua_isboolean(L, -1)) { set_type_error(out_errmsg, L, -1, LUA_TBOOLEAN, ""); return false; } bool flag = lua_toboolean(L, -1); CURLcode rc = curl_easy_setopt(dst->C, which, (long) flag); if (rc != CURLE_OK) { set_curl_error(out_errmsg, rc); return false; } return true; } static bool fetch_int( lua_State *L, int *out_res, char **out_errmsg) { // L: ? something if (!lua_isnumber(L, -1)) { set_type_error(out_errmsg, L, -1, LUA_TNUMBER, ""); return false; } double fp = lua_tonumber(L, -1); if (isnan(fp)) { set_error(out_errmsg, "value is NaN"); return false; } if (fp < 0) { *out_res = -1; return true; } if (fp > INT_MAX) { set_error(out_errmsg, "value is greater than INT_MAX (%d)", (int) INT_MAX); return false; } *out_res = (int) fp; return true; } static bool apply_long_nonneg(NextRequestParams *dst, lua_State *L, CURLoption which, char **out_errmsg) { int val; if (!fetch_int(L, &val, out_errmsg)) { return false; } if (val < 0) { set_error(out_errmsg, "value is negative"); } CURLcode rc = curl_easy_setopt(dst->C, which, (long) val); if (rc != CURLE_OK) { set_curl_error(out_errmsg, rc); return false; } return true; } static bool apply_long_nonneg_or_minus1(NextRequestParams *dst, lua_State *L, CURLoption which, char **out_errmsg) { int val; if (!fetch_int(L, &val, out_errmsg)) { return false; } CURLcode rc = curl_easy_setopt(dst->C, which, (long) val); if (rc != CURLE_OK) { set_curl_error(out_errmsg, rc); return false; } return true; } static bool apply_headers(NextRequestParams *dst, lua_State *L, CURLoption which, char **out_errmsg) { (void) which; // This must be impossible. Still, let's check. if (dst->headers != NULL) { set_error(out_errmsg, "the headers were passed multiple times, somehow"); return false; } // L: ? something if (!lua_istable(L, -1)) { set_type_error(out_errmsg, L, -1, LUA_TTABLE, ""); return false; } // L: ? table size_t n = ls_lua_array_len(L, -1); for (size_t i = 1; i <= n; ++i) { lua_rawgeti(L, -1, i); // L: ? table header if (!lua_isstring(L, -1)) { set_type_error(out_errmsg, L, -1, LUA_TSTRING, "array element: "); return false; } const char *s = lua_tostring(L, -1); dst->headers = curl_slist_append(dst->headers, s); if (!dst->headers) { LS_PANIC("curl_slist_append() returned NULL: out of memory"); } lua_pop(L, 1); // L: ? table } CURLcode rc = curl_easy_setopt(dst->C, CURLOPT_HTTPHEADER, dst->headers); if (rc != CURLE_OK) { set_curl_error(out_errmsg, rc); return false; } return true; } static bool apply_timeout(NextRequestParams *dst, lua_State *L, CURLoption which, char **out_errmsg) { (void) which; // L: ? value if (!lua_isnumber(L, -1)) { set_type_error(out_errmsg, L, -1, LUA_TNUMBER, ""); return false; } double fp = lua_tonumber(L, -1); LS_TimeDelta TD; if (!ls_double_to_TD_checked(fp, &TD)) { set_error(out_errmsg, "invalid timeout"); return false; } int ms = ls_TD_to_poll_ms_tmo(TD); if (ms < 0) { ms = INT_MAX; } CURLcode rc = curl_easy_setopt(dst->C, CURLOPT_TIMEOUT_MS, (long) ms); if (rc != CURLE_OK) { set_curl_error(out_errmsg, rc); return false; } return true; } const Opt OPTS[] = { {"url", apply_str, CURLOPT_URL}, {"headers", apply_headers, 0}, {"timeout", apply_timeout, 0}, {"max_file_size", apply_long_nonneg, CURLOPT_MAXFILESIZE}, {"auto_referer", apply_bool, CURLOPT_AUTOREFERER}, {"custom_request", apply_str, CURLOPT_CUSTOMREQUEST}, {"follow_location", apply_bool, CURLOPT_FOLLOWLOCATION}, {"interface", apply_str, CURLOPT_INTERFACE}, {"max_redirs", apply_long_nonneg_or_minus1, CURLOPT_MAXREDIRS}, {"tcp_keepalive", apply_bool, CURLOPT_TCP_KEEPALIVE}, {"proxy", apply_str, CURLOPT_PROXY}, {"proxy_username", apply_str, CURLOPT_PROXYUSERNAME}, {"proxy_password", apply_str, CURLOPT_PROXYPASSWORD}, {"post_fields", apply_str, CURLOPT_COPYPOSTFIELDS}, }; const size_t OPTS_NUM = LS_ARRAY_SIZE(OPTS); ================================================ FILE: plugins/web/opts.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include #include #include #include "next_request_params.h" typedef struct { const char *spelling; bool (*apply)( NextRequestParams *dst, lua_State *L, CURLoption which, char **out_errmsg); CURLoption which; } Opt; extern const Opt OPTS[]; extern const size_t OPTS_NUM; enum { REQUIRED_OPTION_INDEX = 0 }; ================================================ FILE: plugins/web/parse_opts.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "parse_opts.h" #include #include #include #include "libls/ls_panic.h" #include "opts.h" #include "next_request_params.h" #include "set_error.h" #include "make_request.h" static int find_opt(const char *spelling) { for (size_t i = 0; i < OPTS_NUM; ++i) { if (strcmp(spelling, OPTS[i].spelling) == 0) { return i; } } return -1; } static bool check_if_our_flag(NextRequestParams *dst, const char *s) { if (strcmp(s, "with_headers") == 0) { dst->local_req_flags |= REQ_FLAG_NEEDS_HEADERS; return true; } if (strcmp(s, "debug") == 0) { dst->local_req_flags |= REQ_FLAG_DEBUG; return true; } return false; } static bool handle_option(NextRequestParams *dst, lua_State *L, char **out_errmsg, int *out_opt_idx) { // L: ? key value if (!lua_isstring(L, -2)) { set_type_error(out_errmsg, L, -2, LUA_TSTRING, "table key: "); return false; } const char *s = lua_tostring(L, -2); if (check_if_our_flag(dst, s)) { *out_opt_idx = -1; return true; } int opt_idx = find_opt(s); if (opt_idx < 0) { set_error(out_errmsg, "unknown option '%s'", s); return false; } const Opt *opt = &OPTS[opt_idx]; char *nested_errmsg; if (!opt->apply(dst, L, opt->which, &nested_errmsg)) { set_error(out_errmsg, "option '%s': %s", s, nested_errmsg); free(nested_errmsg); return false; } *out_opt_idx = opt_idx; return true; } bool parse_opts(NextRequestParams *dst, lua_State *L, char **out_errmsg) { LS_ASSERT(dst->C != NULL); LS_ASSERT(dst->headers == NULL); LS_ASSERT(lua_istable(L, -1)); bool got_required_option = false; // L: ? table lua_pushnil(L); // L: ? table nil while (lua_next(L, -2)) { // L: ? table key value int opt_idx; if (!handle_option(dst, L, out_errmsg, &opt_idx)) { return false; } if (opt_idx == REQUIRED_OPTION_INDEX) { got_required_option = true; } lua_pop(L, 1); // L: ? table key } // L: ? table if (!got_required_option) { const Opt *opt = &OPTS[REQUIRED_OPTION_INDEX]; set_error(out_errmsg, "missing required option '%s'", opt->spelling); return false; } return true; } ================================================ FILE: plugins/web/parse_opts.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include #include "next_request_params.h" bool parse_opts(NextRequestParams *dst, lua_State *L, char **out_errmsg); ================================================ FILE: plugins/web/set_error.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "set_error.h" #include #include #include #include #include "libls/ls_panic.h" void set_type_error(char **out_errmsg, lua_State *L, int pos, int expected_type, const char *prefix) { LS_ASSERT(prefix != NULL); set_error( out_errmsg, "%s" "expected %s, found %s", prefix, lua_typename(L, expected_type), luaL_typename(L, pos) ); } void set_curl_error(char **out_errmsg, CURLcode rc) { set_error(out_errmsg, "libcurl error: %s", curl_easy_strerror(rc)); } ================================================ FILE: plugins/web/set_error.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include #include "libls/ls_xallocf.h" #define set_error(out_errmsg, ...) (*(out_errmsg) = ls_xallocf(__VA_ARGS__)) void set_type_error(char **out_errmsg, lua_State *L, int pos, int expected_type, const char *prefix); void set_curl_error(char **out_errmsg, CURLcode rc); ================================================ FILE: plugins/web/web.c ================================================ /* * Copyright (C) 2015-2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "libmoonvisit/moonvisit.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_evloop_lfuncs.h" #include "libls/ls_io_utils.h" #include "libls/ls_time_utils.h" #include "libls/ls_string.h" #include "libls/ls_strarr.h" #include "libls/ls_lua_compat.h" #include "libls/ls_tls_ebuf.h" #include "next_request_params.h" #include "set_error.h" #include "parse_opts.h" #include "make_request.h" #include "opts.h" #include "compat_lua_resume.h" #include "mod_json/mod_json.h" #include "mod_urlencode/mod_urlencode.h" typedef struct { int global_req_flags; lua_State *coro; int lref; int pipefds[2]; } Priv; static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; ls_close(p->pipefds[0]); ls_close(p->pipefds[1]); free(p); } static lua_State *make_coro(lua_State *L, int *out_lref) { // L: ? func lua_State *coro = lua_newthread(L); // L: ? func thread *out_lref = luaL_ref(L, LUA_REGISTRYINDEX); // L: ? func lua_xmove(L, coro, 1); // L: ? // coro: func return coro; } static bool parse_planner(MoonVisit *mv, Priv *p) { lua_State *L = mv->L; // L: ? table lua_getfield(L, -1, "planner"); // L: ? table planner if (moon_visit_checktype_at(mv, "planner", -1, LUA_TFUNCTION) < 0) { return false; } p->coro = make_coro(L, &p->lref); // L: ? table return true; } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .global_req_flags = 0, .coro = NULL, .lref = LUA_REFNIL, .pipefds = {-1, -1}, }; char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Parse with_headers_global bool with_headers_global = false; if (moon_visit_bool(&mv, -1, "with_headers_global", &with_headers_global, true) < 0) { goto mverror; } if (with_headers_global) { p->global_req_flags |= REQ_FLAG_NEEDS_HEADERS; } // Parse debug_global bool debug_global = false; if (moon_visit_bool(&mv, -1, "debug_global", &debug_global, true) < 0) { goto mverror; } if (debug_global) { p->global_req_flags |= REQ_FLAG_DEBUG; } // Parse make_self_pipe bool mkpipe = false; if (moon_visit_bool(&mv, -1, "make_self_pipe", &mkpipe, true) < 0) { goto mverror; } if (mkpipe) { if (ls_self_pipe_open(p->pipefds) < 0) { LS_FATALF(pd, "ls_self_pipe_open: %s", ls_tls_strerror(errno)); goto error; } } // Parse planner if (!parse_planner(&mv, p)) { goto mverror; } return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); error: destroy(pd); return LUASTATUS_ERR; } static int l_get_supported_opts(lua_State *L) { lua_createtable(L, 0, OPTS_NUM); // L: ? table for (size_t i = 0; i < OPTS_NUM; ++i) { lua_pushboolean(L, 1); // L: ? table true lua_setfield(L, -2, OPTS[i].spelling); // L: ? table } return 1; } static int l_time_now(lua_State *L) { double res = ls_timespec_to_raw_double(ls_now_timespec()); lua_pushnumber(L, res); return 1; } static void register_funcs(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv; // L: table ls_self_pipe_push_luafunc(p->pipefds, L); // L: table func lua_setfield(L, -2, "wake_up"); // L: table lua_pushcfunction(L, l_get_supported_opts); // L: table func lua_setfield(L, -2, "get_supported_opts"); lua_pushcfunction(L, l_time_now); // L: table func lua_setfield(L, -2, "time_now"); mod_json_register_funcs(L); mod_urlencode_register_funcs(L); } typedef enum { NACT_REQUEST, NACT_SLEEP, NACT_CALL_CB, NACT__LAST, } NextAction; typedef enum { WUPSTAT_NO_WAKEUP, WUPSTAT_YES_WAKEUP, WUPSTAT_NOT_APPLICABLE, } WakeupStatus; typedef struct { NextAction nact; NextRequestParams next_req_params; LS_TimeDelta TD; char *what; WakeupStatus wakeup_status; } Ctx; static void clear_next_req_params(NextRequestParams *X) { if (X->headers) { curl_slist_free_all(X->headers); X->headers = NULL; } curl_easy_reset(X->C); X->local_req_flags = 0; } static void destroy_ctx(Ctx *ctx) { clear_next_req_params(&ctx->next_req_params); free(ctx->what); curl_easy_cleanup(ctx->next_req_params.C); } static bool parseY_request(lua_State *L, Ctx *ctx, char **out_errmsg) { // L: ? table lua_getfield(L, -1, "params"); // L: ? table params if (!lua_istable(L, -1)) { set_type_error(out_errmsg, L, -1, LUA_TTABLE, "'params' field: "); return false; } return parse_opts(&ctx->next_req_params, L, out_errmsg); } static bool parseY_sleep(lua_State *L, Ctx *ctx, char **out_errmsg) { // L: ? table lua_getfield(L, -1, "period"); // L: ? table period if (!lua_isnumber(L, -1)) { set_type_error(out_errmsg, L, -1, LUA_TNUMBER, "'period' field: "); return false; } double d = lua_tonumber(L, -1); if (!ls_double_to_TD_checked(d, &ctx->TD)) { set_error(out_errmsg, "invalid period"); return false; } return true; } static bool parseY_call_cb(lua_State *L, Ctx *ctx, char **out_errmsg) { // L: ? table lua_getfield(L, -1, "what"); // L: ? table what if (!lua_isstring(L, -1)) { set_type_error(out_errmsg, L, -1, LUA_TSTRING, "'what' field: "); return false; } free(ctx->what); ctx->what = ls_xstrdup(lua_tostring(L, -1)); return true; } static NextAction parse_action(const char *s) { if (strcmp(s, "request") == 0) { return NACT_REQUEST; } if (strcmp(s, "sleep") == 0) { return NACT_SLEEP; } if (strcmp(s, "call_cb") == 0) { return NACT_CALL_CB; } return NACT__LAST; } static bool parseY(lua_State *L, Ctx *ctx, char **out_errmsg) { if (!lua_istable(L, -1)) { set_type_error(out_errmsg, L, -1, LUA_TTABLE, "yielded value: "); return false; } // L: ? table lua_getfield(L, -1, "action"); // L: ? table action if (!lua_isstring(L, -1)) { set_type_error(out_errmsg, L, -1, LUA_TSTRING, "'action' field: "); return false; } ctx->nact = parse_action(lua_tostring(L, -1)); lua_pop(L, 1); // L: ? table switch (ctx->nact) { case NACT_REQUEST: clear_next_req_params(&ctx->next_req_params); return parseY_request(L, ctx, out_errmsg); case NACT_SLEEP: return parseY_sleep(L, ctx, out_errmsg); case NACT_CALL_CB: return parseY_call_cb(L, ctx, out_errmsg); case NACT__LAST: set_error(out_errmsg, "unknown action"); return false; } LS_MUST_BE_UNREACHABLE(); } static inline void push_wakeup_status(lua_State *L, WakeupStatus wakeup_status) { if (wakeup_status == WUPSTAT_NOT_APPLICABLE) { lua_pushnil(L); } else { lua_pushboolean(L, wakeup_status == WUPSTAT_YES_WAKEUP); } } static inline void push_planner_by_lref(Priv *p, lua_State *L, lua_State *coro) { // L: ? // coro: ? lua_rawgeti(L, LUA_REGISTRYINDEX, p->lref); // L: ? thread lua_xmove(L, coro, 1); // L: ? // coro: ? thread } static bool make_flash_call( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, Ctx *ctx) { Priv *p = pd->priv; lua_State *L = funcs.call_begin(pd->userdata); lua_State *coro = p->coro; int L_orig_top = lua_gettop(L); int coro_orig_top = lua_gettop(coro); char *errmsg = NULL; bool is_ok; // L: ? push_wakeup_status(L, ctx->wakeup_status); // L: ? wakeup_status ctx->wakeup_status = WUPSTAT_NOT_APPLICABLE; int nresults; push_planner_by_lref(p, L, coro); // coro: thread int rc = compat_lua_resume(coro, L, 1, &nresults); if (rc == LUA_YIELD) { if (nresults != 1) { set_error(&errmsg, "expected 1 yielded value, got %d", nresults); is_ok = false; goto done; } is_ok = parseY(coro, ctx, &errmsg); goto done; } else if (rc == 0) { // coro: uh, something; doesn't matter actually set_error(&errmsg, "coroutine finished its execution"); is_ok = false; goto done; } else { // coro: err const char *lua_errmsg = lua_tostring(coro, -1); if (!lua_errmsg) { lua_errmsg = "(Lua object cannot be converted to string)"; } set_error(&errmsg, "Lua error in planner: %s", lua_errmsg); is_ok = false; goto done; } done: lua_settop(coro, coro_orig_top); // coro: - lua_settop(L, L_orig_top); // L: ? funcs.call_cancel(pd->userdata); if (!is_ok) { LS_ASSERT(errmsg != NULL); LS_FATALF(pd, "%s", errmsg); } free(errmsg); return is_ok; } static void action_call_cb( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, Ctx *ctx) { lua_State *L = funcs.call_begin(pd->userdata); // L: ? lua_createtable(L, 0, 1); // L: ? table lua_pushstring(L, ctx->what); // L: ? table str lua_setfield(L, -2, "what"); // L: ? table funcs.call_end(pd->userdata); free(ctx->what); ctx->what = NULL; } static void push_headers(lua_State *L, LS_StringArray headers) { size_t n = ls_strarr_size(headers); if (n > (size_t) LS_LUA_MAXI) { lua_pushnil(L); return; } lua_createtable(L, ls_lua_num_prealloc(n), 0); // L: ? table headers for (size_t i = 0; i < n; ++i) { size_t ns; const char *s = ls_strarr_at(headers, i, &ns); lua_pushlstring(L, s, ns); // L: ? table headers str lua_rawseti(L, -2, i + 1); // L: ? table headers } } static void report_request_result_ok( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, long status, const char *body, size_t nbody, const LS_StringArray *p_headers) { lua_State *L = funcs.call_begin(pd->userdata); // L: ? lua_createtable(L, 0, 3); // L: ? table lua_pushstring(L, "response"); // L: ? table str lua_setfield(L, -2, "what"); // L: ? table lua_pushinteger(L, status); // L: ? table status lua_setfield(L, -2, "status"); // L: ? table if (p_headers) { push_headers(L, *p_headers); lua_setfield(L, -2, "headers"); // L: ? table } lua_pushlstring(L, body, nbody); // L: ? table body lua_setfield(L, -2, "body"); // L: ? table funcs.call_end(pd->userdata); } static void report_request_result_error( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, const char *err_descr, bool with_headers) { lua_State *L = funcs.call_begin(pd->userdata); // L: ? lua_createtable(L, 0, 4); // L: ? table lua_pushstring(L, "response"); // L: ? table str lua_setfield(L, -2, "what"); // L: ? table lua_pushinteger(L, 0); // L: ? table status lua_setfield(L, -2, "status"); // L: ? table lua_pushstring(L, ""); // L: ? table body lua_setfield(L, -2, "body"); // L: ? table if (with_headers) { lua_newtable(L); // L: ? table headers lua_setfield(L, -2, "headers"); // L: ? table } lua_pushstring(L, err_descr); // L: ? table body lua_setfield(L, -2, "error"); // L: ? table funcs.call_end(pd->userdata); } static void action_request( LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs, Ctx *ctx) { Priv *p = pd->priv; int req_flags = p->global_req_flags | ctx->next_req_params.local_req_flags; bool with_headers = req_flags & REQ_FLAG_NEEDS_HEADERS; Response resp; char *errmsg; if (make_request(pd, req_flags, ctx->next_req_params.C, &resp, &errmsg)) { report_request_result_ok( pd, funcs, resp.status, resp.body.data, resp.body.size, with_headers ? &resp.headers : NULL); } else { report_request_result_error(pd, funcs, errmsg, with_headers); free(errmsg); } ls_string_free(resp.body); ls_strarr_destroy(resp.headers); } static void action_sleep(LuastatusPluginData *pd, Ctx *ctx) { Priv *p = pd->priv; if (p->pipefds[0] >= 0) { int rc = ls_wait_input_on_fd(p->pipefds[0], ctx->TD); if (rc < 0) { LS_PANIC_WITH_ERRNUM("ls_wait_input_on_fd() failed", errno); } if (rc) { char ignored; ssize_t num_read = read(p->pipefds[0], &ignored, 1); (void) num_read; ctx->wakeup_status = WUPSTAT_YES_WAKEUP; } else { ctx->wakeup_status = WUPSTAT_NO_WAKEUP; } } else { ls_sleep(ctx->TD); } } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { CURL *C = curl_easy_init(); if (!C) { LS_FATALF(pd, "curl_easy_init() failed"); return; } Ctx ctx = { .nact = NACT__LAST, .next_req_params = { .C = C, .headers = NULL, .local_req_flags = 0, }, .TD = {0}, .what = NULL, .wakeup_status = WUPSTAT_NOT_APPLICABLE, }; for (;;) { if (!make_flash_call(pd, funcs, &ctx)) { break; } switch (ctx.nact) { case NACT_REQUEST: action_request(pd, funcs, &ctx); break; case NACT_SLEEP: action_sleep(pd, &ctx); break; case NACT_CALL_CB: action_call_cb(pd, funcs, &ctx); break; case NACT__LAST: LS_MUST_BE_UNREACHABLE(); } } destroy_ctx(&ctx); } LuastatusPluginIface luastatus_plugin_iface_v1 = { .init = init, .register_funcs = register_funcs, .run = run, .destroy = destroy, }; ================================================ FILE: plugins/xkb/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_plugin (plugin-xkb $ $ ${sources}) target_compile_definitions (plugin-xkb PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-xkb LUA) target_include_directories (plugin-xkb PUBLIC "${PROJECT_SOURCE_DIR}") find_package (PkgConfig REQUIRED) pkg_check_modules (xstuff REQUIRED x11) luastatus_target_build_with (plugin-xkb xstuff) luastatus_add_man_page (README.rst luastatus-plugin-xkb 7) ================================================ FILE: plugins/xkb/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-xkb .. :X-man-page-only: #################### .. :X-man-page-only: .. :X-man-page-only: ###################################### .. :X-man-page-only: X keyboard layout plugin for luastatus .. :X-man-page-only: ###################################### .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This plugin monitors current keyboard layout, and, optionally, the state of LED indicators (such as "Caps Lock" and "Num Lock"). Options ======= The following options are supported: * ``display``: string Display to connect to. Default is to use ``DISPLAY`` environment variable. * ``device_id``: number Keyboard device ID (as shown by ``xinput(1)``). Default is ``XkbUseCoreKbd``. * ``led``: boolean Also report (and subscribe to changes of) the state of LED indicators, such as "Caps Lock" and "Num Lock". * ``how``: string This option controls which method of obtaining the list of group names is used. Currently, there are two methods. The first one, codenamed "*wrongly*", is the default one; it consists of querying and parsing the ``_XKB_RULES_NAMES`` property of the root window. This method is known to be wrong, and on current Debian Sid it does not work as expected: on a setup with two keyboard layouts, English and Russian, it reports only the English one. Specify ``how="wrongly"`` to use this method. The second one, codenamed "*somehow*", consists of calling ``XkbGetNames(..., XkbSymbolsNameMask, ...)`` in order to obtain a string called "symbols", which looks like this:: pc+us+ru(winkeys):2+inet(evdev)+group(rctrl_toggle)+level3(ralt_switch)+capslock(ctrl_modifier)+typ Note that this string has been truncated to exactly 99 characters (the end should have been ``typo`` and then probably something else). It was not me who truncated it, but rather the guts of X11. And I have no idea why. The fact that this string can be truncated certainly does not contribute to the reliability of this method. We obtain the list of group names, then, by splitting the "symbols" by plus signs, and then filtering out known "bad" symbols (that do not indicate a keyboard layout). This is quite unreliable, but somehow works. Specify ``how="somehow"`` to use this method. * ``somehow_bad``: string Comma-separated list of bad symbols for the "somehow" method (note that normally it should not include spaces). Set to empty string to express an empty list. The default value is ``group,inet,pc``. ``cb`` argument =============== A table with the following entries: * ``name``: string Group name (if number of group names reported is sufficient). * ``id``: number Group ID (0, 1, 2, or 3). * ``requery``: boolean True if either this is the first call, or this call is due to a change in keyboard geometry, the *list* of layouts, or similar event that requires re-query of the list of group names. Otherwise, ``requery`` is nil. This is useful for widgets that fetch the list of group names via some external mechanism (e.g. by parsing the output of a command): they need to re-query the list if ``requery`` is true. * ``led_state``: number Bit mask representing the current state of LED indicators. On virtually all setups, bit ``(1 << 0) = 1`` is "Caps Lock", bit ``(1 << 1) = 2`` is "Num Lock", bit ``(1 << 2) = 4`` is "Scroll Lock". To list all indicators your X server knows of, run ``xset q``. If you look at the output, you will note there are a lot of weird things that are, in some reason, also considered indicators; for example, "Group 2" (that is, alternative keyboard layout) corresponds to ``(1 << 12) = 4096`` on my setup. So you should not assume that, just because your physical keyboard has no indicators for other things (or no indicators at all), the mask will never contain anything other than "Caps Lock", "Num Lock" and "Scroll Lock". Note that bitwise operations were only introduced in Lua 5.3. The "lowest common denominator" (working on all Lua versions) check if a bit is set is the following:: function is_set(mask, bit) return mask % (2 * bit) >= bit end Use it as follows:: cb = function(t) -- ...do something... if is_set(t.led_state, 1) then -- "Caps Lock" is ON -- ...do something... end if is_set(t.led_state, 2) then -- "Num Lock" is ON -- ...do something... end if is_set(t.led_state, 4) then -- "Scroll Lock" is ON -- ...do something... end -- ...do something... end, ================================================ FILE: plugins/xkb/somehow.c ================================================ /* * Copyright (C) 2021-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "somehow.h" #include #include #include #include #include #include #include #include #include "libls/ls_alloc_utils.h" char *somehow_fetch_symbols(Display *dpy, uint64_t deviceid) { char *ret = NULL; XkbDescRec *k = XkbAllocKeyboard(); if (!k) goto done; k->dpy = dpy; if (deviceid != XkbUseCoreKbd) k->device_spec = deviceid; XkbGetNames(dpy, XkbSymbolsNameMask, k); if (!k->names) goto done; Atom a = k->names->symbols; if (a == None) goto done; char *s = XGetAtomName(dpy, a); if (s) { ret = ls_xstrdup(s); XFree(s); } done: if (k) { XkbFreeNames(k, XkbSymbolsNameMask, True); XFree(k); } return ret; } // Assumes that /s[nbad]/ and /bad[nbad]/ are defined and set to either the next symbol or '\0'. static inline bool bad_matches(const char *bad, size_t nbad, const char *s, size_t ns) { if (ns >= nbad && memcmp(bad, s, nbad) == 0) { if (ns == nbad) return true; char c = s[nbad]; if (c == '(' || c == ':') return true; } return false; } static inline bool check_bad(const char *bad, const char *s, size_t ns) { if (!ns) return true; if (bad[0] == '\0') return false; for (const char *bad_next; (bad_next = strchr(bad, ',')); bad = bad_next + 1) if (bad_matches(bad, bad_next - bad, s, ns)) return true; return bad_matches(bad, strlen(bad), s, ns); } void somehow_parse_symbols(const char *symbols, LS_StringArray *out, const char *bad) { const char *prev = symbols; size_t balance = 0; for (;; ++symbols) { switch (*symbols) { case '(': ++balance; break; case ')': --balance; break; case '+': if (balance == 0) { if (!check_bad(bad, prev, symbols - prev)) { ls_strarr_append(out, prev, symbols - prev); } prev = symbols + 1; } break; case '\0': if (!check_bad(bad, prev, symbols - prev)) { ls_strarr_append(out, prev, symbols - prev); } return; } } } ================================================ FILE: plugins/xkb/somehow.h ================================================ /* * Copyright (C) 2021-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef somehow_h_ #define somehow_h_ #include #include #include "libls/ls_strarr.h" // Returns either NULL or a NUL-terminated string allocated as if with /malloc()/. char *somehow_fetch_symbols(Display *dpy, uint64_t deviceid); void somehow_parse_symbols(const char *symbols, LS_StringArray *out, const char *bad); #endif ================================================ FILE: plugins/xkb/wrongly.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "wrongly.h" #include #include #include #include #include #include #include "libls/ls_alloc_utils.h" static const char *NAMES_PROP_ATOM = "_XKB_RULES_NAMES"; static inline char *dup_and_advance(const char **pcur, const char *end) { const char *cur = *pcur; if (cur == end) return NULL; size_t n = strlen(cur); *pcur += n + 1; return ls_xmemdup(cur, n + 1); } bool wrongly_fetch(Display *dpy, WronglyResult *out) { bool ret = false; unsigned char *data = NULL; Atom rules_atom = XInternAtom(dpy, NAMES_PROP_ATOM, True); if (rules_atom == None) goto done; unsigned long ndata; long maxlen = 1024; for (;;) { Atom actual_type; int fmt; unsigned long bytes_after; if (XGetWindowProperty( dpy, DefaultRootWindow(dpy), rules_atom, 0L, maxlen, False, XA_STRING, &actual_type, &fmt, &ndata, &bytes_after, &data) != Success) { data = NULL; goto done; } if (actual_type != XA_STRING) goto done; if (fmt != 8) goto done; if (!bytes_after) break; if (maxlen > LONG_MAX / 2) goto done; maxlen *= 2; } const char *cur = (const char *) data; const char *end = cur + ndata; out->rules = dup_and_advance(&cur, end); out->model = dup_and_advance(&cur, end); out->layout = dup_and_advance(&cur, end); out->options = dup_and_advance(&cur, end); ret = true; done: if (data) XFree(data); return ret; } void wrongly_parse_layout(const char *layout, LS_StringArray *out) { const char *prev = layout; size_t balance = 0; for (;; ++layout) { switch (*layout) { case '(': ++balance; break; case ')': --balance; break; case ',': if (balance == 0) { ls_strarr_append(out, prev, layout - prev); prev = layout + 1; } break; case '\0': ls_strarr_append(out, prev, layout - prev); return; } } } ================================================ FILE: plugins/xkb/wrongly.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef wrongly_h_ #define wrongly_h_ #include #include #include "libls/ls_strarr.h" typedef struct { // These all are either zero-terminated strings allocated as if with /malloc()/, or /NULL/. char *rules; char *model; char *layout; char *options; } WronglyResult; bool wrongly_fetch(Display *dpy, WronglyResult *out); void wrongly_parse_layout(const char *layout, LS_StringArray *out); #endif ================================================ FILE: plugins/xkb/xkb.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "libmoonvisit/moonvisit.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_strarr.h" #include "wrongly.h" #include "somehow.h" // If this plugin is used, the whole process gets killed if a connection to the display is lost, // because Xlib is terrible. // // See: // * https://tronche.com/gui/x/xlib/event-handling/protocol-errors/XSetIOErrorHandler.html // * https://tronche.com/gui/x/xlib/event-handling/protocol-errors/XSetErrorHandler.html typedef enum { HOW_WRONGLY, HOW_SOMEHOW, } How; typedef struct { char *dpyname; uint64_t deviceid; How how; char *somehow_bad; bool led; } Priv; static int parse_how_str(MoonVisit *mv, void *ud, const char *s, size_t ns) { (void) ns; How *out = ud; if (strcmp(s, "wrongly") == 0) { *out = HOW_WRONGLY; return 1; } if (strcmp(s, "somehow") == 0) { *out = HOW_SOMEHOW; return 1; } moon_visit_err(mv, "unknown how string: '%s'", s); return -1; } static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; free(p->dpyname); free(p->somehow_bad); free(p); } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .dpyname = NULL, .deviceid = XkbUseCoreKbd, .how = HOW_WRONGLY, .somehow_bad = NULL, .led = false, }; char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Parse display if (moon_visit_str(&mv, -1, "display", &p->dpyname, NULL, true) < 0) goto mverror; // Parse device_id if (moon_visit_uint(&mv, -1, "device_id", &p->deviceid, true) < 0) goto mverror; // Parse how if (moon_visit_str_f(&mv, -1, "how", parse_how_str, &p->how, true) < 0) goto mverror; // Parse somehow_bad if (moon_visit_str(&mv, -1, "somehow_bad", &p->somehow_bad, NULL, true) < 0) goto mverror; if (!p->somehow_bad) p->somehow_bad = ls_xstrdup("group,inet,pc"); // Parse led if (moon_visit_bool(&mv, -1, "led", &p->led, true) < 0) goto mverror; // Call 'XInitThreads()' if we are the first entity to use libx11. static char dummy[1]; void **ptr = pd->map_get(pd->userdata, "flag:library_used:x11"); if (!*ptr) { if (!XInitThreads()) { LS_FATALF(pd, "XInitThreads() failed"); goto error; } *ptr = dummy; } return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); error: destroy(pd); return LUASTATUS_ERR; } static Display *open_dpy( LuastatusPluginData *pd, char *dpyname, int *ext_base_ev_code) { XkbIgnoreExtension(False); int event_out; int error_out; int reason_out; int major_in_out = XkbMajorVersion; int minor_in_out = XkbMinorVersion; Display *dpy = XkbOpenDisplay( dpyname, &event_out, &error_out, &major_in_out, &minor_in_out, &reason_out); if (dpy && reason_out == XkbOD_Success) { *ext_base_ev_code = event_out; return dpy; } const char *msg; switch (reason_out) { case XkbOD_BadLibraryVersion: msg = "bad XKB library version"; break; case XkbOD_ConnectionRefused: msg = "can't open display, connection refused"; break; case XkbOD_BadServerVersion: msg = "server has an incompatible extension version"; break; case XkbOD_NonXkbServer: msg = "extension is not present in the server"; break; default: msg = "unknown error"; break; } LS_FATALF(pd, "XkbOpenDisplay() failed: %s", msg); return NULL; } static bool query_wrongly(LuastatusPluginData *pd, Display *dpy, LS_StringArray *groups) { ls_strarr_clear(groups); WronglyResult res; if (!wrongly_fetch(dpy, &res)) { LS_WARNF(pd, "[wrongly] wrongly_fetch() failed"); return false; } if (res.layout) { LS_DEBUGF(pd, "[wrongly] layout string: %s", res.layout); wrongly_parse_layout(res.layout, groups); } else { LS_DEBUGF(pd, "[wrongly] layout string is NULL"); } free(res.rules); free(res.model); free(res.layout); free(res.options); return true; } static bool query_somehow(LuastatusPluginData *pd, Display *dpy, LS_StringArray *groups) { ls_strarr_clear(groups); Priv *p = pd->priv; char *res = somehow_fetch_symbols(dpy, p->deviceid); if (!res) { LS_WARNF(pd, "[somehow] somehow_fetch_symbols() failed"); return false; } LS_DEBUGF(pd, "[somehow] symbols string: %s", res); somehow_parse_symbols(res, groups, p->somehow_bad); free(res); return true; } static inline bool query(LuastatusPluginData *pd, Display *dpy, LS_StringArray *groups) { Priv *p = pd->priv; switch (p->how) { case HOW_WRONGLY: return query_wrongly(pd, dpy, groups); case HOW_SOMEHOW: return query_somehow(pd, dpy, groups); } LS_MUST_BE_UNREACHABLE(); } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { Priv *p = pd->priv; LS_StringArray groups = ls_strarr_new(); Display *dpy = NULL; int ext_base_ev_code; if (!(dpy = open_dpy(pd, p->dpyname, &ext_base_ev_code))) goto error; // So, the X server maintains the bit mask of events we are interesed in; we can alter this mask // for XKB-related events with 'XkbSelectEvents()' and 'XkbSelectEventDetails()'. // Both of those functions take // 'unsigned long bits_to_change, // unsigned long values_for_bits' // as the last two arguments. What it means is that, by invoking those functions, we say that we // only want to *change* the bits in our current event mask that are *set* in 'bits_to_change'; // and that we want to assign them the *values* of corresponding bits in 'values_for_bits'. // // In other words, the algorithm for the server is something like this: // // // clear bits that the client wants to be changed // mask &= ~bits_to_change; // // // set bits that the client wants to set // mask |= (values_for_bits & bits_to_change); // These are events that any XKB client *must* listen to, unrelated to layout/LED. const unsigned long BOILERPLATE_EVENTS_MASK = XkbNewKeyboardNotifyMask | XkbMapNotifyMask; // These are events related to LED. const unsigned long LED_EVENTS_MASK = XkbIndicatorStateNotifyMask | XkbIndicatorMapNotifyMask; if (XkbSelectEvents( dpy, p->deviceid, /*bits_to_change=*/BOILERPLATE_EVENTS_MASK, /*values_for_bits=*/BOILERPLATE_EVENTS_MASK) == False) { LS_FATALF(pd, "XkbSelectEvents() failed (boilerplate events)"); goto error; } if (XkbSelectEventDetails( dpy, p->deviceid, /*event_type=*/XkbStateNotify, /*bits_to_change=*/XkbAllStateComponentsMask, /*values_for_bits=*/XkbGroupStateMask) == False) { LS_FATALF(pd, "XkbSelectEventDetails() failed (layout events)"); goto error; } if (p->led) { if (XkbSelectEvents( dpy, p->deviceid, /*bits_to_change=*/LED_EVENTS_MASK, /*values_for_bits=*/LED_EVENTS_MASK) == False) { LS_FATALF(pd, "XkbSelectEvents() failed (LED events)"); goto error; } } bool requery_everything = true; size_t group = -1; unsigned led_state = -1; while (1) { // re-query everything, if needed if (requery_everything) { // re-query group names if (!query(pd, dpy, &groups)) { LS_WARNF(pd, "query() failed"); } // explicitly query current group XkbStateRec state; if (XkbGetState(dpy, p->deviceid, &state) != Success) { LS_FATALF(pd, "XkbGetState() failed"); goto error; } group = state.group; // check if group is valid and possibly re-query group names if (group >= ls_strarr_size(groups)) { LS_WARNF(pd, "group ID (%zu) is too large, requerying", group); if (!query(pd, dpy, &groups)) { LS_WARNF(pd, "query() failed"); } if (group >= ls_strarr_size(groups)) { LS_WARNF(pd, "group ID is still too large"); } } // explicitly query current state of LED indicators if (p->led) { if (XkbGetIndicatorState(dpy, p->deviceid, &led_state) != Success) { LS_FATALF(pd, "XkbGetIndicatorState() failed"); goto error; } } } // make a call lua_State *L = funcs.call_begin(pd->userdata); // L: - lua_createtable(L, 0, 2); // L: table lua_pushinteger(L, group); // L: table n lua_setfield(L, -2, "id"); // L: table if (group < ls_strarr_size(groups)) { size_t nbuf; const char *buf = ls_strarr_at(groups, group, &nbuf); lua_pushlstring(L, buf, nbuf); // L: table name lua_setfield(L, -2, "name"); // L: table } if (p->led) { lua_pushinteger(L, led_state); // L: table n lua_setfield(L, -2, "led_state"); // L: table } if (requery_everything) { lua_pushboolean(L, true); // L: table true lua_setfield(L, -2, "requery"); // L: table } funcs.call_end(pd->userdata); // wait for next event XEvent event = {0}; XNextEvent(dpy, &event); // interpret the event requery_everything = false; if (event.type == ext_base_ev_code) { XkbAnyEvent ev1; memcpy(&ev1, &event, sizeof(ev1)); switch (ev1.xkb_type) { case XkbNewKeyboardNotify: case XkbMapNotify: requery_everything = true; break; case XkbStateNotify: { XkbStateNotifyEvent ev2; memcpy(&ev2, &event, sizeof(ev2)); group = ev2.group; } break; case XkbIndicatorStateNotify: case XkbIndicatorMapNotify: { XkbIndicatorNotifyEvent ev2; memcpy(&ev2, &event, sizeof(ev2)); led_state = ev2.state; } break; default: LS_WARNF(pd, "got XKB event of unknown xkb_type %d", ev1.xkb_type); } } else { LS_WARNF(pd, "got X event of unknown type %d", event.type); } } error: if (dpy) XCloseDisplay(dpy); ls_strarr_destroy(groups); } LuastatusPluginIface luastatus_plugin_iface_v1 = { .init = init, .run = run, .destroy = destroy, }; ================================================ FILE: plugins/xtitle/CMakeLists.txt ================================================ file (GLOB sources "*.c") luastatus_add_plugin (plugin-xtitle $ $ ${sources}) target_compile_definitions (plugin-xtitle PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-xtitle LUA) target_include_directories (plugin-xtitle PUBLIC "${PROJECT_SOURCE_DIR}") find_package (PkgConfig REQUIRED) pkg_check_modules (XCBLIBS REQUIRED xcb xcb-ewmh xcb-icccm xcb-event) luastatus_target_build_with (plugin-xtitle XCBLIBS) luastatus_add_man_page (README.rst luastatus-plugin-xtitle 7) ================================================ FILE: plugins/xtitle/README.rst ================================================ .. :X-man-page-only: luastatus-plugin-xtitle .. :X-man-page-only: ####################### .. :X-man-page-only: .. :X-man-page-only: ######################################## .. :X-man-page-only: active window title plugin for luastatus .. :X-man-page-only: ######################################## .. :X-man-page-only: .. :X-man-page-only: :Copyright: LGPLv3 .. :X-man-page-only: :Manual section: 7 Overview ======== This plugin monitors the title of the active window. Options ======= * ``display``: string Display to connect to. Default is to use ``DISPLAY`` environment variable. * ``visible``: boolean If true, try to retrieve the title from the ``_NET_WM_VISIBLE_NAME`` atom. Default is false. * ``extended_fmt``: boolean If true, *class* and *instance* of the active window will also be reported; the argument to ``cb`` will be a table instead of string or nil. Defaults to false. ``cb`` argument =============== If ``extended_fmt`` option was not enabled (this is the default), the argument is a string with the title of the active window, or ``nil`` if there is no active window. If ``extended_fmt`` is enabled, the argument will be a table with keys ``class``, ``instance`` and ``title``. All values are strings or ``nil`` if there is no active window. ================================================ FILE: plugins/xtitle/xtitle.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "libmoonvisit/moonvisit.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_tls_ebuf.h" #include "libls/ls_io_utils.h" #include "libls/ls_time_utils.h" // some parts of this file (including the name) are proudly stolen from // xtitle (https://github.com/baskerville/xtitle). typedef struct { char *dpyname; bool visible; bool extended_fmt; } Priv; static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; free(p->dpyname); free(p); } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .dpyname = NULL, .visible = false, .extended_fmt = false, }; char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Parse display if (moon_visit_str(&mv, -1, "display", &p->dpyname, NULL, true) < 0) goto mverror; // Parse visible if (moon_visit_bool(&mv, -1, "visible", &p->visible, true) < 0) goto mverror; // Parse extended_fmt if (moon_visit_bool(&mv, -1, "extended_fmt", &p->extended_fmt, true) < 0) goto mverror; return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); //error: destroy(pd); return LUASTATUS_ERR; } typedef struct { xcb_connection_t *conn; xcb_ewmh_connection_t *ewmh; bool ewmh_inited; xcb_window_t root; int screenp; bool visible; bool extended_fmt; } Data; static inline void do_watch(Data *d, xcb_window_t win, bool state) { if (win == XCB_NONE) return; uint32_t value = state ? XCB_EVENT_MASK_PROPERTY_CHANGE : XCB_EVENT_MASK_NO_EVENT; xcb_change_window_attributes(d->conn, win, XCB_CW_EVENT_MASK, &value); } static inline bool get_active_window(Data *d, xcb_window_t *win) { return xcb_ewmh_get_active_window_reply( d->ewmh, xcb_ewmh_get_active_window(d->ewmh, d->screenp), win, NULL ) == 1; } static bool push_window_title(Data *d, lua_State *L, xcb_window_t win) { if (win == XCB_NONE) return false; xcb_ewmh_get_utf8_strings_reply_t ewmh_txt_prop; xcb_icccm_get_text_property_reply_t icccm_txt_prop; ewmh_txt_prop.strings = NULL; icccm_txt_prop.name = NULL; if (d->visible && xcb_ewmh_get_wm_visible_name_reply( d->ewmh, xcb_ewmh_get_wm_visible_name(d->ewmh, win), &ewmh_txt_prop, NULL ) == 1 && ewmh_txt_prop.strings) { lua_pushlstring(L, ewmh_txt_prop.strings, ewmh_txt_prop.strings_len); xcb_ewmh_get_utf8_strings_reply_wipe(&ewmh_txt_prop); return true; } if (xcb_ewmh_get_wm_name_reply( d->ewmh, xcb_ewmh_get_wm_name(d->ewmh, win), &ewmh_txt_prop, NULL ) == 1 && ewmh_txt_prop.strings) { lua_pushlstring(L, ewmh_txt_prop.strings, ewmh_txt_prop.strings_len); xcb_ewmh_get_utf8_strings_reply_wipe(&ewmh_txt_prop); return true; } if (xcb_icccm_get_wm_name_reply( d->conn, xcb_icccm_get_wm_name(d->conn, win), &icccm_txt_prop, NULL ) == 1 && icccm_txt_prop.name) { lua_pushlstring(L, icccm_txt_prop.name, icccm_txt_prop.name_len); xcb_icccm_get_text_property_reply_wipe(&icccm_txt_prop); return true; } return false; } static inline void push_window_title_or_nil(Data *d, lua_State *L, xcb_window_t win) { if (!push_window_title(d, L, win)) { lua_pushnil(L); } } static bool push_window_class_and_instance(Data *d, lua_State *L, xcb_window_t win) { if (win == XCB_NONE) { return false; } xcb_get_property_cookie_t cookie = xcb_icccm_get_wm_class(d->conn, win); xcb_icccm_get_wm_class_reply_t prop; if (!xcb_icccm_get_wm_class_reply(d->conn, cookie, &prop, NULL)) { return false; } const char *class_name = prop.class_name; if (!class_name) { class_name = ""; } lua_pushstring(L, class_name); // L: class const char *instance_name = prop.instance_name; if (!instance_name) { instance_name = ""; } lua_pushstring(L, instance_name); // L: instance xcb_icccm_get_wm_class_reply_wipe(&prop); return true; } static void push_arg(Data *d, lua_State *L, xcb_window_t win) { if (!d->extended_fmt) { push_window_title_or_nil(d, L, win); // L: title return; } lua_createtable(L, 0, 3); // L: table push_window_title_or_nil(d, L, win); // L: table title lua_setfield(L, -2, "title"); // L: table if (!push_window_class_and_instance(d, L, win)) { lua_pushnil(L); // L: table nil lua_pushnil(L); // L: table nil nil } // L: table class instance lua_setfield(L, -3, "instance"); // L: table class lua_setfield(L, -2, "class"); // L: table } // updates /*win/ and /*last_win/ if the active window was changed static bool title_changed( Data *d, xcb_generic_event_t *evt, xcb_window_t *win, xcb_window_t *last_win) { if (XCB_EVENT_RESPONSE_TYPE(evt) != XCB_PROPERTY_NOTIFY) return false; xcb_property_notify_event_t *pne = (xcb_property_notify_event_t *) evt; if (pne->atom == d->ewmh->_NET_ACTIVE_WINDOW) { do_watch(d, *last_win, false); if (get_active_window(d, win)) { do_watch(d, *win, true); *last_win = *win; } else { *win = *last_win = XCB_NONE; } return true; } if (*win != XCB_NONE && pne->window == *win && ((d->visible && pne->atom == d->ewmh->_NET_WM_VISIBLE_NAME) || pne->atom == d->ewmh->_NET_WM_NAME || pne->atom == XCB_ATOM_WM_NAME)) { return true; } return false; } static xcb_screen_iterator_t find_nth_screen(xcb_connection_t *conn, int n) { const xcb_setup_t *setup = xcb_get_setup(conn); xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup); for (int i = 0; i < n; ++i) xcb_screen_next(&iter); return iter; } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { Priv *p = pd->priv; Data d = { .conn = NULL, .ewmh = LS_XNEW(xcb_ewmh_connection_t, 1), .ewmh_inited = false, .visible = p->visible, .extended_fmt = p->extended_fmt, }; int err; // connect d.conn = xcb_connect(p->dpyname, &d.screenp); if ((err = xcb_connection_has_error(d.conn))) { LS_FATALF(pd, "xcb_connect: XCB error %d", err); // /xcb_disconnect/ should be called even if /xcb_connection_has_error/ returned non-zero, // so we should not set /d.conn/ to /NULL/ here. goto error; } // iterate over screens to find our root window d.root = find_nth_screen(d.conn, d.screenp).data->root; // initialize ewmh if ( xcb_ewmh_init_atoms_replies( d.ewmh, xcb_ewmh_init_atoms(d.conn, d.ewmh), NULL) == 0) { LS_FATALF(pd, "xcb_ewmh_init_atoms_replies() failed"); goto error; } d.ewmh_inited = true; xcb_window_t win = XCB_NONE; xcb_window_t last_win = XCB_NONE; // get initial active window; make a call on success. if (get_active_window(&d, &win)) { push_arg(&d, funcs.call_begin(pd->userdata), win); funcs.call_end(pd->userdata); } // set up initial watchers do_watch(&d, d.root, true); do_watch(&d, win, true); // poll for changes int fd = xcb_get_file_descriptor(d.conn); xcb_flush(d.conn); while (1) { int nfds = ls_wait_input_on_fd(fd, LS_TD_FOREVER); if (nfds < 0) { LS_FATALF(pd, "ls_wait_input_on_fd: %s", ls_tls_strerror(errno)); goto error; } else if (nfds > 0) { xcb_generic_event_t *evt; while ((evt = xcb_poll_for_event(d.conn))) { if (title_changed(&d, evt, &win, &last_win)) { push_arg(&d, funcs.call_begin(pd->userdata), win); funcs.call_end(pd->userdata); } free(evt); } if ((err = xcb_connection_has_error(d.conn))) { LS_FATALF(pd, "xcb_poll_for_event: XCB error %d", err); goto error; } } } error: if (d.ewmh_inited) { xcb_ewmh_connection_wipe(d.ewmh); } if (d.conn) { xcb_disconnect(d.conn); } free(d.ewmh); } LuastatusPluginIface luastatus_plugin_iface_v1 = { .init = init, .run = run, .destroy = destroy, }; ================================================ FILE: release.sh ================================================ #!/bin/sh set -e if [ "$#" -ne 1 ]; then echo >&2 "USAGE: $0 new_version" exit 2 fi RELEASE_BRANCH=release DEV_BRANCH=master NEW_VERSION=$1 T() { printf >&2 -- '-> %s\n' "$*" "$@" } T_redir_to_file() { local f=$1 shift printf >&2 -- '-> %s > %s\n' "$*" "$f" "$@" > "$f" } T_append_to_file() { local f=$1 shift printf >&2 -- '-> %s >> %s\n' "$*" "$f" "$@" >> "$f" } ask() { local ans echo "" echo -n " $* (“yes” to proceed) >>> " read -r ans || exit 3 if [ "$ans" != yes ]; then exit 3 fi } if ! T git checkout $RELEASE_BRANCH; then T git branch $RELEASE_BRANCH T git checkout $RELEASE_BRANCH fi last_msg=$(git log -1 --pretty=%B) release_msg="Release $NEW_VERSION" if [ "$last_msg" = "$release_msg" ]; then echo "" echo "Last commit message is “$last_msg”." echo "Apparently, you have run this script before, so exiting now." exit 1 fi T git merge $DEV_BRANCH T_redir_to_file VERSION echo $NEW_VERSION T_append_to_file RELEASE_NOTES echo T sed \ -e "1i\luastatus $NEW_VERSION ($(LC_ALL=C date +'%B %d, %Y'))\n===\n(Please describe the changes here.)\n" \ -e :a -e '/^\n*$/{$d;N;};/\n$/ba' \ -i RELEASE_NOTES T ${EDITOR:-edit} RELEASE_NOTES ask 'Commit?' T git add VERSION RELEASE_NOTES T git commit -m "$release_msg" cat < release(s)” link on the GitHub page of the project * The “Tags” tab * “Add release notes” * Fill in everything needed, click “Publish release” * Tell everybody about it!! === EOF ================================================ FILE: run_literally_all_tests.sh ================================================ #!/usr/bin/env bash set -e if (( $# != 1 )); then printf '%s\n' "USAGE: $0 BUILD_DIR" >&2 exit 2 fi # Resolve $1 into $BUILD_DIR according to the old working directory if [[ $1 == /* ]]; then BUILD_DIR=$1 else BUILD_DIR=$PWD/$1 fi cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" COMMON_D_ARGS=( -DBUILD_PLUGIN_PULSE=YES -DBUILD_PLUGIN_UNIXSOCK=YES -DBUILD_PLUGIN_WEB=YES -DBUILD_TESTS=YES ) OPTIONS=( WITH_UBSAN WITH_ASAN WITH_TSAN WITH_LSAN ) MAX_LAG_valgrind=500 MAX_LAG_sanitizers=250 LAST_OPTION='(none, just starting up)' get_cmake_D_args() { local enable_opt=$1 local opt for opt in "${OPTIONS[@]}"; do local yesno=NO if [[ $opt == $enable_opt ]]; then yesno=YES fi printf '%s\n' "-D${opt}=${yesno}" done } build_with_option() { LAST_OPTION=${2:-$1} cmake "${COMMON_D_ARGS[@]}" $(get_cmake_D_args "$1") "$BUILD_DIR" \ || return $? ( set -e; cd "$BUILD_DIR"; make; ) \ || return $? echo >&2 "Built with option: $LAST_OPTION" } trap ' if [[ $LAST_OPTION != "(ok)" ]]; then echo >&2 "Last enabled option: $LAST_OPTION" fi ' EXIT build_with_option '-' '(none)' ./tests/pt.sh "$BUILD_DIR" build_with_option '-' '(none, under valgrind/memcheck)' PT_TOOL=valgrind PT_MAX_LAG=$MAX_LAG_valgrind ./tests/pt.sh "$BUILD_DIR" build_with_option '-' '(none, under valgrind/helgrind)' PT_TOOL=helgrind PT_MAX_LAG=$MAX_LAG_valgrind ./tests/pt.sh "$BUILD_DIR" build_with_option '-' '(none, torture test)' ./tests/torture.sh "$BUILD_DIR" for opt in "${OPTIONS[@]}"; do build_with_option "$opt" PT_MAX_LAG=$MAX_LAG_sanitizers ./tests/pt.sh "$BUILD_DIR" done LAST_OPTION='(ok)' echo >&2 "OK" ================================================ FILE: run_luacheck.sh ================================================ #!/usr/bin/env bash set -e cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" if ! command -v luacheck >/dev/null; then echo >&2 'FATAL: "luacheck" command was not found. Please install it.' exit 1 fi rc=0 find -type f -name '*.lua' -print0 | xargs -0 luacheck --config ./.luacheckrc \ || rc=$? echo >&2 '==============================' if (( rc == 0 )); then echo >&2 'Everything is OK' else echo >&2 'luacheck failed; see above.' fi exit $rc ================================================ FILE: tests/.gitignore ================================================ /parrot /stopwatch /listnets ================================================ FILE: tests/CMakeLists.txt ================================================ luastatus_add_plugin_noinstall (plugin-mock $ $ "mock_plugin.c") target_compile_definitions (plugin-mock PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (plugin-mock LUA) target_include_directories (plugin-mock PUBLIC "${PROJECT_SOURCE_DIR}") luastatus_add_barlib_noinstall (barlib-mock $ "mock_barlib.c") target_compile_definitions (barlib-mock PUBLIC -D_POSIX_C_SOURCE=200809L) luastatus_target_compile_with (barlib-mock LUA) target_include_directories (barlib-mock PUBLIC "${PROJECT_SOURCE_DIR}") add_executable (parrot "parrot.c") target_compile_definitions (parrot PUBLIC -D_POSIX_C_SOURCE=200809L) # find pthreads set (CMAKE_THREAD_PREFER_PTHREAD TRUE) set (THREADS_PREFER_PTHREAD_FLAG TRUE) find_package (Threads REQUIRED) # link against pthreads target_link_libraries (parrot PUBLIC Threads::Threads) add_executable (stopwatch "stopwatch.c") target_compile_definitions (stopwatch PUBLIC -D_POSIX_C_SOURCE=200809L) add_executable (kcov_wrapper "kcov_wrapper.c") target_compile_definitions (kcov_wrapper PUBLIC -D_POSIX_C_SOURCE=200809L) if (BUILD_PLUGIN_INOTIFY) add_executable (listnets "listnets.c") target_compile_definitions (listnets PUBLIC -D_POSIX_C_SOURCE=200809L) endif () if (BUILD_PLUGIN_WEB) add_subdirectory ("httpserv") endif () ================================================ FILE: tests/README.txt ================================================ This directory contains two test systems for luastatus: * pt (plain tests): the main test system; it tests plugins, barlibs and luastatus itself. It is written in bash. * torture: stress tests for luastatus, under valgrind. kcov ---- kcov does not redirect the signals it receives to its child process. This is why we have the kcov_wrapper thing: it is intended for wrapping kcov DIR COMMAND [ARGS...] process. This wrapper spawns a process (normally kcov), then redirects the following signals: * SIGINT; * SIGTERM; * SIGHUP; * SIGQUIT to the child's child process. If the child spawns more than one process, then which grandchild is selected for sending signals is unspecified. Until the grandchild process is spawned, all signals received are queued. It works under Linux only (requires procfs to be mounted at /proc). Also, luastatus sometimes (rarely) segfaults when run under kcov. We don't know why. ================================================ FILE: tests/barlib-runners/runner-redirect-34 ================================================ #!/bin/sh exec "$@" 3<&0 0&1 1>&2 ================================================ FILE: tests/dlopen.supp ================================================ { Pattern 1 Memcheck:Leak match-leak-kinds: reachable fun:calloc ... fun:_dlerror_run fun:dlopen@@GLIBC_2.2.5 ... } { Pattern 2 Memcheck:Leak match-leak-kinds: reachable fun:malloc ... fun:dlclose ... } { Pattern 3 Memcheck:Leak match-leak-kinds: reachable fun:malloc ... fun:openaux ... fun:_dl_open fun:dlopen_doit fun:_dl_catch_error } { Pattern 4 Memcheck:Leak match-leak-kinds: reachable fun:calloc ... fun:openaux ... fun:_dl_open fun:dlopen_doit fun:_dl_catch_error } ================================================ FILE: tests/glib.supp ================================================ { glib-thread-fuckery-misc-1 Helgrind:Race ... fun:g_source_attach } { glib-thread-fuckery-misc-2 Helgrind:Race ... fun:g_bus_get_sync } { glib-thread-fuckery-misc-3 Helgrind:Race ... fun:g_dbus_connection_send_message_with_reply_sync } { glib-thread-fuckery-misc-4 Helgrind:Race ... fun:g_task_propagate_pointer } { glib-thread-fuckery-ctx-1 Helgrind:Race ... fun:g_main_context_acquire } { glib-thread-fuckery-ctx-2 Helgrind:Race ... fun:g_main_context_iteration } { glib-thread-fuckery-ctx-3 Helgrind:Race ... fun:g_main_context_release } { glib-thread-fuckery-ctx-4 Helgrind:Race ... fun:g_main_loop_run } { glib-thread-fuckery-wtf Helgrind:Race ... fun:g_variant_get_type } ================================================ FILE: tests/httpserv/.gitignore ================================================ /httpserv ================================================ FILE: tests/httpserv/CMakeLists.txt ================================================ file (GLOB sources "*.c") add_executable (httpserv ${sources}) target_compile_definitions (httpserv PUBLIC -D_POSIX_C_SOURCE=200809L) find_package (PkgConfig REQUIRED) pkg_check_modules (LWS REQUIRED libwebsockets) target_include_directories (httpserv SYSTEM PUBLIC ${LWS_INCLUDE_DIRS}) target_compile_options (httpserv PUBLIC ${LWS_CFLAGS_OTHER}) target_link_libraries (httpserv PUBLIC ${LWS_LIBRARIES}) ================================================ FILE: tests/httpserv/argparser.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "argparser.h" #include #include #include #include #include // If zero-terminated string /str/ starts with zero-terminated string /prefix/, returns // /str + strlen(prefix)/; otherwise, returns /NULL/. static const char *strfollow(const char *str, const char *prefix) { size_t nprefix = strlen(prefix); return strncmp(str, prefix, nprefix) == 0 ? str + nprefix : NULL; } static bool parse_int(const char *s, int *dst) { errno = 0; char *endptr; long res = strtol(s, &endptr, 10); if (errno != 0 || *s == '\0' || *endptr != '\0') { return false; } #if LONG_MAX > INT_MAX if (res < INT_MIN || res > INT_MAX) { return false; } #endif *dst = res; return true; } static ArgParser_Option *find_opt(ArgParser_Option *opts, const char *arg, const char **out_value) { for (ArgParser_Option *cur = opts; cur->spelling; ++cur) { const char *value = strfollow(arg, cur->spelling); if (value) { *out_value = value; return cur; } } return NULL; } bool arg_parser_parse( ArgParser_Option *opts, char **pos, int npos, int argc, char **argv, char *errbuf, size_t nerrbuf) { bool dash_dash_mode = false; int pos_idx = 0; for (int i = 1; i < argc; ++i) { char *arg = argv[i]; if (arg[0] == '-' && !dash_dash_mode) { // This is either an option or a dash-dash. if (strcmp(arg, "--") == 0) { // This is a dash-dash. dash_dash_mode = true; continue; } // This is an option. const char *value; ArgParser_Option *found = find_opt(opts, arg, &value); if (!found) { snprintf(errbuf, nerrbuf, "unknown option '%s'", arg); return false; } if (!parse_int(value, found->dst)) { snprintf(errbuf, nerrbuf, "cannot parse value of '%s' option as int", arg); return false; } } else { // This is a position argument. if (pos_idx == npos) { snprintf(errbuf, nerrbuf, "too many positional arguments"); return false; } pos[pos_idx++] = arg; } } if (pos_idx != npos) { snprintf(errbuf, nerrbuf, "not enough positional arguments"); return false; } return true; } ================================================ FILE: tests/httpserv/argparser.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include typedef struct { const char *spelling; int *dst; } ArgParser_Option; bool arg_parser_parse( ArgParser_Option *opts, char **pos, int npos, int argc, char **argv, char *errbuf, size_t nerrbuf); ================================================ FILE: tests/httpserv/buffer.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "buffer.h" #include #include #include "common.h" void buffer_append(Buffer *b, const char *chunk, size_t len) { if (!len) { return; } while (b->capacity - b->size < len) { b->data = x2realloc(b->data, &b->capacity, sizeof(char)); } memcpy(b->data + b->size, chunk, len); b->size += len; } void buffer_destroy(Buffer *b) { free(b->data); } ================================================ FILE: tests/httpserv/buffer.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include typedef struct { char *data; size_t size; size_t capacity; } Buffer; #define BUFFER_STATIC_INIT {0} void buffer_append(Buffer *b, const char *chunk, size_t len); void buffer_destroy(Buffer *b); ================================================ FILE: tests/httpserv/common.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "common.h" #include #include #include #include static inline size_t mul_zu_or_oom(size_t a, size_t b) { if (b && a > SIZE_MAX / b) { panic_oom(); } return a * b; } void *xrealloc(void *p, size_t n, size_t m) { size_t total = mul_zu_or_oom(n, m); if (!total) { free(p); return NULL; } void *r = realloc(p, total); if (!r) { panic_oom(); } return r; } void *x2realloc(void *p, size_t *pcapacity, size_t elem_sz) { if (*pcapacity == 0) { *pcapacity = 1; } else { *pcapacity = mul_zu_or_oom(*pcapacity, 2); } return xrealloc(p, *pcapacity, elem_sz); } void *xmemdup(const void *p, size_t n) { if (!n) { return NULL; } void *r = xmalloc(n, 1); memcpy(r, p, n); return r; } char *xstrdup(const char *s) { return xmemdup(s, strlen(s) + 1); } ================================================ FILE: tests/httpserv/common.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include #define xmalloc(n, m) xrealloc(NULL, (n), (m)) void *xrealloc(void *p, size_t n, size_t m); void *x2realloc(void *p, size_t *pcapacity, size_t elem_sz); void *xmemdup(const void *p, size_t n); char *xstrdup(const char *s); #define panic(s) (fprintf(stderr, "FATAL: %s\n", (s)), abort()) #define panic_oom() panic("out of memory") #if __GNUC__ >= 2 # define ATTR_NORETURN __attribute__((noreturn)) #else # define ATTR_NORETURN /*nothing*/ #endif ================================================ FILE: tests/httpserv/main.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include #include #include "server.h" #include "common.h" #include "sleep_millis.h" #include "argparser.h" typedef struct { const char *expected_path; bool expected_method_is_post; int max_requests; int freeze_for; } GlobalOptions; static GlobalOptions global_options = { .max_requests = -1, .freeze_for = 0, }; typedef struct { uint64_t cookie; int status; const char *status_text; } Request; typedef struct { Request *data; size_t size; size_t capacity; } RequestList; static RequestList request_list = {0}; Request *request_new(uint64_t cookie) { Request req = { .cookie = cookie, .status = 0, }; if (request_list.size == request_list.capacity) { request_list.data = x2realloc(request_list.data, &request_list.capacity, sizeof(Request)); } Request *p_req = &request_list.data[request_list.size++]; *p_req = req; return p_req; } static Request *request_find_or_die(uint64_t cookie) { for (size_t i = 0; i < request_list.size; ++i) { if (request_list.data[i].cookie == cookie) { return &request_list.data[i]; } } panic("cannot find request with given cookie"); } static void request_free(Request *req) { *req = request_list.data[request_list.size - 1]; --request_list.size; } static int request_prepare_error(Request *req, int status, const char *status_text) { req->status = status; req->status_text = status_text; return status; } static const char *strip_leading_slashes(const char *s) { while (*s == '/') { ++s; } return s; } int my_before_req_cb( uint64_t cookie, const char *path, bool is_method_post, char **out_mime_type, void *ud) { (void) ud; *out_mime_type = xstrdup("text/plain"); Request *req = request_new(cookie); const char *normalized_path_cur = strip_leading_slashes( path); const char *normalized_path_exp = strip_leading_slashes( global_options.expected_path); if (strcmp(normalized_path_cur, normalized_path_exp) != 0) { return request_prepare_error(req, 404, "Not Found"); } if (is_method_post != global_options.expected_method_is_post) { return request_prepare_error(req, 405, "Method Not Allowed"); } req->status = 200; return req->status; } static char *send_prepared_error(Request *req, size_t *out_len) { char buf[128]; snprintf(buf, sizeof(buf), "%d %s", req->status, req->status_text); *out_len = strlen(buf); return xstrdup(buf); } static char *my_write_body_cb( uint64_t cookie, const char *body, size_t nbody, size_t *out_len, void *ud) { (void) ud; Request *req = request_find_or_die(cookie); char *ret; int status = req->status; if (status != 200) { ret = send_prepared_error(req, out_len); goto done; } if (putchar('>') == EOF) { goto write_error; } for (size_t i = 0; i < nbody; ++i) { char c = body[i]; if (c == '\n') { c = ' '; } if (putchar((unsigned char) c) == EOF) { goto write_error; } } if (putchar('\n') == EOF) { goto write_error; } if (fflush(stdout) == EOF) { goto write_error; } char *buf = NULL; size_t nbuf = 0; ssize_t r = getline(&buf, &nbuf, stdin); if (r < 0) { perror("getline"); panic("getline() failed"); } else if (r == 0) { panic("got EOF"); } if (buf[r - 1] == '\n') { --r; } *out_len = r; ret = buf; done: request_free(req); return ret; write_error: panic("cannot write to stdout"); } static void freeze_or_die(void) { if (global_options.freeze_for < 0) { fprintf(stderr, "Existing as requested by --max-requests=...\n"); exit(0); } else if (global_options.freeze_for > 0) { fprintf(stderr, "Freezing as requested by --freeze-for=...\n"); sleep_millis(global_options.freeze_for); } } static void my_after_req_cb( void *ud) { (void) ud; static unsigned req_count = 0; ++req_count; if ( global_options.max_requests >= 0 && req_count == (unsigned) global_options.max_requests) { freeze_or_die(); req_count = 0; } } static void my_ready_cb( void *ud) { (void) ud; if (fputs("ready\n", stdout) == EOF) { goto fail; } if (fflush(stdout) == EOF) { goto fail; } return; fail: panic("cannot write to stdout"); } ATTR_NORETURN static void print_usage_and_die(const char *s) { fprintf(stderr, "Wrong USAGE: %s\n", s); fprintf(stderr, "USAGE: httpserv [--port=PORT] [--max-requests=N] [--freeze-for=MILLIS] METHOD PATH\n"); exit(2); } static bool parse_method_or_die(const char *s) { if (strcmp(s, "GET") == 0) { return false; } if (strcmp(s, "POST") == 0) { return true; } print_usage_and_die("invalid METHOD"); } int main(int argc, char **argv) { int port = 8080; char *pos[2]; ArgParser_Option options[] = { {"--port=", &port}, {"--max-requests=", &global_options.max_requests}, {"--freeze-for=", &global_options.freeze_for}, {0}, }; char errbuf[128]; if (!arg_parser_parse(options, pos, 2, argc, argv, errbuf, sizeof(errbuf))) { print_usage_and_die(errbuf); } if (port < 0 || port > 0xFFFF) { print_usage_and_die("invalid PORT"); } global_options.expected_method_is_post = parse_method_or_die(pos[0]); global_options.expected_path = xstrdup(pos[1]); run_server( port, my_before_req_cb, my_write_body_cb, my_after_req_cb, my_ready_cb, NULL); } ================================================ FILE: tests/httpserv/server.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "server.h" #include #include #include #include #include "buffer.h" #include "common.h" typedef struct { BeforeRequestCallback before_req_cb; WriteBodyCallback write_body_cb; AfterRequestCallback after_req_cb; ReadyCallback ready_cb; void *ud; } Callbacks; static Callbacks callbacks; typedef struct { uint64_t cookie; Buffer buf; bool done_writing; } MyContext; static uint64_t next_cookie; static void my_context_new(MyContext *my_ctx) { *my_ctx = (MyContext) { .cookie = next_cookie++, .buf = BUFFER_STATIC_INIT, .done_writing = false, }; } static int my_context_before_request( MyContext *my_ctx, const char *path, bool is_method_post, char **out_mime_type) { return callbacks.before_req_cb( my_ctx->cookie, path, is_method_post, out_mime_type, callbacks.ud ); } static void my_context_add_chunk(MyContext *ctx, const char *chunk, size_t len) { buffer_append(&ctx->buf, chunk, len); } static char *my_context_write_body( MyContext *my_ctx, size_t *out_len) { return callbacks.write_body_cb( my_ctx->cookie, my_ctx->buf.data, my_ctx->buf.size, out_len, callbacks.ud ); } static void my_context_destroy(MyContext *my_ctx) { buffer_destroy(&my_ctx->buf); my_ctx->buf = (Buffer) BUFFER_STATIC_INIT; } static int my_callback( struct lws *wsi, enum lws_callback_reasons reason, void *userdata, void *in, size_t len) { MyContext *my_ctx = userdata; uint8_t buf[LWS_PRE + 512]; uint8_t *start = buf + LWS_PRE; uint8_t *end = buf + sizeof(buf) - 1; uint8_t *p = start; if (reason == LWS_CALLBACK_HTTP) { my_context_new(my_ctx); char *mime_type; int status = my_context_before_request( my_ctx, /*path=*/ in, /*is_method_post=*/ lws_hdr_total_length(wsi, WSI_TOKEN_POST_URI) > 0, /*out_mime_type=*/ &mime_type ); bool is_ok = true; if (lws_add_http_common_headers( wsi, status, mime_type, LWS_ILLEGAL_HTTP_CONTENT_LEN, &p, end) < 0) { is_ok = false; } if (lws_finalize_write_http_header(wsi, start, &p, end)) { is_ok = false; } free(mime_type); if (is_ok) { lws_callback_on_writable(wsi); } return is_ok ? 0 : -1; } else if (reason == LWS_CALLBACK_HTTP_BODY) { my_context_add_chunk(my_ctx, in, len); return 0; } else if (reason == LWS_CALLBACK_HTTP_BODY_COMPLETION) { lws_callback_on_writable(wsi); return 0; } else if (reason == LWS_CALLBACK_HTTP_WRITEABLE) { if (!my_ctx) { return -1; } if (my_ctx->done_writing) { return 0; } my_ctx->done_writing = true; size_t nbody; char *body = my_context_write_body(my_ctx, &nbody); if (nbody > INT_MAX) { panic("response body is too large"); } int write_rc = lws_write(wsi, (uint8_t *) body, nbody, LWS_WRITE_HTTP_FINAL); my_context_destroy(my_ctx); free(body); if (write_rc != (int) nbody) { return -1; } if (lws_http_transaction_completed(wsi)) { return -1; } return 0; } else if (reason == LWS_CALLBACK_CLOSED_HTTP) { my_context_destroy(my_ctx); callbacks.after_req_cb(callbacks.ud); return 0; } else { return 0; } } static struct lws_protocols protocols[] = { {"http", my_callback, sizeof(MyContext), 0, 0, NULL, 0}, {NULL, NULL, 0, 0, 0, NULL, 0}, }; static const struct lws_http_mount mount = { .mount_next = NULL, .mountpoint = "/", .mountpoint_len = 1, .origin = NULL, .def = NULL, .protocol = "http", .cgienv = NULL, .extra_mimetypes = NULL, .interpret = NULL, .cgi_timeout = 0, .cache_max_age = 0, .auth_mask = 0, .cache_reusable = 0, .cache_revalidate = 0, .cache_intermediaries = 0, .origin_protocol = LWSMPRO_CALLBACK, .basic_auth_login_file = NULL, }; bool run_server( int port, BeforeRequestCallback before_req_cb, WriteBodyCallback write_body_cb, AfterRequestCallback after_req_cb, ReadyCallback ready_cb, void *ud) { callbacks = (Callbacks) { .before_req_cb = before_req_cb, .write_body_cb = write_body_cb, .after_req_cb = after_req_cb, .ready_cb = ready_cb, .ud = ud, }; signal(SIGPIPE, SIG_IGN); struct lws_context_creation_info info = { .port = port, .protocols = protocols, .mounts = &mount, .options = LWS_SERVER_OPTION_DISABLE_IPV6, }; lws_set_log_level(LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE, NULL); struct lws_context *context = lws_create_context(&info); if (!context) { fprintf(stderr, "lws_create_context() failed\n"); return false; } bool first = true; while (lws_service(context, 1000) >= 0) { if (first) { callbacks.ready_cb(callbacks.ud); first = false; } } // unreachable lws_context_destroy(context); return true; } ================================================ FILE: tests/httpserv/server.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once #include #include #include typedef int (*BeforeRequestCallback)( uint64_t cookie, const char *path, bool is_method_post, char **out_mime_type, void *ud); typedef char *(*WriteBodyCallback)( uint64_t cookie, const char *body, size_t nbody, size_t *out_len, void *ud); typedef void (*AfterRequestCallback)( void *ud); typedef void (*ReadyCallback)( void *ud); bool run_server( int port, BeforeRequestCallback before_req_cb, WriteBodyCallback write_body_cb, AfterRequestCallback after_req_cb, ReadyCallback ready_cb, void *ud ); ================================================ FILE: tests/httpserv/sleep_millis.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include "sleep_millis.h" #include #include #include void sleep_millis(int millis) { assert(millis >= 0); struct timespec ts = { .tv_sec = millis / 1000, .tv_nsec = (millis % 1000) * 1000 * 1000, }; struct timespec rem; while (nanosleep(&ts, &rem) < 0 && errno == EINTR) { ts = rem; } } ================================================ FILE: tests/httpserv/sleep_millis.h ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #pragma once void sleep_millis(int millis); ================================================ FILE: tests/kcov_wrapper.c ================================================ /* * Copyright (C) 2026 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ // This thing is intended for wrapping "kcov DIR COMMAND [ARGS...]" process: kcov // does not redirect the signals it receives to its child process. // // This wrapper spawns a process (normally kcov), then redirects the following signals: // * SIGINT; // * SIGTERM; // * SIGHUP; // * SIGQUIT // to the child's child process. If the child spawns more than one process, then which // grandchild is selected for sending signals is unspecified. // // Until the grandchild process is spawned, all signals received are queued. // // It works under Linux only (requires procfs to be mounted at /proc). #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define MY_NAME "kcov_wrapper" static inline void say_perror(const char *msg) { fprintf(stderr, "%s: %s: %s\n", MY_NAME, msg, strerror(errno)); } static inline void say_error(const char *msg) { fprintf(stderr, "%s: %s\n", MY_NAME, msg); } enum { RC_SPAWN_ERROR = 50, RC_UNEXPECTED_ERROR = 51, RC_CHILD_TERMINATED_IN_WEIRD_WAY = 99, }; static int self_pipe_fds[2] = {-1, -1}; static int make_cloexec(int fd) { int flags = fcntl(fd, F_GETFD); if (flags < 0) { return -1; } flags |= FD_CLOEXEC; if (fcntl(fd, F_SETFD, flags) < 0) { return -1; } return fd; } static bool make_self_pipe(void) { if (pipe(self_pipe_fds) < 0) { say_perror("pipe"); return false; } (void) make_cloexec(self_pipe_fds[0]); (void) make_cloexec(self_pipe_fds[1]); return true; } static int full_read(int fd, char *buf, size_t nbuf) { size_t nread = 0; while (nread != nbuf) { ssize_t r = read(fd, buf + nread, nbuf - nread); if (r < 0) { if (errno == EINTR) { continue; } return -1; } else if (r == 0) { return 0; } else { nread += r; } } return 1; } static int wait_for_input_on_fd(int fd, int timeout_ms) { struct pollfd pfd = {.fd = fd, .events = POLLIN}; int rc = poll(&pfd, 1, timeout_ms); if (rc < 0 && errno != EINTR) { say_perror("poll"); abort(); } return rc; } static int full_write(int fd, const char *data, size_t ndata) { size_t nwritten = 0; while (nwritten != ndata) { ssize_t w = write(fd, data + nwritten, ndata - nwritten); if (w < 0) { if (errno == EINTR) { continue; } return -1; } nwritten += w; } return 0; } static void sig_handler(int signo) { int saved_errno = errno; if (full_write(self_pipe_fds[1], (char *) &signo, sizeof(signo)) < 0) { abort(); } errno = saved_errno; } static bool handle_signal(int signo) { sigset_t empty; if (sigemptyset(&empty) < 0) { say_perror("sigemptyset"); return false; } int flags = SA_RESTART; if (signo == SIGCHLD) { flags |= SA_NOCLDSTOP; } struct sigaction sa = { .sa_handler = sig_handler, .sa_mask = empty, .sa_flags = flags, }; if (sigaction(signo, &sa, NULL) < 0) { say_perror("sigaction"); return false; } return true; } static const int TERM_SIGNALS[] = { SIGINT, SIGTERM, SIGHUP, SIGQUIT, }; enum { TERM_SIGNALS_NUM = sizeof(TERM_SIGNALS) / sizeof(TERM_SIGNALS[0]) }; static bool install_signal_handlers(void) { if (!handle_signal(SIGCHLD)) { return false; } for (int i = 0; i < TERM_SIGNALS_NUM; ++i) { if (!handle_signal(TERM_SIGNALS[i])) { return false; } } return true; } static int wait_and_make_exit_code(pid_t pid) { int status; pid_t rc; while ((rc = waitpid(pid, &status, 0)) < 0 && errno == EINTR) { // do nothing } if (rc < 0) { say_perror("waitpid"); return RC_UNEXPECTED_ERROR; } else if (rc == 0) { say_error("waitpid returned 0"); return RC_UNEXPECTED_ERROR; } if (WIFEXITED(status)) { return WEXITSTATUS(status); } if (WIFSIGNALED(status)) { return 128 + WTERMSIG(status); } return RC_CHILD_TERMINATED_IN_WEIRD_WAY; } static char *read_line(FILE *f) { char *buf = NULL; size_t nbuf = 0; if (getline(&buf, &nbuf, f) < 0) { free(buf); return NULL; } return buf; } static bool parse_imax(const char *s, intmax_t *out) { errno = 0; char *endptr; *out = strtoimax(s, &endptr, 10); if (errno || endptr == s || endptr[0] != '\0') { return false; } return true; } static void trim_newline(char *s) { size_t ns = strlen(s); if (ns && s[ns - 1] == '\n') { s[ns - 1] = '\0'; } } static pid_t get_grandchild_pid(pid_t child_pid) { static const char *SHELL_PROGRAM_SUFFIX = "cd /proc || exit $?\n" "exec 2>/dev/null || exit $?\n" "for pid in [0-9]*; do\n" " read _ _ _ ppid _ < $pid/stat || continue\n" " if [ $ppid = $target_pid ]; then\n" " x=$(readlink $pid/exe) || continue\n" " case \"$x\" in\n" " */luastatus) echo $pid; exit $? ;;\n" " esac" " fi\n" "done\n" "echo 0\n" ; enum { CAPACITY = 16 * 1024 }; char *prog = malloc(CAPACITY); if (!prog) { say_perror("malloc"); abort(); } int nprog = snprintf(prog, CAPACITY, "target_pid=%jd; %s", (intmax_t) child_pid, SHELL_PROGRAM_SUFFIX); if (nprog < 0 || nprog > CAPACITY - 32) { say_error("snprintf failed (shell program)"); abort(); } pid_t res = -1; char *line = NULL; FILE *f = popen(prog, "r"); if (!f) { say_perror("popen"); goto cleanup; } line = read_line(f); if (!line) { say_perror("cannot read line from shell program"); goto cleanup; } trim_newline(line); intmax_t raw_res; if (!parse_imax(line, &raw_res)) { say_error("cannot parse line from shell program into intmax_t"); goto cleanup; } res = raw_res; cleanup: free(line); if (f) { int status = pclose(f); if (status < 0 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) { say_error("our shell program failed"); res = -1; } } free(prog); return res; } typedef struct { pid_t child_pid; pid_t grandchild_pid; } State; static bool state_has_grandchild_pid(State *s) { if (s->grandchild_pid) { return true; } pid_t new_pid = get_grandchild_pid(s->child_pid); if (new_pid < 0) { say_error("(warning) cannot get real luastatus' PID"); return false; } else if (new_pid == 0) { say_error("(warning) real luastatus was not spawned yet"); return false; } else { fprintf(stderr, "%s: info: real luastatus' PID = %jd\n", MY_NAME, (intmax_t) new_pid); s->grandchild_pid = new_pid; return true; } } static void state_kill(State *s, int signo) { fprintf(stderr, "%s: info: killing luastatus with signal %d\n", MY_NAME, signo); if (kill(s->grandchild_pid, signo) < 0) { say_perror("(warning) kill"); } } typedef struct { int signums[TERM_SIGNALS_NUM]; } QueuedSignals; #define queued_signals_nonempty(Q_) (((Q_)->signums[0]) != 0) #define queued_signals_at(Q_, Idx_) ((Q_)->signums[(Idx_)]) static inline void queued_signals_add(QueuedSignals *q, int signo) { bool found = false; for (int i = 0; i < TERM_SIGNALS_NUM; ++i) { found |= (TERM_SIGNALS[i] == signo); } if (!found) { return; } for (int i = 0; i < TERM_SIGNALS_NUM; ++i) { if (q->signums[i] == signo) { return; } else if (!q->signums[i]) { q->signums[i] = signo; return; } } } int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "USAGE: %s COMMAND [ARGS...]\n", MY_NAME); return 2; } char **child_argv = argv + 1; if (!make_self_pipe()) { return RC_UNEXPECTED_ERROR; } if (!install_signal_handlers()) { return RC_UNEXPECTED_ERROR; } extern char **environ; pid_t child_pid; int rc = posix_spawnp( &child_pid, child_argv[0], NULL, NULL, child_argv, environ); if (rc != 0) { errno = rc; say_perror("posix_spawnp"); return RC_SPAWN_ERROR; } fprintf(stderr, "%s: info: child PID: %jd\n", MY_NAME, (intmax_t) child_pid); State state = {.child_pid = child_pid, .grandchild_pid = 0}; QueuedSignals q = {0}; for (;;) { int poll_rc = wait_for_input_on_fd(self_pipe_fds[0], /*timeout_ms=*/ 200); if (poll_rc == 0) { // Timeout if (queued_signals_nonempty(&q)) { say_error("timeout and we have queued signals"); if (state_has_grandchild_pid(&state)) { for (int i = 0; i < TERM_SIGNALS_NUM; ++i) { int signo = queued_signals_at(&q, i); if (signo) { state_kill(&state, signo); } } } q = (QueuedSignals) {0}; } continue; } int signo; int read_rc = full_read(self_pipe_fds[0], (char *) &signo, sizeof(signo)); if (read_rc < 0) { say_perror("read (from self-pipe)"); return RC_UNEXPECTED_ERROR; } else if (read_rc == 0) { say_error("read (from self-pipe) returned 0"); return RC_UNEXPECTED_ERROR; } if (signo == SIGCHLD) { fprintf(stderr, "%s: info: got SIGCHLD\n", MY_NAME); return wait_and_make_exit_code(child_pid); } else { fprintf(stderr, "%s: info: got signal %d\n", MY_NAME, signo); if (state_has_grandchild_pid(&state)) { state_kill(&state, signo); } else { say_error("queueing this signal"); queued_signals_add(&q, signo); } } } } ================================================ FILE: tests/listnets.c ================================================ /* * Copyright (C) 2021-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include int main() { struct ifaddrs *nets; if (getifaddrs(&nets) < 0) { perror("listnets: getifaddrs"); return 1; } for (struct ifaddrs *p = nets; p; p = p->ifa_next) { if (!p->ifa_addr) { continue; } int family = p->ifa_addr->sa_family; if (family != AF_INET && family != AF_INET6) { continue; } char host[1025]; int r = getnameinfo( p->ifa_addr, family == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6), host, sizeof(host), NULL, 0, NI_NUMERICHOST); if (r) { if (r == EAI_SYSTEM) { perror("listnets: getnameinfo"); } else { fprintf(stderr, "listnets: getnameinfo: %s\n", gai_strerror(r)); } continue; } printf("%s %s %s\n", p->ifa_name, family == AF_INET ? "ipv4" : "ipv6", host); } freeifaddrs(nets); return 0; } ================================================ FILE: tests/minstd.h ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #ifndef minstd_h_ #define minstd_h_ #include #include #include #include "libls/ls_compdep.h" #include "libls/ls_panic.h" // MINSTD pseudo-random number generator, year 1990 version. LS_INHEADER uint32_t minstd_make_up_some_seed(void) { struct timespec ts; if (clock_gettime(CLOCK_REALTIME, &ts) < 0) { return getpid(); } uint32_t nsec = ts.tv_nsec; uint32_t sec = ts.tv_sec; return nsec + sec * (1000 * 1000 * 1000); } typedef struct { uint32_t x; } MINSTD_Prng; enum { MINSTD_M = 0x7fffffff, MINSTD_A = 48271, }; LS_INHEADER MINSTD_Prng minstd_prng_new(uint32_t seed) { uint32_t res = seed % MINSTD_M; if (!res) { res = 1 + (seed % (MINSTD_M - 1)); } return (MINSTD_Prng) {res}; } LS_INHEADER uint32_t minstd_prng_next_raw(MINSTD_Prng *P) { uint64_t prod = ((uint64_t) P->x) * MINSTD_A; P->x = prod % MINSTD_M; return P->x; } LS_INHEADER uint64_t minstd_prng_next_u64(MINSTD_Prng *P) { uint64_t res = 0; for (int i = 0; i < 5; ++i) { res *= MINSTD_M; res += minstd_prng_next_raw(P); } return res; } // Returns random integer x such that 0 <= x < limit. LS_INHEADER uint32_t minstd_prng_next_limit_u32(MINSTD_Prng *P, uint32_t limit) { LS_ASSERT(limit != 0); return minstd_prng_next_u64(P) % limit; } #endif ================================================ FILE: tests/mock_barlib.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include "include/barlib_v1.h" #include "include/sayf_macros.h" #include "libls/ls_alloc_utils.h" #include "libls/ls_cstring_utils.h" #include "libls/ls_parse_int.h" #include "minstd.h" typedef struct { size_t nwidgets; unsigned char *widgets; int nevents; int64_t prng_seed; } Priv; static void destroy(LuastatusBarlibData *bd) { Priv *p = bd->priv; free(p->widgets); free(p); } static int init(LuastatusBarlibData *bd, const char *const *opts, size_t nwidgets) { Priv *p = bd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .nwidgets = nwidgets, .widgets = LS_XNEW0(unsigned char, nwidgets), .nevents = 0, .prng_seed = -1, }; if (nwidgets > (size_t) INT32_MAX) { LS_FATALF(bd, "too many widgets"); goto error; } for (const char *const *s = opts; *s; ++s) { const char *v; if ((v = ls_strfollow(*s, "gen_events="))) { if ((p->nevents = ls_full_strtou(v)) < 0) { LS_FATALF(bd, "gen_events value is not a proper integer"); goto error; } } else if ((v = ls_strfollow(*s, "prng_seed="))) { if ((p->prng_seed = ls_full_strtou(v)) < 0) { LS_FATALF(bd, "prng_seed value is not a proper integer"); goto error; } } else { LS_FATALF(bd, "unknown option: '%s'", *s); goto error; } } return LUASTATUS_OK; error: destroy(bd); return LUASTATUS_ERR; } static int set(LuastatusBarlibData *bd, lua_State *L, size_t widget_idx) { Priv *p = bd->priv; if (!lua_isnil(L, -1)) { LS_ERRF(bd, "got non-nil data"); return LUASTATUS_NONFATAL_ERR; } ++p->widgets[widget_idx]; return LUASTATUS_OK; } static int set_error(LuastatusBarlibData *bd, size_t widget_idx) { Priv *p = bd->priv; --p->widgets[widget_idx]; return LUASTATUS_OK; } static int event_watcher(LuastatusBarlibData *bd, LuastatusBarlibEWFuncs funcs) { Priv *p = bd->priv; if (p->nevents && !p->nwidgets) { LS_FATALF(bd, "no widgets to generate events on"); return LUASTATUS_ERR; } uint32_t seed; if (p->prng_seed >= 0) { seed = (uint32_t) p->prng_seed; } else { static const uint32_t SALT = 41744457; seed = minstd_make_up_some_seed() ^ SALT; LS_INFOF(bd, "Seed: %" PRIu32, seed); } MINSTD_Prng my_prng = minstd_prng_new(seed); for (int i = 0; i < p->nevents; ++i) { size_t widget_idx = minstd_prng_next_limit_u32(&my_prng, p->nwidgets); lua_State *L = funcs.call_begin(bd->userdata, widget_idx); lua_pushnil(L); funcs.call_end(bd->userdata, widget_idx); } return LUASTATUS_NONFATAL_ERR; } LuastatusBarlibIface luastatus_barlib_iface_v1 = { .init = init, .set = set, .set_error = set_error, .event_watcher = event_watcher, .destroy = destroy, }; ================================================ FILE: tests/mock_plugin.c ================================================ /* * Copyright (C) 2015-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include "include/plugin_v1.h" #include "include/sayf_macros.h" #include "libmoonvisit/moonvisit.h" #include "libls/ls_alloc_utils.h" typedef struct { uint64_t ncalls; } Priv; static void destroy(LuastatusPluginData *pd) { Priv *p = pd->priv; free(p); } static int init(LuastatusPluginData *pd, lua_State *L) { Priv *p = pd->priv = LS_XNEW(Priv, 1); *p = (Priv) { .ncalls = 0, }; char errbuf[256]; MoonVisit mv = {.L = L, .errbuf = errbuf, .nerrbuf = sizeof(errbuf)}; // Parse make_calls if (moon_visit_uint(&mv, -1, "make_calls", &p->ncalls, true) < 0) { goto mverror; } return LUASTATUS_OK; mverror: LS_FATALF(pd, "%s", errbuf); //error: destroy(pd); return LUASTATUS_ERR; } static void run(LuastatusPluginData *pd, LuastatusPluginRunFuncs funcs) { Priv *p = pd->priv; for (uint64_t i = 0; i < p->ncalls; ++i) { lua_State *L = funcs.call_begin(pd->userdata); lua_pushnil(L); funcs.call_end(pd->userdata); } } LuastatusPluginIface luastatus_plugin_iface_v1 = { .init = init, .run = run, .destroy = destroy, }; ================================================ FILE: tests/optional/dbus_srv.py ================================================ #!/usr/bin/env python3 # Copyright (C) 2026 luastatus developers # # This file is part of luastatus. # # luastatus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # luastatus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with luastatus. If not, see . # ================================================================================ # This is a simple D-Bus server. It is needed for testing plugin functions of the # "dbus" plugin. # ================================================================================ import sys import gi.repository.GLib import dbus import dbus.service import dbus.mainloop.glib MY_INTERFACE = 'io.github.shdown.luastatus.test' MY_BUS_NAME = 'io.github.shdown.luastatus.test' MY_OBJECT_PATH = '/io/github/shdown/luastatus/test/MyObject' PROP_INTERFACE = 'org.freedesktop.DBus.Properties' def log(s): print(f'dbus_srv.py: {s}', file=sys.stderr) class MyServerObject(dbus.service.Object): def __init__(self, bus, object_path): dbus.service.Object.__init__(self, bus, object_path, MY_BUS_NAME) self.prop = '' @dbus.service.method(MY_INTERFACE, in_signature='s', out_signature='s') def Upcase(self, msg): msg = str(msg) log(f'Called Upcase on "{msg}"') return str(msg).upper() @dbus.service.method(MY_INTERFACE, in_signature='', out_signature='i') def ReturnFortyTwo(self): log('Called ReturnFortyTwo') return 42 @dbus.service.method(MY_INTERFACE, in_signature='as', out_signature='a{ss}') def ConvertArrayToDictHexify(self, arr): log('Called ConvertArrayToDictHexify') def _hexify(s): return s.encode('utf8').hex().upper() return { _hexify(str(i)): _hexify(s) for i, s in enumerate(arr) } @dbus.service.method(MY_INTERFACE, in_signature='a{ss}', out_signature='s') def ConvertDictToString(self, d): log('Called ConvertDictToString') return ','.join(f'{key}:{value}' for key, value in sorted(d.items())) @dbus.service.method(MY_INTERFACE, in_signature='', out_signature='s') def RecvTuple0(self): log('Called RecvTuple0') return 'Empty' @dbus.service.method(MY_INTERFACE, in_signature='s', out_signature='s') def RecvTuple1(self, arg): log('Called RecvTuple1') return arg @dbus.service.method(MY_INTERFACE, in_signature='ss', out_signature='s') def RecvTuple2(self, arg1, arg2): log('Called RecvTuple2') return arg1 + arg2 @dbus.service.method(MY_INTERFACE, in_signature='sss', out_signature='s') def RecvTuple3(self, arg1, arg2, arg3): log('Called RecvTuple3') return arg1 + arg2 + arg3 @dbus.service.method(MY_INTERFACE, in_signature='v', out_signature='s') def RecvVariant(self, x): log(f'Called RecvVariant, type(x)={type(x)}') TABLE = [ (dbus.Boolean, 'bool'), (dbus.Double, 'double'), (dbus.Byte, 'byte'), (dbus.String, 'string'), (dbus.ObjectPath, 'object_path'), (dbus.Signature, 'signature'), (dbus.Int16, 'i16'), (dbus.Int32, 'i32'), (dbus.Int64, 'i64'), (dbus.UInt16, 'u16'), (dbus.UInt32, 'u32'), (dbus.UInt64, 'u64'), ] if hasattr(x, 'variant_level'): if x.variant_level > 0: return 'variant' for dbus_type, retval in TABLE: if isinstance(x, dbus_type): return retval raise ValueError('Got Variant wrapping an unsupported type') @dbus.service.method(PROP_INTERFACE, in_signature='ss', out_signature='s') def Get(self, _, prop_name): prop_name = str(prop_name) log(f'Called Get (property): prop_name="{prop_name}"') if prop_name != 'MyProperty': raise ValueError('wrong property name') return self.prop @dbus.service.method(PROP_INTERFACE, in_signature='s', out_signature='a{sv}') def GetAll(self, _): log('Called GetAll (properties)') return { 'MyProperty': self.prop, } @dbus.service.method(PROP_INTERFACE, in_signature='sss', out_signature='') def Set(self, _, prop_name, value): prop_name = str(prop_name) value = str(value) log(f'Called Set (property): prop_name="{prop_name}", value="{value}"') if prop_name != 'MyProperty': raise ValueError('wrong property name') self.prop = value def main(): args = sys.argv[1:] if len(args) != 1: print(f'USAGE: {sys.argv[0]} FIFO_FILE', file=sys.stderr) sys.exit(2) log('Opening FIFO...') with open(args[0], 'w') as fifo_file: log('OK, opened') dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) bus = dbus.SessionBus() bus_name = dbus.service.BusName(MY_BUS_NAME, bus, do_not_queue=True) log(f'Acquired bus name "{bus_name.get_name()}"') my_object = MyServerObject(bus, MY_OBJECT_PATH) mainloop = gi.repository.GLib.MainLoop() log('Starting up') print('running', file=fifo_file, flush=True) mainloop.run() if __name__ == '__main__': main() ================================================ FILE: tests/parrot.c ================================================ /* * Copyright (C) 2021-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include static struct { bool half_duplex_ok; bool reuseaddr; bool print_line_when_ready; bool print_line_on_accept; bool just_check; } global_flags; static void fail(void) { _exit(1); } static int full_write(int fd, const char *buf, size_t nbuf) { for (size_t nwritten = 0; nwritten < nbuf;) { ssize_t w = write(fd, buf + nwritten, nbuf - nwritten); if (w < 0) { perror("parrot: write"); return -1; } nwritten += w; } return 0; } typedef struct { int read_fd; int write_fd; } ThreadArg; static void *thread_func(void *varg) { ThreadArg *arg = varg; int read_fd = arg->read_fd; int write_fd = arg->write_fd; char buf[1024]; for (;;) { ssize_t r = read(read_fd, buf, sizeof(buf)); if (r < 0) { perror("parrot: read"); fail(); } if (r == 0) { break; } if (full_write(write_fd, buf, r) < 0) { fail(); } } if (!global_flags.half_duplex_ok) { _exit(0); } return NULL; } static void parrot(int other_fd) { ThreadArg args[2] = { {.read_fd = other_fd, .write_fd = 1}, {.read_fd = 0, .write_fd = other_fd}, }; pthread_t threads[2]; for (int i = 0; i < 2; ++i) { if ((errno = pthread_create(&threads[i], NULL, thread_func, &args[i]))) { perror("parrot: pthead_create"); fail(); } } for (int i = 0; i < 2; ++i) { if ((errno = pthread_join(threads[i], NULL))) { perror("parrot: pthread_join"); fail(); } } } typedef struct { bool *out; const char *name; } FlagOption; static FlagOption flag_options[] = { {&global_flags.half_duplex_ok, "--half-duplex-ok"}, {&global_flags.reuseaddr, "--reuseaddr"}, {&global_flags.print_line_when_ready, "--print-line-when-ready"}, {&global_flags.print_line_on_accept, "--print-line-on-accept"}, {&global_flags.just_check, "--just-check"}, {0}, }; static void print_usage(void) { fprintf(stderr, "USAGE: parrot [options...] {TCP-CLIENT port | TCP-SERVER port | UNIX-CLIENT path | UNIX-SERVER path}\n"); fprintf(stderr, "Supported options:\n"); fprintf(stderr, " --half-duplex-ok\n"); fprintf(stderr, " Continue even after one side has hung up.\n"); fprintf(stderr, " --reuseaddr\n"); fprintf(stderr, " Set SO_REUSEADDR option on the TCP server socket.\n"); fprintf(stderr, " --print-line-when-ready\n"); fprintf(stderr, " Write 'parrot: ready' line when the server is ready to accept a connection.\n"); fprintf(stderr, " --print-line-on-accept\n"); fprintf(stderr, " Write 'parrot: accepted' line when the server has accepted a connection.\n"); fprintf(stderr, " --just-check\n"); fprintf(stderr, " Exit with code 0 after creating the server.\n"); } static int client(int (*func)(const char *arg), const char *arg) { int fd = func(arg); if (fd < 0) { fail(); } parrot(fd); close(fd); return 0; } static int server(int (*func)(const char *arg), const char *arg) { int server_fd = func(arg); if (server_fd < 0) { fail(); } if (global_flags.just_check) { close(server_fd); return 0; } if (global_flags.print_line_when_ready) { const char *line = "parrot: ready\n"; if (full_write(1, line, strlen(line)) < 0) { fail(); } } int client_fd = accept(server_fd, NULL, NULL); if (client_fd < 0) { perror("parrot: accept"); fail(); } close(server_fd); if (global_flags.print_line_on_accept) { const char *line = "parrot: accepted\n"; if (full_write(1, line, strlen(line)) < 0) { fail(); } } parrot(client_fd); close(client_fd); return 0; } static in_addr_t localhost(void) { in_addr_t res = inet_addr("127.0.0.1"); if (res == (in_addr_t) -1) { perror("parrot: inet_addr"); fail(); } return res; } static int make_unix_client(const char *path) { int fd = -1; struct sockaddr_un saun = {.sun_family = AF_UNIX}; size_t npath = strlen(path); if (npath + 1 > sizeof(saun.sun_path)) { fprintf(stderr, "parrot: socket path is too long.\n"); goto error; } memcpy(saun.sun_path, path, npath + 1); fd = socket(PF_UNIX, SOCK_STREAM, 0); if (fd < 0) { perror("parrot: socket"); goto error; } if (connect(fd, (struct sockaddr *) &saun, sizeof(saun)) < 0) { perror("parrot: connect"); goto error; } return fd; error: close(fd); return -1; } static bool parse_uint(const char *s, unsigned *out) { char *endptr; errno = 0; *out = strtoul(s, &endptr, 10); if (errno || endptr == s || endptr[0] != '\0') { return false; } return true; } static int make_tcp_client(const char *portstr) { int fd = -1; unsigned port; if (!parse_uint(portstr, &port)) { fprintf(stderr, "parrot: invalid port number: '%s'.\n", portstr); goto error; } fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { perror("parrot: socket"); goto error; } struct sockaddr_in sain = { .sin_family = AF_INET, .sin_addr = {.s_addr = localhost()}, .sin_port = htons(port), }; if (connect(fd, (struct sockaddr *) &sain, sizeof(sain)) < 0) { perror("parrot: connect"); goto error; } return fd; error: close(fd); return -1; } static int make_tcp_server(const char *portstr) { int fd = -1; unsigned port; if (!parse_uint(portstr, &port)) { fprintf(stderr, "parrot: invalid port number: '%s'.\n", portstr); goto error; } fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { perror("parrot: socket"); goto error; } if (global_flags.reuseaddr) { int value = 1; if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value)) < 0) { perror("parrot: setsockopt (SO_REUSEADDR)"); goto error; } } struct sockaddr_in sain = { .sin_family = AF_INET, .sin_addr = {.s_addr = localhost()}, .sin_port = htons(port), }; if (bind(fd, (struct sockaddr *) &sain, sizeof(sain)) < 0) { perror("parrot: bind"); goto error; } if (listen(fd, SOMAXCONN) < 0) { perror("parrot: listen"); goto error; } return fd; error: close(fd); return -1; } static int make_unix_server(const char *path) { int fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) { perror("parrot: socket"); goto error; } struct sockaddr_un saun = {.sun_family = AF_UNIX}; size_t npath = strlen(path); if (npath + 1 > sizeof(saun.sun_path)) { fprintf(stderr, "parrot: socket path is too long.\n"); goto error; } memcpy(saun.sun_path, path, npath + 1); if (bind(fd, (struct sockaddr *) &saun, sizeof(saun)) < 0) { perror("parrot: bind"); goto error; } if (listen(fd, SOMAXCONN) < 0) { perror("parrot: listen"); goto error; } return fd; error: close(fd); return fd; } int main(int argc, char **argv) { int argi = 1; for (; argi < argc; ++argi) { const char *arg = argv[argi]; if (arg[0] != '-') { break; } bool *out = NULL; for (FlagOption *o = flag_options; o->out; ++o) { if (strcmp(arg, o->name) == 0) { out = o->out; break; } } if (!out) { fprintf(stderr, "Unknown option: '%s'.\n", arg); print_usage(); return 2; } *out = true; } if (argc - argi != 2) { fprintf(stderr, "Expected exactly 2 positional arguments, found %d.\n", argc - argi); print_usage(); return 2; } const char *what = argv[argi]; const char *what_arg = argv[argi + 1]; if (strcmp(what, "TCP-CLIENT") == 0) { return client(make_tcp_client, what_arg); } else if (strcmp(what, "TCP-SERVER") == 0) { return server(make_tcp_server, what_arg); } else if (strcmp(what, "UNIX-CLIENT") == 0) { return client(make_unix_client, what_arg); } else if (strcmp(what, "UNIX-SERVER") == 0) { return server(make_unix_server, what_arg); } else { fprintf(stderr, "Unknown command: '%s'.\n", what); print_usage(); return 2; } } ================================================ FILE: tests/pt.sh ================================================ #!/usr/bin/env bash set -e PT_OPWD=$PWD cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" source ./utils.lib.bash fail_wrong_usage() { printf >&2 '%s\n' "$*" printf >&2 '%s\n' "USAGE: $0 [{run: | skip:}...]" exit 2 } if (( $# < 1 )); then fail_wrong_usage "No build root passed." fi PT_BUILD_DIR=$(resolve_relative "$1" "$PT_OPWD") # Shift the build root shift PT_SOURCE_DIR=.. case "$PT_TOOL" in '') PT_PREFIX=() ;; valgrind) PT_PREFIX=( valgrind -q --exit-on-first-error=yes --error-exitcode=42 ) ;; helgrind) PT_PREFIX=( valgrind --tool=helgrind -q --exit-on-first-error=yes --error-exitcode=42 ) PT_PREFIX+=( --suppressions="$PT_BUILD_DIR/tests/glib.supp" ) ;; kcov) PT_PREFIX=( ./kcov_wrapper kcov "${PT_KCOV_DIR?}" ) ;; *) echo >&2 "$0: Unknown PT_TOOL: $PT_TOOL." exit 2 ;; esac PT_LUASTATUS=( "$PT_BUILD_DIR"/luastatus/luastatus ${DEBUG:+-l trace} ) PT_PARROT=$PT_BUILD_DIR/tests/parrot PT_HTTPSERV=$PT_BUILD_DIR/tests/httpserv/httpserv PT_WIDGET_FILES=() PT_FILES_TO_REMOVE=() PT_DIRS_TO_REMOVE=() declare -A PT_SPAWNED_THINGS=() declare -A PT_SPAWNED_THINGS_FDS_0=() declare -A PT_SPAWNED_THINGS_FDS_1=() PT_LINE= source ./pt_stopwatch.lib.bash source ./pt_dbus_daemon.lib.bash source ./pt_pulseaudio_daemon.lib.bash # Sanity checks if ! [[ -x "$PT_BUILD_DIR"/luastatus/luastatus ]]; then echo >&2 "FATAL ERROR: '$PT_BUILD_DIR/luastatus/luastatus' is not an executable file." echo >&2 "Is '$PT_BUILD_DIR' the correct build root? Did you forget to build the project?" exit 1 fi if ! [[ -x "$PT_PARROT" ]]; then echo >&2 "FATAL ERROR: '$PT_PARROT' is not an executable file." echo >&2 "Did you forget to pass '-DBUILD_TESTS=ON' to cmake?" exit 1 fi pt_stack_trace() { echo >&2 "Stack trace:" local n=${#FUNCNAME[@]} local i=${1:-1} for (( ; i < n; ++i )); do local func=${FUNCNAME[$i]:-MAIN} local lineno=${BASH_LINENO[(( i - 1 ))]} local src=${BASH_SOURCE[$i]:-'???'} echo >&2 " in $src:$lineno (function $func)" done } pt_fail_internal_error() { printf >&2 '%s\n' '=== INTERNAL ERROR ===' "$@" pt_stack_trace 2 exit 3 } pt_fail() { printf >&2 '%s\n' '=== FAILED ===' "$@" pt_stack_trace 2 exit 1 } pt_check() { "$@" || pt_fail "pt_check($*) failed with code $?" } pt_write_widget_file() { local f f=$(mktemp --suffix=.lua) || pt_fail "mktemp failed" PT_WIDGET_FILES+=("$f") PT_FILES_TO_REMOVE+=("$f") cat > "$f" || pt_fail "cannot write widget file" } pt_add_file_to_remove() { PT_FILES_TO_REMOVE+=("$1") } pt_add_dir_to_remove() { PT_DIRS_TO_REMOVE+=("$1") } pt_add_dirs_to_remove_inorder() { PT_DIRS_TO_REMOVE+=("$@") } pt_add_fifo() { rm -f "$1" || true mkfifo "$1" || pt_fail "mkfifo failed" pt_add_file_to_remove "$1" } pt_find_free_tcp_port() { local port=12121 while true; do echo >&2 "[pt_find_free_tcp_port] Checking port $port..." if "$PT_PARROT" --reuseaddr --just-check TCP-SERVER "$port"; then break fi echo >&2 "[pt_find_free_tcp_port] Port $port does not seem to be free, incrementing." port=$(( port + 1 )) if (( port >= 65536 )); then pt_fail "[pt_find_free_tcp_port] Cannot find a free port." fi done echo >&2 "[pt_find_free_tcp_port] Chosen port $port." PT_FOUND_FREE_PORT=$port } pt_require_tools() { local tool for tool in "$@"; do if ! command -v "$tool" >/dev/null; then pt_fail "pt_require_tools: tool '$tool' was not found." fi done } pt_read_line() { echo >&2 "Reading line..." IFS= read -r PT_LINE || pt_fail "pt_read_line: cannot read next line (process died?)" } pt_expect_line() { echo >&2 "Expecting line “$1”..." IFS= read -r PT_LINE || pt_fail "expect_line: cannot read next line (process died?)" if [[ "$PT_LINE" != "$1" ]]; then pt_fail "pt_expect_line: line does not match" "Expected: '$1'" "Found: '$PT_LINE'" fi } pt_spawn_thing() { local k=$1 shift local pid=${PT_SPAWNED_THINGS[$k]} if [[ -n $pid ]]; then pt_fail_internal_error "pt_spawn_thing: thing '$k' has already been spawned (PID $pid)." fi "$@" & pid=$! PT_SPAWNED_THINGS[$k]=$pid } pt_spawn_thing_pipe() { local k=$1 shift local pid=${PT_SPAWNED_THINGS[$k]} if [[ -n $pid ]]; then pt_fail_internal_error "pt_spawn_thing_pipe: thing '$k' has already been spawned (PID $pid)." fi local fifo_in=./_internal-tmpfifo-$k-in local fifo_out=./_internal-tmpfifo-$k-out pt_add_fifo "$fifo_in" pt_add_fifo "$fifo_out" "$@" >"$fifo_out" <"$fifo_in" & pid=$! PT_SPAWNED_THINGS[$k]=$pid exec {PT_SPAWNED_THINGS_FDS_0[$k]}<"$fifo_out" exec {PT_SPAWNED_THINGS_FDS_1[$k]}>"$fifo_in" } pt_has_spawned_thing() { local k=$1 [[ -n "${PT_SPAWNED_THINGS[$k]}" ]] } pt_close_fd() { local fd=$(( $1 )) exec {fd}>&- } pt_close_thing_fds() { local k=$1 local fd for fd in "${PT_SPAWNED_THINGS_FDS_0[$k]}" "${PT_SPAWNED_THINGS_FDS_1[$k]}"; do if [[ -n $fd ]]; then pt_close_fd "$fd" fi done unset PT_SPAWNED_THINGS_FDS_0[$k] unset PT_SPAWNED_THINGS_FDS_1[$k] } pt_wait_thing() { local k=$1 local pid=${PT_SPAWNED_THINGS[$k]} if [[ -z $pid ]]; then pt_fail_internal_error "pt_wait_thing: unknown thing '$k' (PT_SPAWNED_THINGS has no such key)" fi echo >&2 "Waiting for '$k' (PID $pid)..." local c=0 wait "${PT_SPAWNED_THINGS[$k]}" || c=$? unset PT_SPAWNED_THINGS[$k] pt_close_thing_fds "$k" return -- "$c" } pt_kill_thing() { local k=$1 local pid=${PT_SPAWNED_THINGS[$k]} if [[ -n $pid ]]; then kill "$pid" || pt_fail "Cannot kill '$k' (PID $pid)." wait "$pid" || true fi unset PT_SPAWNED_THINGS[$k] pt_close_thing_fds "$k" } pt_spawn_luastatus() { pt_spawn_thing luastatus \ "${PT_PREFIX[@]}" "${PT_LUASTATUS[@]}" -b ./barlib-mock.so "$@" "${PT_WIDGET_FILES[@]}" } pt_spawn_luastatus_for_barlib_test_via_runner() { local runner=$1 shift pt_spawn_thing_pipe luastatus \ "$runner" "${PT_PREFIX[@]}" "${PT_LUASTATUS[@]}" "$@" "${PT_WIDGET_FILES[@]}" } pt_spawn_luastatus_directly() { pt_spawn_thing luastatus \ "${PT_PREFIX[@]}" "${PT_LUASTATUS[@]}" "$@" "${PT_WIDGET_FILES[@]}" } pt_wait_luastatus() { pt_wait_thing luastatus } pt_kill_luastatus() { pt_kill_thing luastatus } pt_kill_everything() { local k for k in "${!PT_SPAWNED_THINGS[@]}"; do pt_kill_thing "$k" done } pt_testcase_begin() { true } pt_testcase_end() { pt_kill_luastatus pt_kill_everything local x for x in "${PT_FILES_TO_REMOVE[@]}"; do rm -f "$x" || true done for x in "${PT_DIRS_TO_REMOVE[@]}"; do rmdir "$x" || true done PT_FILES_TO_REMOVE=() PT_DIRS_TO_REMOVE=() PT_WIDGET_FILES=() } trap ' for k in "${!PT_SPAWNED_THINGS[@]}"; do pid=${PT_SPAWNED_THINGS[$k]} echo >&2 "Killing “$k” (PID $pid)." kill "$pid" || true done ' EXIT pt_run_test_case() { echo >&2 "==> Invoking file '$1'..." source "$1" || pt_fail "'source' failed" } pt_run_test_suite() { if ! [[ -d $1 ]]; then pt_fail_internal_error "'$1' is not a directory." fi if [[ -f "$1/skip-me-if.lib.bash" ]]; then if source "$1/skip-me-if.lib.bash"; then echo >&2 "==> Skipping test suite '$1' (skip-me-if.lib.bash said so)." return fi fi echo >&2 "==> Listing files in '$1'..." local f for f in "$1"/*; do if [[ $f != *.lib.bash ]]; then pt_fail_internal_error "File '$f' does not have suffix '.lib.bash'." fi if [[ $f == */skip-me-if.lib.bash ]]; then continue fi if [[ ${f##*/} != [0-9][0-9]-* ]]; then pt_fail_internal_error "File '$f' does not have prefix of two digits and a dash (e.g. '99-testcase.lib.bash')." fi pt_run_test_case "$f" done } pt_cmake_opt_enabled() { local sed_cmd='s/^' sed_cmd+=$1 sed_cmd+=':BOOL=(.*)$/\1/p' local val val=$(cmake -LA "$PT_SOURCE_DIR" | sed -rn "$sed_cmd") \ || pt_fail "Cannot extract value of CMake option '$1'" case "$val" in 1|ON|YES|TRUE|Y) return 0 ;; 0|OFF|NO|FALSE|N|IGNORE|NOTFOUND|*-NOTFOUND) return 1 ;; esac pt_fail "Cannot make heads or tails of CMake boolean value '$val' (key $1)" } PT_SKIP_ME_YES=0 PT_SKIP_ME_NO=1 pt_setup_sanitizers() { if pt_cmake_opt_enabled WITH_TSAN; then export TSAN_OPTIONS=halt_on_error=1,atexit_sleep_ms=1 fi if pt_cmake_opt_enabled WITH_ASAN; then export ASAN_OPTIONS=halt_on_error=1,detect_leaks=1,detect_stack_use_after_return=1,strict_string_checks=1,detect_invalid_pointer_pairs=3 fi if pt_cmake_opt_enabled WITH_LSAN; then export LSAN_OPTIONS=halt_on_error=true fi if pt_cmake_opt_enabled WITH_UBSAN; then export UBSAN_OPTIONS=halt_on_error=1 fi } pt_main() { local args_run=() local -A args_skip=() local arg for arg in "$@"; do if [[ $arg == */* ]]; then fail_wrong_usage "Invalid argument: '$arg': suite name can not contain a slash." fi if [[ $arg != *:* ]]; then fail_wrong_usage "Invalid argument: '$arg': is not of key:value form." fi local k=${arg%%:*} local v=${arg#*:} if [[ -z "$v" ]]; then fail_wrong_usage "Invalid argument: '$arg': value is empty." fi case "$k" in run) args_run+=("$v") ;; skip) args_skip[$v]=1 ;; *) fail_wrong_usage "Invalid argument: '$arg': unknown key '$k'." ;; esac done if (( ${#args_run[@]} != 0 )); then local x for x in "${args_run[@]}"; do local d=./pt_tests/$x if [[ -n "${args_skip[$x]}" ]]; then echo >&2 "==> Skipping test suite '$d'." continue fi pt_run_test_suite "$d" done else local d for d in ./pt_tests/*; do if ! [[ -d $d ]]; then continue fi local x=${d##*/} if [[ -n "${args_skip[$x]}" ]]; then echo >&2 "==> Skipping test suite '$d'." continue fi pt_run_test_suite "$d" done fi } pt_setup_sanitizers pt_main "$@" echo >&2 "=== PASSED ===" ================================================ FILE: tests/pt_dbus_daemon.lib.bash ================================================ PT_DBUS_DAEMON_FIFO=./_tmpfifo_dbus_daemon_fd3 PT_DBUS_DAEMON_ALREADY_RUNNING=-1 pt_dbus_daemon__wrapper() { echo >&2 "Spawning dbus-daemon as PID $$." exec dbus-daemon "$@" --print-address=3 3>"$PT_DBUS_DAEMON_FIFO" } pt_dbus_daemon__wait_until_works() { local bus_arg=$1 local other_args=( /org/luastatus/just/testing/whether/dbus/works org.luastatus.ExampleInterface.ExampleMethod int32:42 objpath:/org/luastatus/sample/object/name ) pt_require_tools dbus-send local i for (( i = 0; i < 10; ++i )); do if dbus-send "$bus_arg" "${other_args[@]}"; then break fi sleep 1 done pt_fail "dbus ($bus_arg) does not work (waited for 10 seconds)" } pt_dbus_daemon_spawn() { local bus_arg=$1 if [[ "$bus_arg" != --system && "$bus_arg" != --session ]]; then pt_fail_internal_error "pt_dbus_daemon_launch: expected either '--system' or '--session' as argument, found '$bus_arg'." fi if (( PT_DBUS_DAEMON_ALREADY_RUNNING < 0 )); then if [[ -n "$DBUS_SESSION_BUS_ADDRESS" ]]; then echo >&2 "dbus-daemon seems to be already running: DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS" PT_DBUS_DAEMON_ALREADY_RUNNING=1 else PT_DBUS_DAEMON_ALREADY_RUNNING=0 fi fi if (( PT_DBUS_DAEMON_ALREADY_RUNNING )); then return 0 fi pt_require_tools dbus-daemon pt_add_fifo "$PT_DBUS_DAEMON_FIFO" echo >&2 "Spawning dbus-daemon ${bus_arg}." pt_spawn_thing __dbus_daemon__ pt_dbus_daemon__wrapper --nofork "$bus_arg" local response local rc=0 IFS= read -t 10 -r response < "$PT_DBUS_DAEMON_FIFO" || rc=$? if (( rc != 0 )); then pt_fail "Cannot read dbus-daemon response within 10 seconds: read exit code $rc." fi export DBUS_SESSION_BUS_ADDRESS=$response echo >&2 "Read DBUS_SESSION_BUS_ADDRESS: $DBUS_SESSION_BUS_ADDRESS" pt_dbus_daemon__wait_until_works "$bus_arg" } pt_dbus_daemon_kill() { if (( PT_DBUS_DAEMON_ALREADY_RUNNING )); then return 0 fi echo >&2 "Killing dbus-daemon." pt_kill_thing __dbus_daemon__ export DBUS_SESSION_BUS_ADDRESS= } ================================================ FILE: tests/pt_pulseaudio_daemon.lib.bash ================================================ PT_PULSEAUDIO_DAEMON_ALREADY_RUNNING=-1 pt_pulseaudio_daemon__check() { local n=$1 pt_require_tools pactl echo >&2 "Checking if PulseAudio daemon is available ($n time(s))..." local i for (( i = 1; i <= n; ++i )); do if pactl info; then echo >&2 "OK, PulseAudio daemon is available." return 0 fi if (( i != n )); then sleep 1 || return $? fi done echo >&2 "PulseAudio daemon seems to be unavailable." return 1 } pt_pulseaudio_daemon_spawn() { if (( PT_PULSEAUDIO_DAEMON_ALREADY_RUNNING < 0 )); then if pt_pulseaudio_daemon__check 1; then echo >&2 "PulseAudio daemon seems to be already running." PT_PULSEAUDIO_DAEMON_ALREADY_RUNNING=1 else PT_PULSEAUDIO_DAEMON_ALREADY_RUNNING=0 fi fi if (( PT_PULSEAUDIO_DAEMON_ALREADY_RUNNING )); then return 0 fi pt_require_tools pulseaudio echo >&2 "Spawning PulseAudio daemon." pt_spawn_thing __pulseaudio_daemon__ pulseaudio --daemonize=no --disallow-exit=yes --exit-idle-time=-1 if ! pt_pulseaudio_daemon__check 10; then pt_fail "PulseAudio daemon is not available after 10 seconds." fi } pt_pulseaudio_daemon_kill() { if (( PT_PULSEAUDIO_DAEMON_ALREADY_RUNNING )); then return 0 fi echo >&2 "Killing PulseAudio daemon." pt_kill_thing __pulseaudio_daemon__ } ================================================ FILE: tests/pt_stopwatch.lib.bash ================================================ using_measure() { pt_spawn_thing_pipe stopwatch "$PT_BUILD_DIR"/tests/stopwatch "${PT_MAX_LAG:-75}" } measure_start() { if ! pt_has_spawned_thing stopwatch; then pt_fail_internal_error "measure_start: stopwatch was not spawned (did you forget 'using_measure'?)." fi echo s >&${PT_SPAWNED_THINGS_FDS_1[stopwatch]} } measure_get_ms() { echo q >&${PT_SPAWNED_THINGS_FDS_1[stopwatch]} local ans IFS= read -r ans <&${PT_SPAWNED_THINGS_FDS_0[stopwatch]} \ || pt_fail "Cannot read next line from stopwatch (the process died?)." printf '%s\n' "$ans" } measure_check_ms() { echo "c $1" >&${PT_SPAWNED_THINGS_FDS_1[stopwatch]} local ans IFS= read -r ans <&${PT_SPAWNED_THINGS_FDS_0[stopwatch]} \ || pt_fail "Cannot read next line from stopwatch (the process died?)." if [[ $ans != 1* ]]; then pt_fail "measure_check_ms $1: stopwatch said: '$ans'." fi } ================================================ FILE: tests/pt_tests/_misc/01-simul.lib.bash ================================================ # Regression test for https://github.com/shdown/luastatus/issues/63. pt_testcase_begin for (( i = 0; i < 8; ++i )); do pt_write_widget_file <<__EOF__ widget = { plugin = '$PT_BUILD_DIR/plugins/mpd/plugin-mpd.so', opts = { port = 0, retry_in = -1, }, cb = function(t) end, } __EOF__ done pt_spawn_luastatus -e pt_wait_luastatus || pt_fail "luastatus exited with non-zero code $?" pt_testcase_end ================================================ FILE: tests/pt_tests/add_test.sh ================================================ #!/usr/bin/env bash set -e set -o pipefail shopt -s nullglob dest_dir=${1?} new_number=${2?} new_name=${3?} cd -- "$dest_dir" prev_testcases_raw=$(printf '%s\n' [0-9][0-9]-*.lib.bash | LC_ALL=C sort -r) prev_testcases=( $prev_testcases_raw ) echo >&2 "Current number of testcases: ${#prev_testcases[@]}" if (( ${#prev_testcases[@]} >= 100 )); then echo >&2 "ERROR: too many testcases." exit 1 fi NN_to_normal() { echo "${1#0}" } normal_to_NN() { printf '%02d\n' "$1" } if [[ $new_number == ?? ]]; then j_new=$(NN_to_normal "$new_number") else j_new=$new_number fi for f in "${prev_testcases[@]}"; do NN=${f%%-*} j=$(NN_to_normal "$NN") if (( j >= j_new )); then f_suffix=${f#??-} NN_next=$(normal_to_NN "$(( j + 1 ))") mv -v -- "$f" "${NN_next}-${f_suffix}" fi done new_NN=$(normal_to_NN "$j_new") bad=$(printf '%s\n' "$new_NN"-*.lib.bash) if [[ -n "$bad" ]]; then echo >&2 "ERROR: there are still files with given number after the 'moving' step:" echo >&2 "~~~" printf >&2 '%s\n' "$bad" echo >&2 "~~~" exit 1 fi touch -- "${new_NN}-${new_name}.lib.bash" echo OK ================================================ FILE: tests/pt_tests/barlib-i3/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main x_spawn_luastatus() { pt_spawn_luastatus_for_barlib_test_via_runner \ "$PT_SOURCE_DIR"/tests/barlib-runners/runner-redirect-34 \ -b "$PT_BUILD_DIR"/barlibs/i3/barlib-i3.so -B in_fd=3 -B out_fd=4 \ "$@" } ================================================ FILE: tests/pt_tests/barlib-i3/01-output.lib.bash ================================================ pt_require_tools jq x_jq_read_file_option= x_figure_out_jq_version() { x_jq_read_file_option=argfile local out if ! out=$(jq --slurpfile foo /dev/null -n '123'); then return 0 fi if [[ "$out" != '123' ]]; then return 0 fi x_jq_read_file_option=slurpfile } x_figure_out_jq_version echo >&2 "Figured out jq option for reading a file into a variable: --$x_jq_read_file_option" x_assert_json_eq() { echo >&2 "Comparing JSONs: Expected: $1 Actual: $2" local out if ! out=$(jq --$x_jq_read_file_option a <(printf '%s\n' "$1") --$x_jq_read_file_option b <(printf '%s\n' "$2") -n '$a == $b'); then pt_fail "jq failed" fi case "$out" in true) ;; false) pt_fail 'x_assert_json_eq: JSON does not match.' "Expected: $1" "Found: $2" ;; *) pt_fail "Unexpected output of jq: '$out'." ;; esac } x_testcase_output() { local lua_expr=$1 local expect_json=$2 pt_testcase_begin pt_write_widget_file <<__EOF__ local _my_flag = false widget = { plugin = '$PT_BUILD_DIR/tests/plugin-mock.so', opts = {make_calls = 2}, cb = function() if _my_flag then error('Boom') end _my_flag = true return ($lua_expr) end, } __EOF__ local opts=() if (( O_NO_SEPARATORS )); then opts+=(-B no_separators) fi if (( O_NO_CLICK_EVENTS )); then opts+=(-B no_click_events) fi if (( O_ALLOW_STOPPING )); then opts+=(-B allow_stopping) fi local first_line='{"version":1' if (( O_NO_CLICK_EVENTS )); then first_line+=',"click_events":false' else first_line+=',"click_events":true' fi if (( ! O_ALLOW_STOPPING )); then first_line+=',"stop_signal":0,"cont_signal":0' fi first_line+='}' local last_line='[{"full_text":"(Error)","color":"#ff0000","background":"#000000"' if (( O_NO_SEPARATORS )); then last_line+=',"separator":false' fi last_line+='}],' x_spawn_luastatus "${opts[@]}" pt_expect_line "$first_line" <&${PT_SPAWNED_THINGS_FDS_0[luastatus]} pt_expect_line '[' <&${PT_SPAWNED_THINGS_FDS_0[luastatus]} if [[ -n "$expect_json" ]]; then pt_read_line <&${PT_SPAWNED_THINGS_FDS_0[luastatus]} if [[ $PT_LINE != *, ]]; then pt_fail "Line does not end with ','." "Line: '$PT_LINE'." fi x_assert_json_eq "$expect_json" "${PT_LINE%,}" fi pt_expect_line "$last_line" <&${PT_SPAWNED_THINGS_FDS_0[luastatus]} pt_testcase_end } O_NO_SEPARATORS=0 O_NO_CLICK_EVENTS=0 O_ALLOW_STOPPING=0 x_testcase_output 'nil' '' x_testcase_output '{}' '' x_testcase_output '{nil}' '' x_testcase_output '{{}}' '[{"name":"0"}]' x_testcase_output '{{},nil,{}}' '[{"name":"0"},{"name":"0"}]' x_testcase_output '{full_text = "Hello, world"}' '[{"name":"0","full_text":"Hello, world"}]' x_testcase_output '{{full_text = "Hello, world"}}' '[{"name":"0","full_text":"Hello, world"}]' x_testcase_output '{{fpval = 1234.5}}' '[{"name":"0","fpval":1234.5}]' x_testcase_output '{{intval = -12345}}' '[{"name":"0","intval":-12345}]' x_testcase_output '{{name = "abc"}}' '[{"name":"0"}]' x_testcase_output '{{name = 123}}' '[{"name":"0"}]' x_testcase_output '{{separator = true}}' '[{"name":"0","separator":true}]' x_testcase_output '{{separator = false}}' '[{"name":"0","separator":false}]' x_testcase_output '{{foo = true, bar = false}}' '[{"name":"0","foo":true,"bar":false}]' O_NO_SEPARATORS=1 x_testcase_output '{{}}' '[{"name":"0","separator":false}]' O_NO_SEPARATORS=1 x_testcase_output '{{separator = true}}' '[{"name":"0","separator":true}]' O_NO_SEPARATORS=1 x_testcase_output '{{separator = false}}' '[{"name":"0","separator":false}]' O_NO_CLICK_EVENTS=1 x_testcase_output 'nil' '' O_ALLOW_STOPPING=1 x_testcase_output 'nil' '' ================================================ FILE: tests/pt_tests/barlib-i3/02-input.lib.bash ================================================ x_testcase_input() { pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') local function _fmt_x(x) local function _validate_str(s) assert(s:find("\n") == nil, "string contains a line break") assert(s:find("\"") == nil, "string contains a double quote sign") end local function _fmt_num(n) if n == math.floor(n) then return string.format("%d", n) else return string.format("%.4f", n) end end local t = type(x) if t == "table" then local tk = type(next(x)) if tk == "nil" then return "{}" elseif tk == "number" then local s = {} for _, v in ipairs(x) do s[#s + 1] = _fmt_x(v) end return string.format("{%s}", table.concat(s, ",")) elseif tk == "string" then local ks = {} for k, _ in pairs(x) do assert(type(k) == "string", "table has mixed-type keys") _validate_str(k) ks[#ks + 1] = k end table.sort(ks) local s = {} for _, k in ipairs(ks) do local v = x[k] s[#s + 1] = string.format("[\"%s\"]=%s", k, _fmt_x(v)) end return string.format("{%s}", table.concat(s, ",")) else error("table key has unexpected type: " .. tk) end elseif t == "string" then _validate_str(x) return string.format("\"%s\"", x) elseif t == "number" then return _fmt_num(x) else return tostring(x) end end widget = { plugin = '$PT_BUILD_DIR/tests/plugin-mock.so', opts = {make_calls = 0}, cb = function() end, event = function(t) f:write('event ' .. _fmt_x(t) .. '\n') end, } __EOF__ x_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line '{"version":1,"click_events":true,"stop_signal":0,"cont_signal":0}' <&${PT_SPAWNED_THINGS_FDS_0[luastatus]} pt_expect_line '[' <&${PT_SPAWNED_THINGS_FDS_0[luastatus]} printf '[\n%s\n' "$1" >&${PT_SPAWNED_THINGS_FDS_1[luastatus]} pt_expect_line "event $2" <&$pfd pt_close_fd "$pfd" pt_testcase_end } x_testcase_input '{"name":"0","foo":"bar"}' '{["foo"]="bar",["name"]="0"}' x_testcase_input '{"name":"1","what":"invalid widget index"}, {"name":"1234567","what":"ivalid widget index"}, {"name":"-0","what":"widget index is minus zero"}, {"name":"-1","what":"negative widget index"}, {"name":"0","finally ok":"yes"}' '{["finally ok"]="yes",["name"]="0"}' x_testcase_input '{"name":"0","foo":[1,2,3,{"k":"v"},4]}' '{["foo"]={1,2,3,{["k"]="v"},4},["name"]="0"}' x_testcase_input '{"name":"0","foo":null,"bar":true,"baz":false}' '{["bar"]=true,["baz"]=false,["name"]="0"}' ================================================ FILE: tests/pt_tests/barlib-i3/03-lfunc-pango-escape.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') local function check(arg, expected) local result = luastatus.barlib.pango_escape(arg) if result ~= expected then print('~~~~~~~~~~~~~') print('Argument: ' .. arg) print('Expected: ' .. expected) print('Found: ' .. result) print('~~~~~~~~~~~~~') error('check failed') end end widget = { plugin = '$PT_BUILD_DIR/tests/plugin-mock.so', opts = {make_calls = 1}, cb = function() check('', '') check('hello', 'hello') check('hello & world', 'hello & world') check('<>', '<>') f:write('ok\n') end, } __EOF__ x_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/barlib-lemonbar/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main x_spawn_luastatus() { pt_spawn_luastatus_for_barlib_test_via_runner \ "$PT_SOURCE_DIR"/tests/barlib-runners/runner-redirect-34 \ -b "$PT_BUILD_DIR"/barlibs/lemonbar/barlib-lemonbar.so -B in_fd=3 -B out_fd=4 \ "$@" } ================================================ FILE: tests/pt_tests/barlib-lemonbar/01-output.lib.bash ================================================ x_testcase_output() { local lua_expr=$1 local expect_line=$2 local opts=() if [[ -n $O_SEPARATOR ]]; then opts+=(-B separator="$O_SEPARATOR") fi pt_testcase_begin pt_write_widget_file <<__EOF__ local _my_flag = false widget = { plugin = '$PT_BUILD_DIR/tests/plugin-mock.so', opts = {make_calls = 2}, cb = function() if _my_flag then error('Boom') end _my_flag = true return ($lua_expr) end, } __EOF__ x_spawn_luastatus "${opts[@]}" if [[ -n "$expect_line" ]]; then pt_expect_line "$expect_line" <&${PT_SPAWNED_THINGS_FDS_0[luastatus]} fi pt_expect_line '%{B#f00}%{F#fff}(Error)%{B-}%{F-}' <&${PT_SPAWNED_THINGS_FDS_0[luastatus]} pt_testcase_end } O_SEPARATOR= x_testcase_output 'nil' '' x_testcase_output '""' '' x_testcase_output '"foo bar"' 'foo bar' x_testcase_output '"?%%"' '?%%' x_testcase_output '"abc%{A:mycommand:}def"' 'abc%{A:0_mycommand:}def' x_testcase_output '"abc%%{A:mycommand:}def"' 'abc%%{A:mycommand:}def' x_testcase_output '{"foo", "bar", "baz", "quiz"}' 'foo | bar | baz | quiz' O_SEPARATOR='tum' x_testcase_output '{"foo", "bar", "baz", "quiz"}' 'footumbartumbaztumquiz' x_testcase_output '{}' '' x_testcase_output '{nil}' '' x_testcase_output '{"", "", nil, ""}' '' ================================================ FILE: tests/pt_tests/barlib-lemonbar/02-input.lib.bash ================================================ x_testcase_input() { pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') widget = { plugin = '$PT_BUILD_DIR/tests/plugin-mock.so', opts = {make_calls = 0}, cb = function() end, event = function(t) f:write('event ' .. t .. '\n') end, } __EOF__ x_spawn_luastatus exec {pfd}<"$main_fifo_file" printf '%s\n' "$1" >&${PT_SPAWNED_THINGS_FDS_1[luastatus]} pt_expect_line "event $2" <&$pfd pt_close_fd "$pfd" pt_testcase_end } x_testcase_input '0_foo bar' 'foo bar' x_testcase_input '0wtf_is_this 1_no_such_widget 1234567_no_such_widget 0__some_thing' '_some_thing' x_testcase_input '-0_what 0_okay' 'okay' x_testcase_input '0_' '' x_testcase_input ' 0_previous line is empty' 'previous line is empty' ================================================ FILE: tests/pt_tests/barlib-lemonbar/03-lfunc-pango-escape.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') local function check(arg, expected) local result = luastatus.barlib.escape(arg) if result ~= expected then print('~~~~~~~~~~~~~') print('Argument: ' .. arg) print('Expected: ' .. expected) print('Found: ' .. result) print('~~~~~~~~~~~~~') error('check failed') end end widget = { plugin = '$PT_BUILD_DIR/tests/plugin-mock.so', opts = {make_calls = 1}, cb = function() check('', '') check('hello', 'hello') check('hello % world', 'hello %% world') check('%%foobar', '%%%%foobar') f:write('ok\n') end, } __EOF__ x_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/barlib-stdout/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main in_fifo_file=./tmp-fifo-main-ev-input x_spawn_luastatus() { pt_spawn_luastatus_for_barlib_test_via_runner \ "$PT_SOURCE_DIR"/tests/barlib-runners/runner-redirect-34 \ -b "$PT_BUILD_DIR"/barlibs/stdout/barlib-stdout.so -B out_fd=4 \ "$@" } ================================================ FILE: tests/pt_tests/barlib-stdout/01-output.lib.bash ================================================ x_testcase_output() { local lua_expr=$1 local expect_line=$2 local expect_error_line='(Error)' local opts=() if [[ -n $O_SEPARATOR ]]; then opts+=(-B separator="$O_SEPARATOR") fi if [[ -n $O_ERROR ]]; then expect_error_line=$O_ERROR opts+=(-B error="$O_ERROR") fi pt_testcase_begin pt_write_widget_file <<__EOF__ local _my_flag = false widget = { plugin = '$PT_BUILD_DIR/tests/plugin-mock.so', opts = {make_calls = 2}, cb = function() if _my_flag then error('Boom') end _my_flag = true return ($lua_expr) end, } __EOF__ x_spawn_luastatus "${opts[@]}" if [[ -n "$expect_line" ]]; then pt_expect_line "$expect_line" <&${PT_SPAWNED_THINGS_FDS_0[luastatus]} fi pt_expect_line "$expect_error_line" <&${PT_SPAWNED_THINGS_FDS_0[luastatus]} pt_testcase_end } O_SEPARATOR= O_ERROR= x_testcase_output 'nil' '' x_testcase_output '""' '' x_testcase_output '"foo bar"' 'foo bar' x_testcase_output '"foo\nbar"' 'foobar' x_testcase_output '"?%%"' '?%%' x_testcase_output '{"foo", "bar", "baz", "quiz"}' 'foo | bar | baz | quiz' O_SEPARATOR='tum' \ x_testcase_output '{"foo", "bar", "baz", "quiz"}' 'footumbartumbaztumquiz' O_ERROR='dammit…' O_SEPARATOR='•' \ x_testcase_output '{"", "str", "", "", "str2", ""}' 'str•str2' O_ERROR=':(' \ x_testcase_output '""' '' x_testcase_output '{}' '' x_testcase_output '{nil}' '' x_testcase_output '{"", "", nil, ""}' '' ================================================ FILE: tests/pt_tests/barlib-stdout/03-input-filename.lib.bash ================================================ x_testcase_input() { pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_add_fifo "$in_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') widget = { plugin = '$PT_BUILD_DIR/tests/plugin-mock.so', opts = {make_calls = 0}, cb = function() end, event = function(t) f:write('event ' .. t .. '\n') end, } __EOF__ x_spawn_luastatus -B in_filename="$in_fifo_file" exec {pfd}<"$main_fifo_file" exec {pfd_in}>"$in_fifo_file" local line for line in "$@"; do sleep 1 printf '%s\n' "$line" >&${pfd_in} pt_expect_line "event $line" <&$pfd done pt_close_fd "$pfd" pt_testcase_end pt_close_fd "$pfd_in" } x_testcase_input x_testcase_input foo x_testcase_input foo bar x_testcase_input foo bar baz ================================================ FILE: tests/pt_tests/barlib-stdout/04-input-fd.lib.bash ================================================ x_testcase_input_fd() { pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') widget = { plugin = '$PT_BUILD_DIR/tests/plugin-mock.so', opts = {make_calls = 0}, cb = function() end, event = function(t) f:write('event ' .. t .. '\n') end, } __EOF__ x_spawn_luastatus -B in_fd=3 exec {pfd}<"$main_fifo_file" local line for line in "$@"; do sleep 1 printf '%s\n' "$line" >&${PT_SPAWNED_THINGS_FDS_1[luastatus]} pt_expect_line "event $line" <&$pfd done pt_close_fd "$pfd" pt_testcase_end pt_close_fd "$pfd_in" } x_testcase_input x_testcase_input foo x_testcase_input foo bar x_testcase_input foo bar baz ================================================ FILE: tests/pt_tests/luastatus/00-common.lib.bash ================================================ main_fifo_file=./tmp-main-fifo hang_timeout=${PT_HANG_TIMEOUT:-3} mock_barlib="$PT_BUILD_DIR"/tests/barlib-mock.so mock_plugin="$PT_BUILD_DIR"/tests/plugin-mock.so assert_exits_with_code() { local expected_c=$1 shift pt_spawn_luastatus_directly "$@" local actual_c=0 pt_wait_luastatus || actual_c=$? if (( expected_c != actual_c )); then pt_fail "Expected exit code $expected_c, found $actual_c." fi } assert_succeeds() { assert_exits_with_code 0 "$@" } assert_fails() { assert_exits_with_code 1 "$@" } assert_hangs() { pt_spawn_luastatus_directly "$@" sleep "$hang_timeout" pt_kill_thing luastatus } assert_works() { assert_hangs "$@" assert_succeeds -e "$@" } testcase_assert_fails() { pt_testcase_begin assert_fails "$@" pt_testcase_end } testcase_assert_succeeds() { pt_testcase_begin assert_succeeds "$@" pt_testcase_end } testcase_assert_works() { pt_testcase_begin assert_works "$@" pt_testcase_end } ================================================ FILE: tests/pt_tests/luastatus/01-misc.lib.bash ================================================ testcase_assert_fails testcase_assert_fails /dev/null testcase_assert_fails /dev/nil testcase_assert_fails -e testcase_assert_fails -TheseFlagsDoNotExist testcase_assert_fails -l '' testcase_assert_fails -l nosuchloglevel testcase_assert_fails -l '•÷¢˝Q⅓' testcase_assert_fails -l testcase_assert_fails -l -l testcase_assert_fails -ы testcase_assert_fails -v testcase_assert_fails -b testcase_assert_fails -b '§n”™°£' testcase_assert_fails -b '/' testcase_assert_fails -b 'nosuchbarlibforsure' testcase_assert_succeeds -b "$mock_barlib" -eeeeeeee -e testcase_assert_works -b "$mock_barlib" testcase_assert_works -b "$mock_barlib" . testcase_assert_works -b "$mock_barlib" . . . . . testcase_assert_works -b "$mock_barlib" /dev/null testcase_assert_works -b "$mock_barlib" /dev/null /dev/null /dev/null pt_testcase_begin pt_write_widget_file <<__EOF__ luastatus = nil __EOF__ assert_works -b "$mock_barlib" pt_testcase_end pt_testcase_begin pt_write_widget_file <<__EOF__ widget = setmetatable( { plugin = setmetatable({}, { __tostring = function() error("hi there (__tostring)") end, }), cb = function() end, }, { __index = function() error("hi there (__index)") end, __pairs = function() error("hi there (__pairs)") end, } ) __EOF__ assert_works -b "$mock_barlib" pt_testcase_end pt_testcase_begin pt_write_widget_file <<__EOF__ widget = { plugin = '$mock_plugin', cb = function() end, } __EOF__ assert_works -b "$mock_barlib" pt_testcase_end ================================================ FILE: tests/pt_tests/luastatus/02-getenv.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" unset_env_var=CzRbFEXexi while [[ -v $unset_env_var ]]; do unset_env_var+='a' done pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') function dump(varname) local r = os.getenv(varname) if r then f:write(string.format('%s: %s\n', varname, r)) else f:write(string.format('%s is not set\n', varname)) end end dump('MY_ENV_VAR') dump('$unset_env_var') __EOF__ MY_ENV_VAR='<>?foo bar~~~' pt_spawn_luastatus_directly -b "$mock_barlib" exec {pfd}<"$main_fifo_file" pt_expect_line 'MY_ENV_VAR: <>?foo bar~~~' <&$pfd pt_expect_line "$unset_env_var is not set" <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/luastatus/03-exit.lib.bash ================================================ pt_testcase_begin pt_write_widget_file <<__EOF__ os.exit(27) __EOF__ assert_exits_with_code 27 -b "$mock_barlib" pt_testcase_end pt_testcase_begin pt_write_widget_file <<__EOF__ os.exit() __EOF__ assert_exits_with_code 0 -b "$mock_barlib" pt_testcase_end ================================================ FILE: tests/pt_tests/luastatus/04-setlocale.lib.bash ================================================ pt_testcase_begin pt_write_widget_file <<__EOF__ assert(os.setlocale() == nil) assert(os.setlocale('C') == nil) os.exit(0) __EOF__ assert_exits_with_code 0 -b "$mock_barlib" pt_testcase_end ================================================ FILE: tests/pt_tests/luastatus/05-libwidechar.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') function my_to_str(x) if type(x) == 'number' then return string.format('%d', x) end if type(x) == 'boolean' then return x and 'TRUE' or 'FALSE' end if x == nil then return 'nil' end return '<<' .. x .. '>>' end function dump(x) f:write(my_to_str(x) .. '\\n') end function dump2(x, y) f:write(string.format('%s %s\\n', my_to_str(x), my_to_str(y))) end WIDTH = luastatus.libwidechar.width TRUNC = luastatus.libwidechar.truncate_to_width MKVALID = luastatus.libwidechar.make_valid_and_printable STR = "Test" dump(WIDTH(STR)) dump2(TRUNC(STR, 3)) dump(MKVALID(STR, "?")) STR = "$(printf \\xFF) Test" dump(WIDTH(STR)) dump2(TRUNC(STR, 3)) dump(MKVALID(STR, "?")) STR = "ЮЩЛЫ" dump(WIDTH(STR)) dump2(TRUNC(STR, 3)) dump(MKVALID(STR, "?")) STR = "t$(printf \\x01)est" dump(WIDTH(STR)) dump2(TRUNC(STR, 3)) dump(MKVALID(STR, "?")) STR = "ABCDEFGHI" dump(WIDTH(STR, 2)) dump(WIDTH(STR, 2, 3)) dump(TRUNC(STR, 1, 2)) dump(TRUNC(STR, 1, 3, 4)) dump(TRUNC(STR, 1, 3, 128)) dump(TRUNC(STR, 100, 3, 8)) dump(TRUNC(STR, 100, 3, 9)) dump(TRUNC(STR, 100, 3, 10)) dump(TRUNC(STR, 100, 3, 11)) dump(MKVALID(STR, '?', 3)) dump(MKVALID(STR, '?', 3, 5)) dump(luastatus.libwidechar.is_dummy_implementation()) __EOF__ pt_spawn_luastatus_directly -b "$mock_barlib" exec {pfd}<"$main_fifo_file" pt_expect_line '4' <&$pfd pt_expect_line '<> 3' <&$pfd pt_expect_line '<>' <&$pfd pt_expect_line 'nil' <&$pfd pt_expect_line 'nil nil' <&$pfd pt_expect_line '<>' <&$pfd pt_expect_line '4' <&$pfd pt_expect_line '<<ЮЩЛ>> 3' <&$pfd pt_expect_line '<<ЮЩЛЫ>>' <&$pfd pt_expect_line 'nil' <&$pfd pt_expect_line 'nil nil' <&$pfd pt_expect_line '<>' <&$pfd pt_expect_line '8' <&$pfd pt_expect_line '2' <&$pfd pt_expect_line '<>' <&$pfd pt_expect_line '<>' <&$pfd pt_expect_line '<>' <&$pfd pt_expect_line '<>' <&$pfd pt_expect_line '<>' <&$pfd pt_expect_line '<>' <&$pfd pt_expect_line '<>' <&$pfd pt_expect_line '<>' <&$pfd pt_expect_line '<>' <&$pfd pt_expect_line 'FALSE' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/luastatus/06-execute.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') if rawlen == nil then -- Lua 5.1 assert(os.execute() ~= 0) assert(type(os.execute('exit 42')) == 'number') f:write('ok\n') else -- Lua >=5.2 assert(os.execute() == true) local is_ok, what, code is_ok, what, code = os.execute('exit 42') assert(is_ok == nil) assert(what == 'exit') assert(code == 42) is_ok, what, code = os.execute('exit 0') assert(is_ok == true) assert(what == 'exit') assert(code == 0) is_ok, what, code = os.execute('kill -9 \$\$') assert(is_ok == nil) assert(what == 'signal') assert(code == 9) f:write('ok\n') end __EOF__ pt_spawn_luastatus_directly -b "$mock_barlib" exec {pfd}<"$main_fifo_file" pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-battery-linux/00-common.lib.bash ================================================ pt_require_tools mktemp main_fifo_file=./tmp-fifo-main battery_testcase() { local expect_str=$1 local uevent_content=$2 # empty string here means remove the file pt_testcase_begin pt_add_fifo "$main_fifo_file" local batdev_dir; batdev_dir=$(mktemp -d) || pt_fail "'mktemp -d' failed" pt_add_dir_to_remove "$batdev_dir" local uevent_file=$batdev_dir/uevent if [[ -n "$uevent_content" ]]; then printf '%s' "$uevent_content" > "$uevent_file" || pt_fail "cannot write uevent_file" else rm -f "$uevent_file" fi pt_add_file_to_remove "$uevent_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') function my_event_func() end x = dofile('$PT_SOURCE_DIR/plugins/battery-linux/battery-linux.lua') widget = x.widget{ _devpath = '$batdev_dir', cb = function(t) f:write('cb') if t.status then f:write(string.format(' status="%s"', t.status)) end if t.capacity then f:write(string.format(' capacity=%.0f', t.capacity)) end if t.consumption then f:write(string.format(' consumption=%.1f', t.consumption)) end if t.rem_time then f:write(string.format(' rem_time=%.1f', t.rem_time)) end f:write('\n') end, event = my_event_func, } widget.plugin = ('$PT_BUILD_DIR/plugins/{}/plugin-{}.so'):gsub('{}', widget.plugin) assert(widget.event == my_event_func) __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line "$expect_str" <&$pfd pt_close_fd "$pfd" pt_testcase_end } ================================================ FILE: tests/pt_tests/plugin-battery-linux/01-full-status-full.lib.bash ================================================ battery_testcase 'cb status="Full" capacity=100' "\ POWER_SUPPLY_NAME=BAT0 POWER_SUPPLY_TYPE=Battery POWER_SUPPLY_STATUS=Full POWER_SUPPLY_PRESENT=1 POWER_SUPPLY_TECHNOLOGY=Li-ion POWER_SUPPLY_CYCLE_COUNT=0 POWER_SUPPLY_VOLTAGE_MIN_DESIGN=15200000 POWER_SUPPLY_VOLTAGE_NOW=17074000 POWER_SUPPLY_CURRENT_NOW=0 POWER_SUPPLY_CHARGE_FULL_DESIGN=3030000 POWER_SUPPLY_CHARGE_FULL=3289000 POWER_SUPPLY_CHARGE_NOW=3285000 POWER_SUPPLY_CAPACITY=99 POWER_SUPPLY_CAPACITY_LEVEL=Normal POWER_SUPPLY_MODEL_NAME=R14B01W POWER_SUPPLY_MANUFACTURER=Coslight POWER_SUPPLY_SERIAL_NUMBER=33468 " ================================================ FILE: tests/pt_tests/plugin-battery-linux/02-full-status-not-charging.lib.bash ================================================ battery_testcase 'cb status="Not charging" capacity=100' "\ POWER_SUPPLY_NAME=BAT0 POWER_SUPPLY_TYPE=Battery POWER_SUPPLY_STATUS=Not charging POWER_SUPPLY_PRESENT=1 POWER_SUPPLY_TECHNOLOGY=Li-ion POWER_SUPPLY_CYCLE_COUNT=0 POWER_SUPPLY_VOLTAGE_MIN_DESIGN=15200000 POWER_SUPPLY_VOLTAGE_NOW=17074000 POWER_SUPPLY_CURRENT_NOW=0 POWER_SUPPLY_CHARGE_FULL_DESIGN=3030000 POWER_SUPPLY_CHARGE_FULL=3289000 POWER_SUPPLY_CHARGE_NOW=3285000 POWER_SUPPLY_CAPACITY=99 POWER_SUPPLY_CAPACITY_LEVEL=Normal POWER_SUPPLY_MODEL_NAME=R14B01W POWER_SUPPLY_MANUFACTURER=Coslight POWER_SUPPLY_SERIAL_NUMBER=33468 " ================================================ FILE: tests/pt_tests/plugin-battery-linux/03-dischaging.lib.bash ================================================ battery_testcase 'cb status="Discharging" capacity=99 consumption=18.6 rem_time=2.9' "\ POWER_SUPPLY_NAME=BAT0 POWER_SUPPLY_TYPE=Battery POWER_SUPPLY_STATUS=Discharging POWER_SUPPLY_PRESENT=1 POWER_SUPPLY_TECHNOLOGY=Li-ion POWER_SUPPLY_CYCLE_COUNT=0 POWER_SUPPLY_VOLTAGE_MIN_DESIGN=15200000 POWER_SUPPLY_VOLTAGE_NOW=16806000 POWER_SUPPLY_CURRENT_NOW=1107000 POWER_SUPPLY_CHARGE_FULL_DESIGN=3030000 POWER_SUPPLY_CHARGE_FULL=3285000 POWER_SUPPLY_CHARGE_NOW=3247000 POWER_SUPPLY_CAPACITY=98 POWER_SUPPLY_CAPACITY_LEVEL=Normal POWER_SUPPLY_MODEL_NAME=R14B01W POWER_SUPPLY_MANUFACTURER=Coslight POWER_SUPPLY_SERIAL_NUMBER=33468 " ================================================ FILE: tests/pt_tests/plugin-battery-linux/04-charging.lib.bash ================================================ battery_testcase 'cb status="Charging" capacity=94 consumption=12.7 rem_time=0.3' "\ POWER_SUPPLY_NAME=BAT0 POWER_SUPPLY_TYPE=Battery POWER_SUPPLY_STATUS=Charging POWER_SUPPLY_PRESENT=1 POWER_SUPPLY_TECHNOLOGY=Li-ion POWER_SUPPLY_CYCLE_COUNT=0 POWER_SUPPLY_VOLTAGE_MIN_DESIGN=15200000 POWER_SUPPLY_VOLTAGE_NOW=17131000 POWER_SUPPLY_CURRENT_NOW=743000 POWER_SUPPLY_CHARGE_FULL_DESIGN=3030000 POWER_SUPPLY_CHARGE_FULL=3298000 POWER_SUPPLY_CHARGE_NOW=3096000 POWER_SUPPLY_CAPACITY=93 POWER_SUPPLY_CAPACITY_LEVEL=Normal POWER_SUPPLY_MODEL_NAME=R14B01W POWER_SUPPLY_MANUFACTURER=Coslight POWER_SUPPLY_SERIAL_NUMBER=33468 " ================================================ FILE: tests/pt_tests/plugin-battery-linux/05-nofile.lib.bash ================================================ battery_testcase 'cb' '' ================================================ FILE: tests/pt_tests/plugin-cpu-freq-linux/00-common.lib.bash ================================================ pt_require_tools mktemp main_fifo_file=./tmp-fifo-main cpu_dir= x_testcase_begin() { cpu_dir=$(mktemp -d) || pt_fail 'mktemp -d failed' } x_add_or_set_cpu() { local i=${1?}; shift local max=${1?}; shift local min=${1?}; shift local cur=${1?}; shift if ! [[ -d "$cpu_dir"/cpu"$i" ]]; then mkdir "$cpu_dir"/cpu"$i" || return $? mkdir "$cpu_dir"/cpu"$i"/cpufreq || return $? pt_add_dirs_to_remove_inorder \ "$cpu_dir"/cpu"$i"/cpufreq \ "$cpu_dir"/cpu"$i" \ || return $? pt_add_file_to_remove "$cpu_dir"/cpu"$i"/cpufreq/cpuinfo_max_freq || return $? pt_add_file_to_remove "$cpu_dir"/cpu"$i"/cpufreq/cpuinfo_min_freq || return $? pt_add_file_to_remove "$cpu_dir"/cpu"$i"/cpufreq/scaling_cur_freq || return $? fi echo "$max" > "$cpu_dir"/cpu"$i"/cpufreq/cpuinfo_max_freq || return $? echo "$min" > "$cpu_dir"/cpu"$i"/cpufreq/cpuinfo_min_freq || return $? echo "$cur" > "$cpu_dir"/cpu"$i"/cpufreq/scaling_cur_freq || return $? } x_testcase() { local reload_each_time=${1?}; shift local callback=${1?}; shift local reload_code= if (( reload_each_time )); then reload_code='data.please_reload = true;' fi pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') function my_event_func() end x = dofile('$PT_SOURCE_DIR/plugins/cpu-freq-linux/cpu-freq-linux.lua') function format_result_entry(x) return string.format('max=%d,min=%d,cur=%d', x.max, x.min, x.cur) end function format_result(arr) local chunks = {} for _, x in ipairs(arr) do table.insert(chunks, format_result_entry(x)) end return table.concat(chunks, ' ') end data = {_cpu_dir = '$cpu_dir'} widget = x.widget({ timer_opts = { period = 0.1, }, cb = function(t) $reload_code if type(t) == 'table' then f:write('cb ' .. format_result(t) .. '\n') else assert(t == nil) f:write('cb nil\n') end end, event = my_event_func, }, data) widget.plugin = ('$PT_BUILD_DIR/plugins/{}/plugin-{}.so'):gsub('{}', widget.plugin) assert(widget.event == my_event_func) __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd local i for (( i = 0; i < $#; ++i )); do pt_check $callback $i local j=$(( i + 1 )) pt_expect_line "${!j}" <&$pfd done pt_close_fd "$pfd" pt_testcase_end } x_testcase_end() { pt_add_dir_to_remove "$cpu_dir" cpu_dir= } ================================================ FILE: tests/pt_tests/plugin-cpu-freq-linux/01-simple.lib.bash ================================================ x_testcase_begin x_add_or_set_cpu 0 1000000 500000 750000 x_my_callback() { case "$1" in 0) true ;; 1) pt_check x_add_or_set_cpu 0 20 10 30 ;; 2) pt_check x_add_or_set_cpu 0 9000000 3000000 2000000 ;; 3) pt_check x_add_or_set_cpu 0 20 10 15 pt_check x_add_or_set_cpu 1 50 30 40 ;; esac } x_testcase \ 0 \ x_my_callback \ 'cb max=1000000,min=500000,cur=750000' \ 'cb max=1000000,min=500000,cur=500000' \ 'cb max=1000000,min=500000,cur=1000000' \ 'cb max=1000000,min=500000,cur=500000' x_testcase_end ================================================ FILE: tests/pt_tests/plugin-cpu-freq-linux/02-please-reload.lib.bash ================================================ x_testcase_begin x_add_or_set_cpu 0 1000000 500000 750000 x_my_callback() { case "$1" in 0) true ;; 1) pt_check x_add_or_set_cpu 0 20 10 30 ;; 2) pt_check x_add_or_set_cpu 0 9000000 3000000 2000000 ;; 3) pt_check x_add_or_set_cpu 0 20 10 15 pt_check x_add_or_set_cpu 1 50 30 40 ;; esac } x_testcase \ 1 \ x_my_callback \ 'cb max=1000000,min=500000,cur=750000' \ 'cb max=20,min=10,cur=20' \ 'cb max=9000000,min=3000000,cur=3000000' \ 'cb max=20,min=10,cur=15 max=50,min=30,cur=40' x_testcase_end ================================================ FILE: tests/pt_tests/plugin-cpu-usage-linux/00-common.lib.bash ================================================ pt_require_tools mktemp main_fifo_file=./tmp-fifo-main stat_content_1="\ cpu 32855780 19344 19217634 468500203 300653 0 273158 0 0 0 cpu0 8358858 4763 5675358 116643347 61382 0 4672 0 0 0 cpu1 8301511 4836 5853008 116508899 59664 0 25225 0 0 0 cpu2 7952134 4371 3472358 117560453 113665 0 221818 0 0 0 cpu3 8243275 5374 4216907 117787501 65941 0 21442 0 0 0 " stat_content_2="\ cpu 32855819 19344 19217652 468500542 300653 0 273158 0 0 0 cpu0 8358868 4763 5675362 116643433 61382 0 4672 0 0 0 cpu1 8301519 4836 5853012 116508986 59664 0 25225 0 0 0 cpu2 7952146 4371 3472364 117560535 113665 0 221818 0 0 0 cpu3 8243285 5374 4216913 117787586 65941 0 21442 0 0 0 " cpu_usage_testcase() { local cpu_option=$1 local expect_str=$2 pt_testcase_begin pt_add_fifo "$main_fifo_file" local proc_dir; proc_dir=$(mktemp -d) || pt_fail "'mktemp -d' failed" pt_add_dir_to_remove "$proc_dir" local stat_file=$proc_dir/stat printf '%s' "$stat_content_1" > "$stat_file" || pt_fail 'cannot write stat_file' pt_add_file_to_remove "$stat_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') function my_event_func() end x = dofile('$PT_SOURCE_DIR/plugins/cpu-usage-linux/cpu-usage-linux.lua') widget = x.widget{ _procpath = '$proc_dir', cpu = $cpu_option, cb = function(t) if t then f:write(string.format('cb %.1f\n', t)) else f:write('cb nil\n') end end, event = my_event_func, } widget.plugin = ('$PT_BUILD_DIR/plugins/{}/plugin-{}.so'):gsub('{}', widget.plugin) assert(widget.event == my_event_func) __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb nil' <&$pfd printf '%s' "$stat_content_2" > "$stat_file" pt_expect_line "$expect_str" <&$pfd pt_close_fd "$pfd" pt_testcase_end } ================================================ FILE: tests/pt_tests/plugin-cpu-usage-linux/01-total.lib.bash ================================================ cpu_usage_testcase 'nil' 'cb 0.1' ================================================ FILE: tests/pt_tests/plugin-cpu-usage-linux/02-cpu1.lib.bash ================================================ cpu_usage_testcase '1' 'cb 0.1' ================================================ FILE: tests/pt_tests/plugin-cpu-usage-linux/03-cpu2.lib.bash ================================================ cpu_usage_testcase '2' 'cb 0.1' ================================================ FILE: tests/pt_tests/plugin-cpu-usage-linux/04-cpu3.lib.bash ================================================ cpu_usage_testcase '3' 'cb 0.2' ================================================ FILE: tests/pt_tests/plugin-cpu-usage-linux/05-cpu4.lib.bash ================================================ cpu_usage_testcase '4' 'cb 0.2' ================================================ FILE: tests/pt_tests/plugin-dbus/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main x_dbus_begin() { pt_dbus_daemon_spawn --session } x_dbus_end() { pt_dbus_daemon_kill } dbus_srv_py_fifo=./tmp-fifo-for-dbus-srv-py x_dbus_spawn_dbus_srv_py() { pt_add_fifo "$dbus_srv_py_fifo" pt_spawn_thing dbus_srv_py ./optional/dbus_srv.py "$dbus_srv_py_fifo" pt_expect_line 'running' < "$dbus_srv_py_fifo" sleep 1 } x_dbus_kill_dbus_srv_py() { pt_kill_thing dbus_srv_py } preface=' local function _fmt_x(x) local function _validate_str(s) assert(s:find("\n") == nil, "string contains a line break") assert(s:find("\"") == nil, "string contains a double quote sign") end local function _fmt_num(n) if n == math.floor(n) then return string.format("%d", n) else return string.format("%.4f", n) end end local t = type(x) if t == "table" then local tk = type(next(x)) if tk == "nil" then return "{}" elseif tk == "number" then local s = {} for _, v in ipairs(x) do s[#s + 1] = _fmt_x(v) end return string.format("{%s}", table.concat(s, ",")) elseif tk == "string" then local ks = {} for k, _ in pairs(x) do assert(type(k) == "string", "table has mixed-type keys") _validate_str(k) ks[#ks + 1] = k end table.sort(ks) local s = {} for _, k in ipairs(ks) do local v = x[k] s[#s + 1] = string.format("[\"%s\"]=%s", k, _fmt_x(v)) end return string.format("{%s}", table.concat(s, ",")) else error("table key has unexpected type: " .. tk) end elseif t == "string" then _validate_str(x) return string.format("\"%s\"", x) elseif t == "number" then return _fmt_num(x) else return tostring(x) end end local function unpack1(t) assert(type(t) == "table") assert(#t == 1) return t[1] end local function unpack2(t) assert(type(t) == "table") assert(#t == 2) return t[1], t[2] end ' ================================================ FILE: tests/pt_tests/plugin-dbus/01-simple.lib.bash ================================================ pt_require_tools dbus-send x_dbus_begin pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/dbus/plugin-dbus.so', opts = { signals = { { object_path = '/org/luastatus/sample/object/name', interface = 'org.luastatus.ExampleInterface', bus = 'session', }, }, }, cb = function(t) if t.what == 'signal' then assert(type(t.sender) == 'string') t.sender = '(non-deterministic)' end f:write('cb ' .. _fmt_x(t) .. '\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd my_sender_process() { while true; do dbus-send \ --session \ /org/luastatus/sample/object/name \ org.luastatus.ExampleInterface.ExampleMethod \ int32:47 string:'hello world' double:63.125 \ array:string:"1st item","next item","last item" \ dict:string:int32:"one",1,"two",2,"three",3 \ variant:int32:-8 \ objpath:/org/luastatus/sample/object/name \ || exit $? sleep 2 done } pt_spawn_thing dbus_sender my_sender_process pt_expect_line 'cb {["bus"]="session",["interface"]="org.luastatus.ExampleInterface",["object_path"]="/org/luastatus/sample/object/name",["parameters"]={"47","hello world",63.1250,{"1st item","next item","last item"},{{"one","1"},{"two","2"},{"three","3"}},"-8","/org/luastatus/sample/object/name"},["sender"]="(non-deterministic)",["signal"]="ExampleMethod",["what"]="signal"}' <&$pfd pt_close_fd "$pfd" pt_testcase_end pt_kill_thing dbus_sender x_dbus_end ================================================ FILE: tests/pt_tests/plugin-dbus/02-timeout.lib.bash ================================================ pt_require_tools dbus-send x_dbus_begin pt_testcase_begin using_measure pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/dbus/plugin-dbus.so', opts = { signals = { { object_path = '/org/luastatus/sample/object/name', interface = 'org.luastatus.ExampleInterface', bus = 'session', }, }, timeout = 2, greet = true, }, cb = function(t) if t.what == 'signal' then assert(type(t.sender) == 'string') t.sender = '(non-deterministic)' end f:write('cb ' .. _fmt_x(t) .. '\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb {["what"]="hello"}' <&$pfd measure_start pt_expect_line 'cb {["what"]="timeout"}' <&$pfd measure_check_ms 2000 for (( i = 0; i < 3; ++i )); do pt_check dbus-send \ --session \ /org/luastatus/sample/object/name \ org.luastatus.ExampleInterface.ExampleMethod \ int32:47 string:'hello world' double:63.125 \ array:string:"1st item","next item","last item" \ dict:string:int32:"one",1,"two",2,"three",3 \ variant:int32:-8 \ objpath:/org/luastatus/sample/object/name measure_start pt_expect_line 'cb {["bus"]="session",["interface"]="org.luastatus.ExampleInterface",["object_path"]="/org/luastatus/sample/object/name",["parameters"]={"47","hello world",63.1250,{"1st item","next item","last item"},{{"one","1"},{"two","2"},{"three","3"}},"-8","/org/luastatus/sample/object/name"},["sender"]="(non-deterministic)",["signal"]="ExampleMethod",["what"]="signal"}' <&$pfd measure_check_ms 0 pt_expect_line 'cb {["what"]="timeout"}' <&$pfd measure_check_ms 2000 pt_expect_line 'cb {["what"]="timeout"}' <&$pfd measure_check_ms 2000 done pt_close_fd "$pfd" pt_testcase_end x_dbus_end ================================================ FILE: tests/pt_tests/plugin-dbus/03-greet.lib.bash ================================================ pt_require_tools dbus-send x_dbus_begin pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/dbus/plugin-dbus.so', opts = { signals = { { object_path = '/org/luastatus/sample/object/name', interface = 'org.luastatus.ExampleInterface', bus = 'session', }, }, greet = true, }, cb = function(t) if t.what == 'signal' then assert(type(t.sender) == 'string') t.sender = '(non-deterministic)' end f:write('cb ' .. _fmt_x(t) .. '\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb {["what"]="hello"}' <&$pfd sleep 1 pt_check dbus-send \ --session \ /org/luastatus/sample/object/name \ org.luastatus.ExampleInterface.ExampleMethod \ int32:47 string:'hello world' double:63.125 \ array:string:"1st item","next item","last item" \ dict:string:int32:"one",1,"two",2,"three",3 \ variant:int32:-8 \ objpath:/org/luastatus/sample/object/name pt_expect_line 'cb {["bus"]="session",["interface"]="org.luastatus.ExampleInterface",["object_path"]="/org/luastatus/sample/object/name",["parameters"]={"47","hello world",63.1250,{"1st item","next item","last item"},{{"one","1"},{"two","2"},{"three","3"}},"-8","/org/luastatus/sample/object/name"},["sender"]="(non-deterministic)",["signal"]="ExampleMethod",["what"]="signal"}' <&$pfd pt_close_fd "$pfd" pt_testcase_end x_dbus_end ================================================ FILE: tests/pt_tests/plugin-dbus/04-get-prop.lib.bash ================================================ x_dbus_begin pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/dbus/plugin-dbus.so', opts = { signals = {}, greet = true, }, cb = function(t) assert(t.what == 'hello', 't.what is not "hello"') local is_ok, res = luastatus.plugin.get_property({ bus = "session", dest = "org.freedesktop.DBus", object_path = "/org/freedesktop/DBus", interface = "org.freedesktop.DBus", property_name = "Features", }) assert(is_ok, res) local features = unpack1(res) assert(type(features) == 'table', 'features is not an array') for _, x in ipairs(features) do print('Feature:', x) assert(type(x) == 'string', 'feature is not a string') end f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end x_dbus_end ================================================ FILE: tests/pt_tests/plugin-dbus/05-get-all-prop.lib.bash ================================================ x_dbus_begin pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/dbus/plugin-dbus.so', opts = { signals = {}, greet = true, }, cb = function(t) assert(t.what == 'hello', 't.what is not "hello"') local is_ok, res = luastatus.plugin.get_all_properties({ bus = "session", dest = "org.freedesktop.DBus", object_path = "/org/freedesktop/DBus", interface = "org.freedesktop.DBus", }) assert(is_ok, res) local arr = unpack1(res) assert(type(arr) == 'table', 'result is not a table') local props_set = {} for _, x in ipairs(arr) do local name, value = unpack2(x) print('Property:', name, value) assert(type(name) == 'string', 'property name is not a string') props_set[name] = true end assert(props_set['Features'], 'no "Features" property') assert(props_set['Interfaces'], 'no "Interfaces" property') f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end x_dbus_end ================================================ FILE: tests/pt_tests/plugin-dbus/06-OPTIONAL-getset-prop.lib.bash ================================================ if (( ! PLUGIN_DBUS_OPTIONAL )); then return 0 fi x_dbus_begin x_dbus_spawn_dbus_srv_py pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface local function do_call_plugin_function(f, params) local is_ok, res = f(params) assert(is_ok, res) return res end local function test_get_set_prop() do_call_plugin_function(luastatus.plugin.set_property_str, { bus = "session", dest = "io.github.shdown.luastatus.test", object_path = "/io/github/shdown/luastatus/test/MyObject", interface = "io.github.shdown.luastatus.test", property_name = "MyProperty", value_str = "hi there", }) local prop = unpack1(do_call_plugin_function(luastatus.plugin.get_property, { bus = "session", dest = "io.github.shdown.luastatus.test", object_path = "/io/github/shdown/luastatus/test/MyObject", interface = "io.github.shdown.luastatus.test", property_name = "MyProperty", })) if prop ~= "hi there" then error(string.format("invalid property value after Set(): %s", prop)) end end widget = { plugin = '$PT_BUILD_DIR/plugins/dbus/plugin-dbus.so', opts = { signals = {}, greet = true, }, cb = function(t) test_get_set_prop() f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end x_dbus_kill_dbus_srv_py x_dbus_end ================================================ FILE: tests/pt_tests/plugin-dbus/07-OPTIONAL-getall-prop.lib.bash ================================================ if (( ! PLUGIN_DBUS_OPTIONAL )); then return 0 fi x_dbus_begin x_dbus_spawn_dbus_srv_py pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface local function do_call_plugin_function(f, params) local is_ok, res = f(params) assert(is_ok, res) return res end local function test_get_all_props() do_call_plugin_function(luastatus.plugin.set_property_str, { bus = "session", dest = "io.github.shdown.luastatus.test", object_path = "/io/github/shdown/luastatus/test/MyObject", interface = "io.github.shdown.luastatus.test", property_name = "MyProperty", value_str = "test... test... test...", }) local res = do_call_plugin_function(luastatus.plugin.get_all_properties, { bus = "session", dest = "io.github.shdown.luastatus.test", object_path = "/io/github/shdown/luastatus/test/MyObject", interface = "io.github.shdown.luastatus.test", }) local k, v = unpack2(unpack1(unpack1(res))) assert(k == 'MyProperty') assert(v == 'test... test... test...') end widget = { plugin = '$PT_BUILD_DIR/plugins/dbus/plugin-dbus.so', opts = { signals = {}, greet = true, }, cb = function(t) test_get_all_props() f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end x_dbus_kill_dbus_srv_py x_dbus_end ================================================ FILE: tests/pt_tests/plugin-dbus/08-OPTIONAL-call-method-str.lib.bash ================================================ if (( ! PLUGIN_DBUS_OPTIONAL )); then return 0 fi x_dbus_begin x_dbus_spawn_dbus_srv_py pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface local function do_call_plugin_function(f, params) local is_ok, res = f(params) assert(is_ok, res) return res end local function test_call_method_upcase() local raw_res = do_call_plugin_function(luastatus.plugin.call_method_str, { bus = "session", dest = "io.github.shdown.luastatus.test", object_path = "/io/github/shdown/luastatus/test/MyObject", interface = "io.github.shdown.luastatus.test", method = "Upcase", arg_str = "Please upcase this", }) local res = unpack1(raw_res) assert(res == 'PLEASE UPCASE THIS') end local function test_call_method_ret42() local raw_res = do_call_plugin_function(luastatus.plugin.call_method_str, { bus = "session", dest = "io.github.shdown.luastatus.test", object_path = "/io/github/shdown/luastatus/test/MyObject", interface = "io.github.shdown.luastatus.test", method = "ReturnFortyTwo", -- no "arg_str" }) local res = unpack1(raw_res) assert(res == '42') end widget = { plugin = '$PT_BUILD_DIR/plugins/dbus/plugin-dbus.so', opts = { signals = {}, greet = true, }, cb = function(t) test_call_method_upcase() test_call_method_ret42() f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end x_dbus_kill_dbus_srv_py x_dbus_end ================================================ FILE: tests/pt_tests/plugin-dbus/09-OPTIONAL-call-method-DTLL.lib.bash ================================================ if (( ! PLUGIN_DBUS_OPTIONAL )); then return 0 fi # DTLL = dbustypes_lowlevel x_dbus_begin x_dbus_spawn_dbus_srv_py pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface local function do_call_plugin_function(f, params) local is_ok, res = f(params) assert(is_ok, res) return res end local function test_call_method() local DTLL = luastatus.plugin.dbustypes_lowlevel local DTLL_str = DTLL.mktype_simple('s') local S = function(s) return DTLL.mkval_simple(DTLL_str, s) end local the_arg = DTLL.mkval_array(DTLL_str, { S('one'), S('two'), S('three'), }) local args = DTLL.mkval_tuple({the_arg}) local raw_res = do_call_plugin_function(luastatus.plugin.call_method, { bus = "session", dest = "io.github.shdown.luastatus.test", object_path = "/io/github/shdown/luastatus/test/MyObject", interface = "io.github.shdown.luastatus.test", method = "ConvertArrayToDictHexify", args = args, }) local res = unpack1(raw_res) assert(type(res) == 'table') local outputs = {} for _, kv in ipairs(res) do table.insert(outputs, string.format('%s:%s', kv[1], kv[2])) end table.sort(outputs) local output = table.concat(outputs, ' ') assert(output == '30:6F6E65 31:74776F 32:7468726565') end widget = { plugin = '$PT_BUILD_DIR/plugins/dbus/plugin-dbus.so', opts = { signals = {}, greet = true, }, cb = function(t) test_call_method() f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end x_dbus_kill_dbus_srv_py x_dbus_end ================================================ FILE: tests/pt_tests/plugin-dbus/10-OPTIONAL-call-method-arr.lib.bash ================================================ if (( ! PLUGIN_DBUS_OPTIONAL )); then return 0 fi x_dbus_begin x_dbus_spawn_dbus_srv_py pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface local function do_call_plugin_function(f, params) local is_ok, res = f(params) assert(is_ok, res) return res end local function test_call_method() local args = luastatus.plugin.dbustypes.mkval_from_fmt('(as)', {{ 'one', 'two', 'three' }}) local raw_res = do_call_plugin_function(luastatus.plugin.call_method, { bus = "session", dest = "io.github.shdown.luastatus.test", object_path = "/io/github/shdown/luastatus/test/MyObject", interface = "io.github.shdown.luastatus.test", method = "ConvertArrayToDictHexify", args = args, }) local res = unpack1(raw_res) assert(type(res) == 'table') local outputs = {} for _, kv in ipairs(res) do table.insert(outputs, string.format('%s:%s', kv[1], kv[2])) end table.sort(outputs) local output = table.concat(outputs, ' ') assert(output == '30:6F6E65 31:74776F 32:7468726565') end widget = { plugin = '$PT_BUILD_DIR/plugins/dbus/plugin-dbus.so', opts = { signals = {}, greet = true, }, cb = function(t) test_call_method() f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end x_dbus_kill_dbus_srv_py x_dbus_end ================================================ FILE: tests/pt_tests/plugin-dbus/11-OPTIONAL-call-method-dict.lib.bash ================================================ if (( ! PLUGIN_DBUS_OPTIONAL )); then return 0 fi x_dbus_begin x_dbus_spawn_dbus_srv_py pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface local function do_call_plugin_function(f, params) local is_ok, res = f(params) assert(is_ok, res) return res end local function test_call_method() local args = luastatus.plugin.dbustypes.mkval_from_fmt('(a{ss})', {{ {'key1', 'value1'}, {'key2', 'value2'}, {'key3', 'value3'}, }}) local raw_res = do_call_plugin_function(luastatus.plugin.call_method, { bus = "session", dest = "io.github.shdown.luastatus.test", object_path = "/io/github/shdown/luastatus/test/MyObject", interface = "io.github.shdown.luastatus.test", method = "ConvertDictToString", args = args, }) local res = unpack1(raw_res) assert(type(res) == 'string') assert(res == 'key1:value1,key2:value2,key3:value3') end widget = { plugin = '$PT_BUILD_DIR/plugins/dbus/plugin-dbus.so', opts = { signals = {}, greet = true, }, cb = function(t) test_call_method() f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end x_dbus_kill_dbus_srv_py x_dbus_end ================================================ FILE: tests/pt_tests/plugin-dbus/12-OPTIONAL-call-method-tuple.lib.bash ================================================ if (( ! PLUGIN_DBUS_OPTIONAL )); then return 0 fi x_dbus_begin x_dbus_spawn_dbus_srv_py pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface local function do_call_plugin_function(f, params) local is_ok, res = f(params) assert(is_ok, res) return res end local function do_test_once(method, args, expected_result) local raw_res = do_call_plugin_function(luastatus.plugin.call_method, { bus = "session", dest = "io.github.shdown.luastatus.test", object_path = "/io/github/shdown/luastatus/test/MyObject", interface = "io.github.shdown.luastatus.test", method = method, args = args, }) local res = unpack1(raw_res) assert(type(res) == 'string') assert(res == expected_result) end local function test_all() do_test_once( 'RecvTuple0', luastatus.plugin.dbustypes.mkval_from_fmt('()', {}), 'Empty' ) do_test_once( 'RecvTuple1', luastatus.plugin.dbustypes.mkval_from_fmt('(s)', {'hello'}), 'hello' ) do_test_once( 'RecvTuple2', luastatus.plugin.dbustypes.mkval_from_fmt('(ss)', {'hello', 'world'}), 'helloworld' ) do_test_once( 'RecvTuple3', luastatus.plugin.dbustypes.mkval_from_fmt('(sss)', {'hello', 'world', '!'}), 'helloworld!' ) end widget = { plugin = '$PT_BUILD_DIR/plugins/dbus/plugin-dbus.so', opts = { signals = {}, greet = true, }, cb = function(t) test_all() f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end x_dbus_kill_dbus_srv_py x_dbus_end ================================================ FILE: tests/pt_tests/plugin-dbus/13-OPTIONAL-call-method-misc.lib.bash ================================================ if (( ! PLUGIN_DBUS_OPTIONAL )); then return 0 fi x_dbus_begin x_dbus_spawn_dbus_srv_py pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface local function do_call_plugin_function(f, params) local is_ok, res = f(params) assert(is_ok, res) return res end local function do_test_once(fmt, value, expected_result) local raw_res = do_call_plugin_function(luastatus.plugin.call_method, { bus = "session", dest = "io.github.shdown.luastatus.test", object_path = "/io/github/shdown/luastatus/test/MyObject", interface = "io.github.shdown.luastatus.test", method = "RecvVariant", args = luastatus.plugin.dbustypes.mkval_from_fmt(fmt, value), }) local res = unpack1(raw_res) assert(type(res) == 'string') assert(res == expected_result) end local function test_all() do_test_once('(b)', {true}, 'bool') do_test_once('(y)', {42}, 'byte') do_test_once('(y)', {'x'}, 'byte') do_test_once('(d)', {1.25}, 'double') do_test_once('(s)', {'hello'}, 'string') do_test_once('(o)', {'/io/github/shdown/luastatus/foo/bar'}, 'object_path') do_test_once('(g)', {'(susssasa{sv}i)'}, 'signature') local wrapped = luastatus.plugin.dbustypes.mkval_from_fmt('s', 'hi there') do_test_once('(v)', {wrapped}, 'variant') local INT_TYPES = { {'(n)', 'i16'}, {'(q)', 'u16'}, {'(i)', 'i32'}, {'(u)', 'u32'}, {'(x)', 'i64'}, {'(t)', 'u64'}, } for _, pq in ipairs(INT_TYPES) do local fmt, expected_result = pq[1], pq[2] do_test_once(fmt, {53}, expected_result) do_test_once(fmt, {'58'}, expected_result) end end widget = { plugin = '$PT_BUILD_DIR/plugins/dbus/plugin-dbus.so', opts = { signals = {}, greet = true, }, cb = function(t) test_all() f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end x_dbus_kill_dbus_srv_py x_dbus_end ================================================ FILE: tests/pt_tests/plugin-dbus/14-OPTIONAL-call-method-handle.lib.bash ================================================ if (( ! PLUGIN_DBUS_OPTIONAL )); then return 0 fi x_dbus_begin x_dbus_spawn_dbus_srv_py pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface local function do_test() local is_ok, err_msg = pcall(luastatus.plugin.dbustypes.mkval_from_fmt, 'h', 42) assert(not is_ok, 'expected error to be thrown from "mkval_from_fmt"') assert(err_msg:find('creation of handles is not supported') ~= nil) end widget = { plugin = '$PT_BUILD_DIR/plugins/dbus/plugin-dbus.so', opts = { signals = {}, greet = true, }, cb = function(t) do_test() f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end x_dbus_kill_dbus_srv_py x_dbus_end ================================================ FILE: tests/pt_tests/plugin-dbus/15-report-when-ready.lib.bash ================================================ pt_require_tools dbus-send x_dbus_begin pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/dbus/plugin-dbus.so', opts = { signals = { { object_path = '/org/luastatus/sample/object/name', interface = 'org.luastatus.ExampleInterface', bus = 'session', }, }, report_when_ready = true, }, cb = function(t) if t.what == 'signal' then assert(type(t.sender) == 'string') t.sender = '(non-deterministic)' end f:write('cb ' .. _fmt_x(t) .. '\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb {["what"]="ready"}' <&$pfd sleep 1 pt_check dbus-send \ --session \ /org/luastatus/sample/object/name \ org.luastatus.ExampleInterface.ExampleMethod \ int32:47 string:'hello world' double:63.125 \ array:string:"1st item","next item","last item" \ dict:string:int32:"one",1,"two",2,"three",3 \ variant:int32:-8 \ objpath:/org/luastatus/sample/object/name pt_expect_line 'cb {["bus"]="session",["interface"]="org.luastatus.ExampleInterface",["object_path"]="/org/luastatus/sample/object/name",["parameters"]={"47","hello world",63.1250,{"1st item","next item","last item"},{{"one","1"},{"two","2"},{"three","3"}},"-8","/org/luastatus/sample/object/name"},["sender"]="(non-deterministic)",["signal"]="ExampleMethod",["what"]="signal"}' <&$pfd pt_close_fd "$pfd" pt_testcase_end x_dbus_end ================================================ FILE: tests/pt_tests/plugin-dbus/skip-me-if.lib.bash ================================================ # It does not pass under TSAN and helgrind. Because glib is crazy or something like that. if [[ $PT_TOOL == helgrind ]]; then return $PT_SKIP_ME_YES fi if pt_cmake_opt_enabled WITH_TSAN; then return $PT_SKIP_ME_YES fi return $PT_SKIP_ME_NO ================================================ FILE: tests/pt_tests/plugin-disk-io-linux/00-common.lib.bash ================================================ pt_require_tools mktemp main_fifo_file=./tmp-fifo-main disk_io_linux_testcase() { local expect_str_1=$1 local expect_str_2=$2 local content_1=$3 local content_2=$4 pt_testcase_begin pt_add_fifo "$main_fifo_file" local proc_dir; proc_dir=$(mktemp -d) || pt_fail "'mktemp -d' failed" pt_add_dir_to_remove "$proc_dir" local diskstats_file=$proc_dir/diskstats pt_add_file_to_remove "$diskstats_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') function my_event_func() end x = dofile('$PT_SOURCE_DIR/plugins/disk-io-linux/disk-io-linux.lua') widget = x.widget{ _proc_path = '$proc_dir', cb = function(t) f:write('cb') table.sort(t, function(a, b) return a.name < b.name end) for _, x in ipairs(t) do f:write(string.format( " (%d,%d name=>%s r=>%d w=>%d)", x.num_major, x.num_minor, x.name, x.read_bytes, x.written_bytes )) end if #t == 0 then f:write(' ') end f:write('\n') end, event = my_event_func, } widget.plugin = ('$PT_BUILD_DIR/plugins/{}/plugin-{}.so'):gsub('{}', widget.plugin) assert(widget.event == my_event_func) __EOF__ printf '%s' "$content_1" > "$diskstats_file" || pt_fail "cannot write diskstats_file" pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line "$expect_str_1" <&$pfd printf '%s' "$content_2" > "$diskstats_file" || pt_fail "cannot write diskstats_file" pt_expect_line "$expect_str_2" <&$pfd pt_close_fd "$pfd" pt_testcase_end } ================================================ FILE: tests/pt_tests/plugin-disk-io-linux/01-simple.lib.bash ================================================ content1="\ 259 0 nvme0n1 137275 50367 21312112 79036 259141 59459 47106050 879950 0 101996 979461 0 0 0 0 5700 20474 259 1 nvme0n1p1 187 0 7679 88 10 0 10 17 0 100 106 0 0 0 0 0 0 259 2 nvme0n1p2 25027 8539 2421066 15822 3282 1336 210032 22593 0 17612 38416 0 0 0 0 0 0 259 3 nvme0n1p3 111967 41828 18880887 63111 255847 58123 46896008 857339 43 90680 920450 0 0 0 0 0 0 254 0 dm-0 153758 0 18880082 102332 313970 0 46896008 5605212 44 95076 5707544 0 0 0 0 0 0 " content2="\ 259 0 nvme0n1 141523 50367 22387160 81628 263894 59459 48322570 900083 0 102748 1002186 0 0 0 0 5700 20474 259 1 nvme0n1p1 187 0 7679 88 10 0 10 17 0 100 106 0 0 0 0 0 0 259 2 nvme0n1p2 25027 8539 2421066 15822 3282 1336 210032 22593 0 17612 38416 0 0 0 0 0 0 259 3 nvme0n1p3 116215 41828 19955935 65702 260600 58123 48112528 877472 74 91556 943175 0 0 0 0 0 0 254 0 dm-0 158006 0 19955130 105076 318723 0 48112528 5662204 75 96024 5767280 0 0 0 0 0 0 " awk_program=' BEGIN { SECTOR_SIZE = 512 } $0 != "" { disk_id = $1 "," $2 " " $3 r = $6 w = $10 if (!seen[disk_id]) { seen[disk_id] = 1 seen_r[disk_id] = r seen_w[disk_id] = w } else { delta_r = r - seen_r[disk_id] delta_w = w - seen_w[disk_id] print(disk_id, delta_r * SECTOR_SIZE, delta_w * SECTOR_SIZE) } }' expect_str= gen_expect_str() { expect_str='cb' local maj_min local name local delta_r local delta_w while read maj_min name delta_r delta_w; do expect_str+=" (${maj_min} name=>${name} r=>${delta_r} w=>${delta_w})" done < <( printf '%s\n%s\n' "${content1}${content2}" \ | awk "$awk_program" \ | LC_ALL=C sort -k2,2 ) } gen_expect_str disk_io_linux_testcase 'cb ' "$expect_str" "$content1" "$content2" ================================================ FILE: tests/pt_tests/plugin-file-contents-linux/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main ================================================ FILE: tests/pt_tests/plugin-file-contents-linux/01-simple.lib.bash ================================================ pt_require_tools mktemp pt_testcase_begin pt_add_fifo "$main_fifo_file" myfile=$(mktemp) || pt_fail 'mktemp failed' pt_add_file_to_remove "$myfile" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') function my_event_func() end x = dofile('$PT_SOURCE_DIR/plugins/file-contents-linux/file-contents-linux.lua') widget = x.widget{ filename = '$myfile', cb = function(myfile) local s = assert(myfile:read('*all')) f:write('update ' .. s:gsub('\n', ';') .. '\n') end, event = my_event_func, } widget.plugin = ('$PT_BUILD_DIR/plugins/{}/plugin-{}.so'):gsub('{}', widget.plugin) assert(widget.event == my_event_func) __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'update ' <&$pfd echo hello > "$myfile" pt_read_line <&$pfd pt_expect_line 'update hello;' <&$pfd echo bye >> "$myfile" pt_read_line <&$pfd pt_expect_line 'update hello;bye;' <&$pfd echo -n nonl > "$myfile" pt_read_line <&$pfd pt_expect_line 'update nonl' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-fs/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main wakeup_fifo_file=./tmp-fifo-wakeup preface=' local function _validate_t(t, ks) local function _assert_for_k(cond, k) if not cond then error(string.format("assertion failed for entry with key <%s> (see the stacktrace)", k)) end end local function _validate_num_for_k(n, k) _assert_for_k(type(n) == "number", k) _assert_for_k(n >= 0, k) _assert_for_k(n ~= math.huge, k) end for _, k in ipairs(ks) do local x = t[k] _assert_for_k(x ~= nil, k) _validate_num_for_k(x.total, k) _validate_num_for_k(x.free, k) _validate_num_for_k(x.avail, k) _assert_for_k(x.free <= x.total, k) _assert_for_k(x.avail <= x.total, k) t[k] = nil end local k = next(t) if k ~= nil then error(string.format("unexpected entry with key <%s>", k)) end end ' ================================================ FILE: tests/pt_tests/plugin-fs/01-default.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/fs/plugin-fs.so', cb = function(t) _validate_t(t, {}) f:write('cb\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-fs/02-root.lib.bash ================================================ pt_testcase_begin using_measure pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/fs/plugin-fs.so', opts = { paths = {'/'}, period = 0.25, }, cb = function(t) _validate_t(t, {'/'}) f:write('cb ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd measure_start pt_expect_line 'cb ok' <&$pfd measure_check_ms 0 pt_expect_line 'cb ok' <&$pfd measure_check_ms 250 pt_expect_line 'cb ok' <&$pfd measure_check_ms 250 pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-fs/03-glob-yesmatch.lib.bash ================================================ pt_require_tools mktemp pt_testcase_begin pt_add_fifo "$main_fifo_file" globtest_dir=$(mktemp -d) || pt_fail "'mktemp -d' failed" pt_add_dir_to_remove "$globtest_dir" for f in havoc alligator za all cucumber; do pt_check touch "$globtest_dir/$f" pt_add_file_to_remove "$globtest_dir/$f" done pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/fs/plugin-fs.so', opts = { globs = {'$globtest_dir/*?a*'}, }, cb = function(t) _validate_t(t, {'$globtest_dir/havoc', '$globtest_dir/alligator', '$globtest_dir/za'}) f:write('cb glob seems to work...\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb glob seems to work...' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-fs/04-glob-nomatch.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/fs/plugin-fs.so', opts = { globs = {'$globtest_dir/foo/bar/*', '$globtest_dir/*z*', '$globtest_dir/*'}, }, cb = function(t) _validate_t(t, {}) f:write('fine\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'fine' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-fs/05-bad-path.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/fs/plugin-fs.so', opts = { paths = {'$globtest_dir/foo/bar'}, }, cb = function(t) _validate_t(t, {}) f:write('good\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'good' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-fs/06-wakeup-fifo.lib.bash ================================================ pt_testcase_begin using_measure pt_add_fifo "$main_fifo_file" pt_add_fifo "$wakeup_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/fs/plugin-fs.so', opts = { paths = {'/'}, period = 0.5, fifo = '$wakeup_fifo_file', }, cb = function(t) _validate_t(t, {'/'}) f:write('cb called\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd measure_start pt_expect_line 'cb called' <&$pfd measure_check_ms 0 pt_expect_line 'cb called' <&$pfd measure_check_ms 500 pt_expect_line 'cb called' <&$pfd measure_check_ms 500 touch "$wakeup_fifo_file" pt_expect_line 'cb called' <&$pfd measure_check_ms 0 pt_expect_line 'cb called' <&$pfd measure_check_ms 500 pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-fs/07-dyn-paths.lib.bash ================================================ pt_require_tools mktemp pt_testcase_begin pt_add_fifo "$main_fifo_file" globtest_dir=$(mktemp -d) || pt_fail "'mktemp -d' failed" pt_add_dir_to_remove "$globtest_dir" for f in 1_one 2_two 3_three; do pt_check touch "$globtest_dir/$f" pt_add_file_to_remove "$globtest_dir/$f" done pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface local i = 1 local function check_eq(s1, s2) if s1 ~= s2 then error(string.format('check_eq: "%s" ~= "%s"', s1, s2)) end end local function add(k, expected_result) local path = '$globtest_dir/' .. k local result = luastatus.plugin.add_dyn_path(path) check_eq(result and 'true' or 'false', expected_result) end local function remove(k, expected_result) local path = '$globtest_dir/' .. k local result = luastatus.plugin.remove_dyn_path(path) check_eq(result and 'true' or 'false', expected_result) end local function check_keys(keys) if i == 1 then check_eq(keys, '') add('1_one', 'true') add('1_one', 'false') return true end if i == 2 then check_eq(keys, '1_one') add('2_two', 'true') add('2_two', 'false') return true end if i == 3 then check_eq(keys, '1_one 2_two') add('3_three', 'true') add('3_three', 'false') return true end if i == 4 then check_eq(keys, '1_one 2_two 3_three') remove('1_one', 'true') remove('1_one', 'false') return true end if i == 5 then check_eq(keys, '2_two 3_three') remove('2_two', 'true') remove('2_two', 'false') return true end if i == 6 then check_eq(keys, '3_three') remove('3_three', 'true') remove('3_three', 'false') return true end if i == 7 then check_eq(keys, '') return true end if i == 8 then local max = luastatus.plugin.get_max_dyn_paths() assert(max == 2147483647) return true end return false end local function basename(s) return assert(s:match('[^/]+$')) end widget = { plugin = '$PT_BUILD_DIR/plugins/fs/plugin-fs.so', opts = { enable_dyn_paths = true, period = 0.1, }, cb = function(t) local keys_tbl = {} for k, _ in pairs(t) do table.insert(keys_tbl, basename(k)) end table.sort(keys_tbl) local keys = table.concat(keys_tbl, ' ') if not check_keys(keys) then return end f:write(string.format('ok %d\n', i)) i = i + 1 end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd for (( i = 1; i <= 8; ++i )); do pt_expect_line "ok $i" <&$pfd done pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-imap/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main pt_find_free_tcp_port port=$PT_FOUND_FREE_PORT fakeimap_spawn() { pt_spawn_thing_pipe imap_parrot "$PT_PARROT" --reuseaddr --print-line-when-ready TCP-SERVER "$port" pt_expect_line 'parrot: ready' <&${PT_SPAWNED_THINGS_FDS_0[imap_parrot]} } fakeimap_spawn_check_accept_time_ms() { pt_spawn_thing_pipe imap_parrot "$PT_PARROT" --reuseaddr --print-line-when-ready --print-line-on-accept TCP-SERVER "$port" pt_expect_line 'parrot: ready' <&${PT_SPAWNED_THINGS_FDS_0[imap_parrot]} measure_start pt_expect_line 'parrot: accepted' <&${PT_SPAWNED_THINGS_FDS_0[imap_parrot]} measure_check_ms "$1" } fakeimap_read_line() { pt_read_line <&${PT_SPAWNED_THINGS_FDS_0[imap_parrot]} if [[ $PT_LINE != *$'\r' ]]; then pt_fail "fakeimap_read_line: line does not end with CRLF." "Line: '$PT_LINE'" fi PT_LINE=${PT_LINE%$'\r'} } fakeimap_expect() { fakeimap_read_line if [[ "$PT_LINE" != "$1" ]]; then pt_fail "fakeimap_expect: line does not match" "Expected: '$1'" "Found: '$PT_LINE'" fi } fakeimap_cmd_expect() { fakeimap_read_line if [[ "$PT_LINE" != *' '* ]]; then pt_fail "fakeimap_cmd_expect: expected command, found line without a space:" "'$PT_LINE'" fi fakeimap_prefix=${PT_LINE%%' '*} if [[ -z "$fakeimap_prefix" ]]; then pt_fail "fakeimap_cmd_expect: expected command, found line string with a space:" "'$PT_LINE'" fi local rest=${PT_LINE#*' '} if [[ "$rest" != "$1" ]]; then pt_fail "fakeimap_cmd_expect: line does not match" "Expected: '$1'" "Found: '$rest'" fi echo >&2 "[imap] Command prefix: '$fakeimap_prefix'." } fakeimap_cmd_done() { fakeimap_say "$fakeimap_prefix $1" } fakeimap_say() { printf '%s\r\n' "$1" >&${PT_SPAWNED_THINGS_FDS_1[imap_parrot]} } fakeimap_kill() { pt_kill_thing imap_parrot } fakeimap_wait() { pt_wait_thing imap_parrot || true } imap_interact_begin() { fakeimap_say "* OK IMAP4rev1 Service Ready" fakeimap_cmd_expect "LOGIN mylogin mypassword" fakeimap_cmd_done "OK LOGIN completed" fakeimap_cmd_expect "SELECT Inbox" fakeimap_say "* 18 EXISTS" fakeimap_say "* OK [UNSEEN 17] Message 17 is the first unseen message" fakeimap_cmd_done "OK [READ-WRITE] SELECT completed" } imap_interact_middle_simple() { local pfd=$1 local i for (( i = 17; i < 20; ++i )); do fakeimap_cmd_expect "STATUS Inbox (UNSEEN)" # outlook.com sends the '* STATUS' line with trailing whitespace. # See https://github.com/shdown/luastatus/issues/64 fakeimap_say "* STATUS Inbox (UNSEEN $i) " fakeimap_cmd_done "OK [READ-WRITE] SELECT completed" fakeimap_cmd_expect "IDLE" pt_expect_line "cb $i" <&$pfd fakeimap_say "* SOMETHING" fakeimap_expect "DONE" fakeimap_cmd_done "OK IDLE completed" done } imap_interact_middle_idle() { local pfd=$1 fakeimap_cmd_expect "STATUS Inbox (UNSEEN)" fakeimap_say "* STATUS Inbox (UNSEEN 135)" fakeimap_cmd_done "OK [READ-WRITE] SELECT completed" pt_expect_line "cb 135" <&$pfd fakeimap_cmd_expect "IDLE" } ================================================ FILE: tests/pt_tests/plugin-imap/01-simple.lib.bash ================================================ pt_testcase_begin fakeimap_spawn pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') x = dofile('$PT_SOURCE_DIR/plugins/imap/imap.lua') widget = x.widget{ host = 'localhost', port = $port, login = 'mylogin', password = 'mypassword', mailbox = 'Inbox', error_sleep_period = 1, cb = function(n) if n == nil then f:write('cb nil\n') else f:write(string.format('cb %d\n', n)) end end, event = my_event_func, } widget.plugin = ('$PT_BUILD_DIR/plugins/{}/plugin-{}.so'):gsub('{}', widget.plugin) assert(widget.event == my_event_func) __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb nil' <&$pfd imap_interact_begin imap_interact_middle_simple "$pfd" fakeimap_kill pt_expect_line 'cb nil' <&$pfd pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-imap/02-retry-timeout.lib.bash ================================================ pt_testcase_begin using_measure fakeimap_spawn pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') x = dofile('$PT_SOURCE_DIR/plugins/imap/imap.lua') widget = x.widget{ host = 'localhost', port = $port, login = 'mylogin', password = 'mypassword', mailbox = 'Inbox', error_sleep_period = 1, cb = function(n) if n == nil then f:write('cb nil\n') else f:write(string.format('cb %d\n', n)) end end, event = my_event_func, } widget.plugin = ('$PT_BUILD_DIR/plugins/{}/plugin-{}.so'):gsub('{}', widget.plugin) assert(widget.event == my_event_func) __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb nil' <&$pfd for (( i = 0; i < 3; ++i )); do imap_interact_begin imap_interact_middle_simple "$pfd" fakeimap_kill pt_expect_line 'cb nil' <&$pfd fakeimap_spawn_check_accept_time_ms 1000 done fakeimap_kill pt_expect_line 'cb nil' <&$pfd pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-imap/03-idle-timeout.lib.bash ================================================ pt_testcase_begin using_measure fakeimap_spawn pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') x = dofile('$PT_SOURCE_DIR/plugins/imap/imap.lua') widget = x.widget{ host = 'localhost', port = $port, login = 'mylogin', password = 'mypassword', mailbox = 'Inbox', error_sleep_period = 1, timeout = 1.5, cb = function(n) if n == nil then f:write('cb nil\n') else f:write(string.format('cb %d\n', n)) end end, event = my_event_func, } widget.plugin = ('$PT_BUILD_DIR/plugins/{}/plugin-{}.so'):gsub('{}', widget.plugin) assert(widget.event == my_event_func) __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb nil' <&$pfd for (( i = 0; i < 3; ++i )); do imap_interact_begin imap_interact_middle_idle "$pfd" measure_start fakeimap_wait measure_check_ms 1500 pt_expect_line 'cb nil' <&$pfd fakeimap_spawn_check_accept_time_ms 1000 done fakeimap_kill pt_expect_line 'cb nil' <&$pfd pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-inotify/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main preface=' local function _fmt_mask(m) local ks = {} for k, _ in pairs(m) do ks[#ks + 1] = k end table.sort(ks) return table.concat(ks, ",") end ' ================================================ FILE: tests/pt_tests/plugin-inotify/01-simple.lib.bash ================================================ pt_require_tools mktemp pt_testcase_begin pt_add_fifo "$main_fifo_file" myfile=$(mktemp) || pt_fail 'mktemp failed' pt_add_file_to_remove "$myfile" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/inotify/plugin-inotify.so', opts = { watch = {['$myfile'] = {'close_write', 'delete_self'}}, }, cb = function(t) assert(t.what == 'event', 'unexpected t.what') f:write('cb event mask=' .. _fmt_mask(t.mask) .. '\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd # Try to avoid race condition: we may modify the file before the watch is set up. sleep 1 echo hello >> "$myfile" || pt_fail 'cannot append to myfile' pt_expect_line 'cb event mask=close_write' <&$pfd rm -f "$myfile" || pt_fail 'cannot remove myfile' pt_expect_line 'cb event mask=delete_self' <&$pfd pt_expect_line 'cb event mask=ignored' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-inotify/02-lfunc-get-initial-wds.lib.bash ================================================ pt_require_tools mktemp pt_testcase_begin pt_add_fifo "$main_fifo_file" myfile1=$(mktemp) || pt_fail 'mktemp failed' myfile2=$(mktemp) || pt_fail 'mktemp failed' pt_add_file_to_remove "$myfile1" pt_add_file_to_remove "$myfile2" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/inotify/plugin-inotify.so', opts = { watch = { ['$myfile1'] = {'close_write', 'delete_self'}, ['$myfile2'] = {'delete_self'}, }, greet = true, }, cb = function(t) if t.what == 'hello' then f:write('cb hello\n') else assert(t.what == 'event', 'unexpected t.what') assert(type(t.wd) == 'number', 't.wd is not a number') local file_ids = { ['$myfile1'] = '1', ['$myfile2'] = '2', } local wds = luastatus.plugin.get_initial_wds() local file_id for k, v in pairs(wds) do local id = file_ids[k] assert(id ~= nil, string.format('unexpected key in initial wds: "%s"', k)) if v == t.wd then file_id = id end end assert(file_id ~= nil, 't.wd is not present in initial wds') f:write('cb event file' .. file_id .. ' mask=' .. _fmt_mask(t.mask) .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb hello' <&$pfd echo hello >> "$myfile2" echo hello >> "$myfile1" pt_expect_line 'cb event file1 mask=close_write' <&$pfd rm -f "$myfile2" pt_expect_line 'cb event file2 mask=delete_self' <&$pfd pt_expect_line 'cb event file2 mask=ignored' <&$pfd rm -f "$myfile1" pt_expect_line 'cb event file1 mask=delete_self' <&$pfd pt_expect_line 'cb event file1 mask=ignored' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-inotify/03-lfunc-add-watch.lib.bash ================================================ pt_require_tools mktemp pt_testcase_begin pt_add_fifo "$main_fifo_file" myfile=$(mktemp) || pt_fail 'mktemp failed' pt_add_file_to_remove "$myfile" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/inotify/plugin-inotify.so', opts = { watch = {}, greet = true, }, cb = function(t) local line if t.what == 'hello' then line = 'cb hello' else assert(t.what == 'event') line = 'cb event mask=' .. _fmt_mask(t.mask) end local wd = luastatus.plugin.add_watch('$myfile', {'close_write', 'delete_self', 'oneshot'}) if wd then f:write(line .. ' (add_watch: ok)\n') else f:write(line .. ' (add_watch: error)\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb hello (add_watch: ok)' <&$pfd for (( i = 0; i < 5; ++i )); do echo hello >> "$myfile" pt_expect_line 'cb event mask=close_write (add_watch: ok)' <&$pfd pt_expect_line 'cb event mask=ignored (add_watch: ok)' <&$pfd done rm -f "$myfile" pt_expect_line 'cb event mask=delete_self (add_watch: error)' <&$pfd pt_expect_line 'cb event mask=ignored (add_watch: error)' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-inotify/04-lfunuc-rm-watch.lib.bash ================================================ pt_require_tools mktemp pt_testcase_begin pt_add_fifo "$main_fifo_file" myfile=$(mktemp) || pt_fail 'mktemp failed' pt_add_file_to_remove "$myfile" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface wd = nil widget = { plugin = '$PT_BUILD_DIR/plugins/inotify/plugin-inotify.so', opts = { watch = {}, greet = true, timeout = 0.25, }, cb = function(t) local initial_wds = luastatus.plugin.get_initial_wds() assert(next(initial_wds) == nil, 'get_initial_wds() result is not empty') if t.what == 'hello' then wd = luastatus.plugin.add_watch('$myfile', {'close_write'}) assert(wd, 'add_watch() failed') f:write('cb hello\n') elseif t.what == 'event' then assert(wd ~= nil, 'global "wd" is nil') local line = 'cb event mask=' .. _fmt_mask(t.mask) local is_ok = luastatus.plugin.remove_watch(wd) if is_ok then f:write(line .. ' (remove_watch: ok)\n') else f:write(line .. ' (remove_watch: error)\n') end else f:write('cb ' .. t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb hello' <&$pfd pt_expect_line 'cb timeout' <&$pfd echo hello >> "$myfile" pt_expect_line 'cb event mask=close_write (remove_watch: ok)' <&$pfd pt_expect_line 'cb event mask=ignored (remove_watch: error)' <&$pfd echo hello >> "$myfile" pt_expect_line 'cb timeout' <&$pfd echo hello >> "$myfile" pt_expect_line 'cb timeout' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-inotify/05-timeout.lib.bash ================================================ pt_require_tools mktemp pt_testcase_begin using_measure pt_add_fifo "$main_fifo_file" myfile=$(mktemp) || pt_fail 'mktemp failed' pt_add_file_to_remove "$myfile" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/inotify/plugin-inotify.so', opts = { watch = {['$myfile'] = {'close_write', 'delete_self'}}, timeout = 0.3, greet = true, }, cb = function(t) if t.what == 'event' then f:write('cb event mask=' .. _fmt_mask(t.mask) .. '\n') else f:write('cb ' .. t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb hello' <&$pfd measure_start pt_expect_line 'cb timeout' <&$pfd measure_check_ms 300 pt_expect_line 'cb timeout' <&$pfd measure_check_ms 300 echo hello >> "$myfile" pt_expect_line 'cb event mask=close_write' <&$pfd measure_check_ms 0 pt_expect_line 'cb timeout' <&$pfd measure_check_ms 300 echo hello >> "$myfile" pt_expect_line 'cb event mask=close_write' <&$pfd measure_check_ms 0 rm -f "$myfile" pt_expect_line 'cb event mask=delete_self' <&$pfd pt_expect_line 'cb event mask=ignored' <&$pfd measure_check_ms 0 pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-inotify/06-dir.lib.bash ================================================ pt_require_tools mktemp stage_dir=$(mktemp -d) || pt_fail "'mktemp -d' failed" stage_subdir=$stage_dir/subdir actor_file_1=$stage_dir/foo1 actor_file_2=$stage_dir/foo2 pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_add_file_to_remove "$actor_file_1" pt_add_file_to_remove "$actor_file_2" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface _cookies = {} _cookies_cnt = 0 function _fmt_cookie(c) assert(type(c) == 'number', 'cookie is not a number') if c == 0 then return '(none)' end local r = _cookies[c] if r == nil then _cookies_cnt = _cookies_cnt + 1 r = string.format('(cookie #%d)', _cookies_cnt) _cookies[c] = r end return r end widget = { plugin = '$PT_BUILD_DIR/plugins/inotify/plugin-inotify.so', opts = { watch = {['$stage_dir'] = {'close_write', 'delete', 'moved_from', 'moved_to'}}, greet = true, }, cb = function(t) if t.what == 'hello' then f:write('cb hello\n') else assert(t.what == 'event', 'unexpected t.what') f:write(string.format('cb event mask=%s cookie=%s name=%s\n', _fmt_mask(t.mask), _fmt_cookie(t.cookie), t.name or '(nil)')) end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb hello' <&$pfd echo hello >> "$actor_file_1" pt_expect_line 'cb event mask=close_write cookie=(none) name=foo1' <&$pfd mv -f "$actor_file_1" "$actor_file_2" pt_expect_line 'cb event mask=moved_from cookie=(cookie #1) name=foo1' <&$pfd pt_expect_line 'cb event mask=moved_to cookie=(cookie #1) name=foo2' <&$pfd rm -f "$actor_file_2" pt_expect_line 'cb event mask=delete cookie=(none) name=foo2' <&$pfd mkdir "$stage_subdir" rmdir "$stage_subdir" pt_expect_line 'cb event mask=delete,isdir cookie=(none) name=subdir' <&$pfd rmdir "$stage_dir" pt_expect_line 'cb event mask=ignored cookie=(none) name=(nil)' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-inotify/07-lfunc-push-timeout.lib.bash ================================================ pt_require_tools mktemp pt_testcase_begin using_measure pt_add_fifo "$main_fifo_file" myfile=$(mktemp) || pt_fail "'mktemp -d' failed" pt_add_file_to_remove "$myfile" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') n=0 widget = { plugin = '$PT_BUILD_DIR/plugins/inotify/plugin-inotify.so', opts = { watch = {['$myfile'] = {'close_write'}}, greet = true, timeout = 0.1, }, cb = function(t) if t.what == 'event' then f:write('event\n') else n=(n+1)%3 f:write(t.what .. ' n=' .. n .. '\n') if n == 0 then luastatus.plugin.push_timeout(0.6) end end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'hello n=1' <&$pfd measure_start for (( i = 0; i < 4; ++i )); do echo bye >> "$myfile" pt_expect_line "event" <&$pfd measure_check_ms 0 pt_expect_line 'timeout n=2' <&$pfd measure_check_ms 100 pt_expect_line 'timeout n=0' <&$pfd measure_check_ms 100 pt_expect_line 'timeout n=1' <&$pfd measure_check_ms 600 done pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-inotify/08-lfunc-get-supported-events.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') function check_events(e) assert(type(e) == 'table') -- These are supported by any glibc version. assert(e.access == 'io') assert(e.move == 'i') assert(e.isdir == 'o') -- Check sanity (all values are either "i", "o" or "io"). for k, v in pairs(e) do print(string.format('Supported event: %s (%s)', k, v)) assert(v == 'i' or v == 'o' or v == 'io') end end widget = { plugin = '$PT_BUILD_DIR/plugins/inotify/plugin-inotify.so', opts = { watch = {}, greet = true, }, cb = function(t) assert(t.what == 'hello') check_events(luastatus.plugin.get_supported_events()) f:write('cb ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb ok' <&$pfd pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-inotify/09-access.lib.bash ================================================ pt_testcase_begin using_measure pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') function call_access(path) local ok, err = luastatus.plugin.access(path) if ok then f:write('exists = true\n') else if err then f:write('error ' .. err .. '\n') else f:write('exists = false\n') end end end widget = { plugin = '$PT_BUILD_DIR/plugins/inotify/plugin-inotify.so', opts = { watch = {}, timeout = 1, greet = true, }, cb = function(t) call_access('/') call_access('/k4dsqflkr8ntrpwagmw7') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'exists = true' <&$pfd pt_expect_line 'exists = false' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-is-program-running/00-common.lib.bash ================================================ pt_require_tools mktemp main_fifo_file=./tmp-fifo-main is_program_running_testcase() { local opts=${1?}; shift local callback=${1?}; shift pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') function my_event_func() end x = dofile('$PT_SOURCE_DIR/plugins/is-program-running/is-program-running.lua') function format_result_entry(x) return string.format('%s=>%s', x[1], tostring(x[2])) end function format_result(t) local chunks = {} local arr = {} if type(next(t)) == 'number' then for i, v in ipairs(t) do table.insert(arr, {i, v}) end table.sort(arr, function(PQ_x, PQ_y) return PQ_x[1] < PQ_y[1] end) else for k, v in pairs(t) do table.insert(arr, {k, v}) end end for _, x in ipairs(arr) do table.insert(chunks, format_result_entry(x)) end return table.concat(chunks, ' ') end widget = x.widget{ timer_opts = { period = 0.1, }, cb = function(t) if type(t) == 'table' then f:write('cb ' .. format_result(t) .. '\n') else f:write('cb ' .. tostring(t) .. '\n') end end, event = my_event_func, $opts } widget.plugin = ('$PT_BUILD_DIR/plugins/{}/plugin-{}.so'):gsub('{}', widget.plugin) assert(widget.event == my_event_func) __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd local i for (( i = 0; i < $#; ++i )); do pt_check $callback $i local j=$(( i + 1 )) pt_expect_line "${!j}" <&$pfd done pt_close_fd "$pfd" pt_testcase_end } x_do_nothing() { true } ================================================ FILE: tests/pt_tests/plugin-is-program-running/01-pidfile-devnull.lib.bash ================================================ is_program_running_testcase \ 'kind="pidfile", path="/dev/null"' \ x_do_nothing \ 'cb false' \ 'cb false' ================================================ FILE: tests/pt_tests/plugin-is-program-running/02-pidfile.lib.bash ================================================ pidfile=$(mktemp) || pt_fail 'mktemp failed' pt_add_file_to_remove "$pidfile" write_pidfile() { printf '%s\n' "$1" > "$pidfile" || pt_fail 'cannot write $pidfile' } x_my_callback() { case "$1" in 0) write_pidfile junk > "$pidfile" ;; 1) write_pidfile "$$" > "$pidfile" ;; 2) write_pidfile 0 > "$pidfile" ;; 3) write_pidfile 1 > "$pidfile" ;; 4) write_pidfile -1 > "$pidfile" ;; 5) true > "$pidfile" || pt_fail 'cannot write $pidfile' ;; esac } is_program_running_testcase \ "kind='pidfile', path='$pidfile'" \ x_my_callback \ 'cb false' \ 'cb true' \ 'cb false' \ 'cb true' \ 'cb false' \ 'cb false' ================================================ FILE: tests/pt_tests/plugin-is-program-running/03-pidfile-fileerr.lib.bash ================================================ is_program_running_testcase \ "kind='pidfile', path='/634Zb012x1g7f5ages2snA1stxdlpr'" \ x_do_nothing \ 'cb false' \ 'cb false' ================================================ FILE: tests/pt_tests/plugin-is-program-running/04-fileexists.lib.bash ================================================ is_program_running_testcase \ 'kind="file_exists", path="/"' \ x_do_nothing \ 'cb true' tmpfile=$(mktemp) || pt_fail 'mktemp failed' pt_add_file_to_remove "$tmpfile" x_my_callback_del_tmpfile() { if (( $1 == 1 )); then pt_check rm -- "$tmpfile" fi } is_program_running_testcase \ "kind='file_exists', path='$tmpfile'" \ x_my_callback_del_tmpfile \ 'cb true' \ 'cb false' \ ================================================ FILE: tests/pt_tests/plugin-is-program-running/05-dirnonempty-root.lib.bash ================================================ is_program_running_testcase \ 'kind="dir_nonempty", path="/"' \ x_do_nothing \ 'cb true' ================================================ FILE: tests/pt_tests/plugin-is-program-running/06-dirnonempty-normal-files.lib.bash ================================================ tmpdir=$(mktemp -d) || pt_fail 'mktemp -d failed' pt_add_file_to_remove "$tmpdir"/.hidden pt_add_file_to_remove "$tmpdir"/normal pt_add_dir_to_remove "$tmpdir" x_my_callback_dirnonempty() { case "$1" in 0) ;; 1) pt_check touch "$tmpdir"/.hidden ;; 2) pt_check touch "$tmpdir"/normal ;; 3) pt_check rm -- "$tmpdir"/normal ;; esac } is_program_running_testcase \ "kind='dir_nonempty', path='$tmpdir'" \ x_my_callback_dirnonempty \ 'cb false' \ 'cb false' \ 'cb true' \ 'cb false' ================================================ FILE: tests/pt_tests/plugin-is-program-running/07-dirnonempty-nonexisting.lib.bash ================================================ nonexisting_dir=/tzmicdb8kf38onA67olhZiyd79xml3 # Without trailing '/' is_program_running_testcase \ "kind='dir_nonempty', path='${nonexisting_dir}'" \ x_do_nothing \ 'cb false' # With trailing '/' is_program_running_testcase \ "kind='dir_nonempty', path='${nonexisting_dir}/'" \ x_do_nothing \ 'cb false' ================================================ FILE: tests/pt_tests/plugin-is-program-running/08-dirnonempty-with-hidden.lib.bash ================================================ tmpdir=$(mktemp -d) || pt_fail 'mktemp -d failed' pt_add_file_to_remove "$tmpdir"/.hidden pt_add_file_to_remove "$tmpdir"/normal pt_add_dir_to_remove "$tmpdir" x_my_callback_dirnonempty_with_hidden() { case "$1" in 0) ;; 1) pt_check touch "$tmpdir"/.hidden ;; 2) pt_check touch "$tmpdir"/normal ;; 3) pt_check rm -- "$tmpdir"/normal ;; 4) pt_check rm -- "$tmpdir"/.hidden ;; esac echo "=== After $i: ===" >&2 find "$tmpdir" >&2 echo "=================" >&2 } is_program_running_testcase \ "kind='dir_nonempty_with_hidden', path='$tmpdir'" \ x_my_callback_dirnonempty_with_hidden \ 'cb false' \ 'cb true' \ 'cb true' \ 'cb true' \ 'cb false' ================================================ FILE: tests/pt_tests/plugin-mem-usage-linux/00-common.lib.bash ================================================ pt_require_tools mktemp main_fifo_file=./tmp-fifo-main mem_usage_testcase() { local expect_str=$1 local meminfo_content=$2 pt_testcase_begin pt_add_fifo "$main_fifo_file" local proc_dir; proc_dir=$(mktemp -d) || pt_fail "'mktemp -d' failed" pt_add_dir_to_remove "$proc_dir" local meminfo_file=$proc_dir/meminfo printf '%s' "$meminfo_content" > "$meminfo_file" || pt_fail 'cannot write meminfo_file' pt_add_file_to_remove "$meminfo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') function my_event_func() end x = dofile('$PT_SOURCE_DIR/plugins/mem-usage-linux/mem-usage-linux.lua') widget = x.widget{ _procpath = '$proc_dir', cb = function(t) f:write(string.format('cb avail=%s(%s) total=%s(%s)\n', t.avail.value, t.avail.unit, t.total.value, t.total.unit)) end, event = my_event_func, } widget.plugin = ('$PT_BUILD_DIR/plugins/{}/plugin-{}.so'):gsub('{}', widget.plugin) assert(widget.event == my_event_func) __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line "$expect_str" <&$pfd pt_close_fd "$pfd" pt_testcase_end } ================================================ FILE: tests/pt_tests/plugin-mem-usage-linux/01-simple.lib.bash ================================================ mem_usage_testcase 'cb avail=1527556(kB) total=3877576(kB)' "\ MemTotal: 3877576 kB MemFree: 774760 kB MemAvailable: 1527556 kB Buffers: 178504 kB Cached: 1263372 kB SwapCached: 0 kB Active: 458264 kB Inactive: 1938980 kB Active(anon): 5360 kB Inactive(anon): 1489156 kB Active(file): 452904 kB Inactive(file): 449824 kB Unevictable: 394212 kB Mlocked: 144 kB SwapTotal: 0 kB SwapFree: 0 kB Dirty: 108 kB Writeback: 0 kB AnonPages: 1259188 kB Mapped: 336640 kB Shmem: 539148 kB KReclaimable: 115248 kB Slab: 212356 kB SReclaimable: 115248 kB SUnreclaim: 97108 kB KernelStack: 6976 kB PageTables: 26212 kB NFS_Unstable: 0 kB Bounce: 0 kB WritebackTmp: 0 kB CommitLimit: 1938788 kB Committed_AS: 6041840 kB VmallocTotal: 34359738367 kB VmallocUsed: 27600 kB VmallocChunk: 0 kB Percpu: 2208 kB HardwareCorrupted: 0 kB AnonHugePages: 444416 kB ShmemHugePages: 0 kB ShmemPmdMapped: 0 kB FileHugePages: 0 kB FilePmdMapped: 0 kB HugePages_Total: 0 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB Hugetlb: 0 kB DirectMap4k: 836220 kB DirectMap2M: 3203072 kB DirectMap1G: 0 kB " ================================================ FILE: tests/pt_tests/plugin-mpd/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main retry_fifo_file=./tmp-fifo-mpd-retry pt_find_free_tcp_port port=$PT_FOUND_FREE_PORT preface=' local function _fmt_kv(m) local ks = {} for k, v in pairs(m) do ks[#ks + 1] = k end table.sort(ks) local s = {} for _, k in ipairs(ks) do s[#s + 1] = string.format("%s=>%s", k, m[k]) end return string.format("{%s}", table.concat(s, ",")) end ' fakempd_spawn() { pt_spawn_thing_pipe mpd_parrot "$PT_PARROT" --reuseaddr --print-line-when-ready TCP-SERVER "$port" pt_expect_line 'parrot: ready' <&${PT_SPAWNED_THINGS_FDS_0[mpd_parrot]} } fakempd_spawn_unixsocket() { pt_spawn_thing_pipe mpd_parrot "$PT_PARROT" --reuseaddr --print-line-when-ready UNIX-SERVER "$1" pt_expect_line 'parrot: ready' <&${PT_SPAWNED_THINGS_FDS_0[mpd_parrot]} } fakempd_expect() { pt_expect_line "$1" <&${PT_SPAWNED_THINGS_FDS_0[mpd_parrot]} } fakempd_say() { printf '%s\n' "$1" >&${PT_SPAWNED_THINGS_FDS_1[mpd_parrot]} } fakempd_kill() { pt_kill_thing mpd_parrot } fakempd_wait() { pt_wait_thing mpd_parrot || true } ================================================ FILE: tests/pt_tests/plugin-mpd/01-bad-answer.lib.bash ================================================ pt_testcase_begin fakempd_spawn pt_write_widget_file <<__EOF__ widget = { plugin = '$PT_BUILD_DIR/plugins/mpd/plugin-mpd.so', opts = { port = $port, retry_in = -1, }, cb = function(t) end, } __EOF__ pt_spawn_luastatus -e fakempd_say "I'm not music player daemon, huh." pt_wait_luastatus || pt_fail "luastatus exited with non-zero exit code $?" fakempd_wait pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-mpd/02-simple-template.lib.bash ================================================ x_mpd_simple_template() { local bind_params=$1 local expect_error_in_beginning=$2 local enable_tcp_keepalive=${3:-0} local extra_opts= if [[ -n "$bind_params" ]]; then local bind_ipver=${bind_params%%/*} local bind_addr=${bind_params#*/} extra_opts="bind = {addr='$bind_addr', ipver='$bind_ipver'}," fi if (( enable_tcp_keepalive )); then extra_opts+='enable_tcp_keepalive = true,' fi fakempd_spawn pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/mpd/plugin-mpd.so', opts = { port = $port, $extra_opts }, cb = function(t) if t.what == 'update' then f:write(string.format('cb update song=%s status=%s\n', _fmt_kv(t.song), _fmt_kv(t.status))) else f:write('cb ' .. t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb connecting' <&$pfd if (( ! $expect_error_in_beginning )); then fakempd_say "OK MPD I-am-actually-a-shell-script" for (( i = 0; i < 3; ++i )); do fakempd_expect 'currentsong' fakempd_say 'Song_Foo: bar' fakempd_say 'Song_Baz: quiz' fakempd_say 'OK' fakempd_expect 'status' fakempd_say 'Status_One: ein' fakempd_say 'Status_Two: zwei' fakempd_say 'Status_Three: drei' fakempd_say "Z: $i" fakempd_say 'OK' fakempd_expect 'idle mixer player' pt_expect_line "cb update song={Song_Baz=>quiz,Song_Foo=>bar} status={Status_One=>ein,Status_Three=>drei,Status_Two=>zwei,Z=>$i}" <&$pfd fakempd_say 'OK' done # First kill fake mpd, then expect an "error" line fakempd_kill pt_expect_line 'cb error' <&$pfd else # First expect an "error" line, then kill fake mpd. pt_expect_line 'cb error' <&$pfd fakempd_kill fi } ================================================ FILE: tests/pt_tests/plugin-mpd/03-simple.lib.bash ================================================ pt_testcase_begin x_mpd_simple_template '' 0 pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-mpd/04-simple-bind.lib.bash ================================================ pt_testcase_begin x_mpd_simple_template ipv4/'127.0.0.1' 0 pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-mpd/05-simple-bind-invalid.lib.bash ================================================ pt_testcase_begin # RFC 5737: "The blocks 192.0.2.0/24 (TEST-NET-1), 198.51.100.0/24 (TEST-NET-2), # and 203.0.113.0/24 (TEST-NET-3) are provided for use in documentation." # # So 192.0.2.0 is a "sort of invalid" IPv4 address. x_mpd_simple_template ipv4/'192.0.2.0' 1 pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-mpd/06-simple-bind-invalid-ipv6.lib.bash ================================================ pt_testcase_begin # This is a malformed IPv4-mapped IPv6 address. # It should fail regardless of whether or not the system we are running on supports IPv6. x_mpd_simple_template ipv6/'::ffff:256.255.255.255' 1 pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-mpd/07-keepalive.lib.bash ================================================ pt_testcase_begin x_mpd_simple_template '' 0 1 pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-mpd/08-retry-in.lib.bash ================================================ pt_testcase_begin using_measure fakempd_spawn pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/mpd/plugin-mpd.so', opts = { port = $port, retry_in = 1.25, }, cb = function(t) if t.what == 'update' then f:write(string.format('cb update song=%s status=%s\n', _fmt_kv(t.song), _fmt_kv(t.status))) else f:write('cb ' .. t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb connecting' <&$pfd for (( i = 0; i < 3; ++i )); do fakempd_say "OK MPD I-am-actually-a-shell-script" fakempd_expect 'currentsong' fakempd_say "Song_Foo: bar" fakempd_say "Song_Baz: quiz" fakempd_say "OK" fakempd_expect 'status' fakempd_say "Status_One: ein" fakempd_say "Status_Two: zwei" fakempd_say "Status_Three: drei" fakempd_say "Z: $i" fakempd_say "OK" fakempd_expect 'idle mixer player' pt_expect_line "cb update song={Song_Baz=>quiz,Song_Foo=>bar} status={Status_One=>ein,Status_Three=>drei,Status_Two=>zwei,Z=>$i}" <&$pfd fakempd_kill pt_expect_line 'cb error' <&$pfd fakempd_spawn measure_start pt_expect_line 'cb connecting' <&$pfd measure_check_ms 1250 done fakempd_kill pt_expect_line 'cb error' <&$pfd pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-mpd/09-retry-fifo.lib.bash ================================================ pt_testcase_begin using_measure fakempd_spawn pt_add_fifo "$main_fifo_file" pt_add_fifo "$retry_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/mpd/plugin-mpd.so', opts = { port = $port, hostname = '127.0.0.1', retry_in = 1, retry_fifo = '$retry_fifo_file', }, cb = function(t) if t.what == 'update' then f:write(string.format('cb update song=%s status=%s\n', _fmt_kv(t.song), _fmt_kv(t.status))) else f:write('cb ' .. t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb connecting' <&$pfd for (( i = 0; i < 6; ++i )); do fakempd_say "OK MPD I-am-actually-a-shell-script" fakempd_expect 'currentsong' fakempd_say "Song_Foo: bar" fakempd_say "Song_Baz: quiz" fakempd_say "OK" fakempd_expect 'status' fakempd_say "Status_One: ein" fakempd_say "Status_Two: zwei" fakempd_say "Status_Three: drei" fakempd_say "Z: $i" fakempd_say "OK" fakempd_expect 'idle mixer player' pt_expect_line "cb update song={Song_Baz=>quiz,Song_Foo=>bar} status={Status_One=>ein,Status_Three=>drei,Status_Two=>zwei,Z=>$i}" <&$pfd fakempd_kill pt_expect_line 'cb error' <&$pfd fakempd_spawn if (( i % 2 == 0 )); then measure_start pt_expect_line 'cb connecting' <&$pfd measure_check_ms 1000 else touch "$retry_fifo_file" measure_start pt_expect_line 'cb connecting' <&$pfd measure_check_ms 0 fi done fakempd_kill pt_expect_line 'cb error' <&$pfd pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-mpd/10-timeout.lib.bash ================================================ pt_testcase_begin using_measure fakempd_spawn pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/mpd/plugin-mpd.so', opts = { port = $port, timeout = 0.25, }, cb = function(t) if t.what == 'update' then f:write(string.format('cb update song=%s status=%s\n', _fmt_kv(t.song), _fmt_kv(t.status))) else f:write('cb ' .. t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb connecting' <&$pfd fakempd_say "OK MPD I-am-actually-a-shell-script" for (( i = 0; i < 3; ++i )); do fakempd_expect 'currentsong' fakempd_say "Song_Foo: bar" fakempd_say "Song_Baz: quiz" fakempd_say "OK" fakempd_expect 'status' fakempd_say "Status_One: ein" fakempd_say "Status_Two: zwei" fakempd_say "Status_Three: drei" fakempd_say "Z: $i" fakempd_say "OK" fakempd_expect 'idle mixer player' measure_start pt_expect_line "cb update song={Song_Baz=>quiz,Song_Foo=>bar} status={Status_One=>ein,Status_Three=>drei,Status_Two=>zwei,Z=>$i}" <&$pfd measure_check_ms 0 pt_expect_line "cb timeout" <&$pfd measure_check_ms 250 pt_expect_line "cb timeout" <&$pfd measure_check_ms 250 fakempd_say "OK" done fakempd_kill pt_expect_line 'cb error' <&$pfd pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-mpd/11-password-and-events.lib.bash ================================================ pt_testcase_begin fakempd_spawn pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/mpd/plugin-mpd.so', opts = { port = $port, hostname = 'localhost', password = 'qwerty123456', events = {'my', 'custom', 'events', 'string'}, }, cb = function(t) if t.what == 'update' then f:write(string.format('cb update song=%s status=%s\n', _fmt_kv(t.song), _fmt_kv(t.status))) else f:write('cb ' .. t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb connecting' <&$pfd fakempd_say "OK MPD I-am-actually-a-shell-script" fakempd_expect 'password "qwerty123456"' fakempd_say "OK" for (( i = 0; i < 3; ++i )); do fakempd_expect "currentsong" fakempd_say "Song_Foo: bar" fakempd_say "Song_Baz: quiz" fakempd_say "OK" fakempd_expect "status" fakempd_say "Status_One: ein" fakempd_say "Status_Two: zwei" fakempd_say "Status_Three: drei" fakempd_say "Z: $i" fakempd_say "OK" fakempd_expect "idle my custom events string" pt_expect_line "cb update song={Song_Baz=>quiz,Song_Foo=>bar} status={Status_One=>ein,Status_Three=>drei,Status_Two=>zwei,Z=>$i}" <&$pfd fakempd_say "OK" done fakempd_kill pt_expect_line 'cb error' <&$pfd pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-mpd/12-connect-via-unix-socket.lib.bash ================================================ pt_testcase_begin socket_file=$PWD/tmp-fake-mpd-socket rm -f "$socket_file" pt_add_file_to_remove "$socket_file" fakempd_spawn_unixsocket "$socket_file" pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/mpd/plugin-mpd.so', opts = { hostname = '$socket_file', }, cb = function(t) if t.what == 'update' then f:write(string.format('cb update song=%s status=%s\n', _fmt_kv(t.song), _fmt_kv(t.status))) else f:write('cb ' .. t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb connecting' <&$pfd fakempd_say "OK MPD I-am-actually-a-shell-script" for (( i = 0; i < 3; ++i )); do fakempd_expect 'currentsong' fakempd_say "Song_Foo: bar" fakempd_say "Song_Baz: quiz" fakempd_say "OK" fakempd_expect 'status' fakempd_say "Status_One: ein" fakempd_say "Status_Two: zwei" fakempd_say "Status_Three: drei" fakempd_say "Z: $i" fakempd_say "OK" fakempd_expect 'idle mixer player' pt_expect_line "cb update song={Song_Baz=>quiz,Song_Foo=>bar} status={Status_One=>ein,Status_Three=>drei,Status_Two=>zwei,Z=>$i}" <&$pfd fakempd_say "OK" done fakempd_kill pt_expect_line 'cb error' <&$pfd pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-mpd/13-bad-password.lib.bash ================================================ pt_testcase_begin fakempd_spawn pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') $preface widget = { plugin = '$PT_BUILD_DIR/plugins/mpd/plugin-mpd.so', opts = { port = $port, password = 'this is password with "QUOTE MARKS" ha-ha', }, cb = function(t) f:write('cb ' .. t.what .. '\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb connecting' <&$pfd fakempd_say "OK MPD I-am-actually-a-shell-script" fakempd_expect 'password "this is password with \"QUOTE MARKS\" ha-ha"' fakempd_say 'ACK wrong password (even though it is with "QUOTE MARKS" he-he)' pt_expect_line 'cb error' <&$pfd fakempd_wait pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-mpd/14-bad-hostname.lib.bash ================================================ pt_testcase_begin fakempd_spawn pt_write_widget_file <<__EOF__ widget = { plugin = '$PT_BUILD_DIR/plugins/mpd/plugin-mpd.so', opts = { port = $port, hostname = '255.255.255.255', retry_in = -1, }, cb = function(t) end, } __EOF__ pt_spawn_luastatus -e pt_wait_luastatus || pt_fail "luastatus exited with non-zero code $?" fakempd_kill pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-multiplex/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main wakeup_fifo_file=./tmp-fifo-wakeup ================================================ FILE: tests/pt_tests/plugin-multiplex/01-simple.lib.bash ================================================ pt_testcase_begin using_measure pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/multiplex/plugin-multiplex.so', opts = { data_sources = { src1 = [[ i = 0 widget = { plugin = '$PT_BUILD_DIR/plugins/timer/plugin-timer.so', cb = function(t) i = i + 1 return string.format("%s %d", t, i) end, } ]], }, }, cb = function(t) assert(t.what == 'update') local text = t.updates.src1 f:write('cb ' .. text .. '\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd measure_start pt_expect_line 'cb hello 1' <&$pfd measure_check_ms 0 pt_expect_line 'cb timeout 2' <&$pfd measure_check_ms 1000 pt_expect_line 'cb timeout 3' <&$pfd measure_check_ms 1000 pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-multiplex/02-two-data-sources.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') function make_data_source(num, period) local prefix1 = string.format("MY_NUM = %d\n", num) local prefix2 = string.format("MY_PERIOD = %.3f\n", period) return prefix1 .. prefix2 .. [[ i = 0 widget = { plugin = '$PT_BUILD_DIR/plugins/timer/plugin-timer.so', opts = { period = MY_PERIOD, }, cb = function(t) if t == 'hello' then return nil end i = i + 1 return string.format("[src%d] %s %d", MY_NUM, t, i) end, } ]] end widget = { plugin = '$PT_BUILD_DIR/plugins/multiplex/plugin-multiplex.so', opts = { data_sources = { src1 = make_data_source(1, 3.0), src2 = make_data_source(2, 7.5), }, }, cb = function(t) assert(t.what == 'update') local text = t.updates.src1 or t.updates.src2 if type(text) == 'string' then f:write('cb ' .. text .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line "cb [src1] timeout 1" <&$pfd pt_expect_line "cb [src1] timeout 2" <&$pfd pt_expect_line "cb [src2] timeout 1" <&$pfd pt_expect_line "cb [src1] timeout 3" <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-multiplex/03-call-event.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/multiplex/plugin-multiplex.so', opts = { data_sources = { src1 = [[ alt_text = '' i = 0 widget = { plugin = '$PT_BUILD_DIR/plugins/timer/plugin-timer.so', cb = function(t) if #alt_text ~= 0 then return alt_text end i = i + 1 return string.format("%s %d", t, i) end, event = function(s) alt_text = string.format('event %s', s) end, } ]], }, }, cb = function(t) assert(t.what == 'update') local text = t.updates.src1 f:write('cb ' .. text .. '\n') if text == 'timeout 3' then assert(luastatus.plugin.call_event('src1', '[some data]')) end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb hello 1' <&$pfd pt_expect_line 'cb timeout 2' <&$pfd pt_expect_line 'cb timeout 3' <&$pfd pt_expect_line 'cb event [some data]' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-network-linux/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main ================================================ FILE: tests/pt_tests/plugin-network-linux/01-simple.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') local function _fmt_t(t) local s = {} for k, v in pairs(t) do for _, x in ipairs(v.ipv4 or {}) do s[#s + 1] = string.format('%s ipv4 %s', k, x) end for _, x in ipairs(v.ipv6 or {}) do s[#s + 1] = string.format('%s ipv6 %s', k, x) end end table.sort(s) return table.concat(s, ';') .. ';' end widget = { plugin = '$PT_BUILD_DIR/plugins/network-linux/plugin-network-linux.so', opts = {new_ip_fmt = true}, cb = function(t) if t == nil then f:write('cb nil\n') else f:write('cb table ' .. _fmt_t(t) .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd listnets_binary=$PT_BUILD_DIR/tests/listnets c=0; nets=$("$listnets_binary") || c=$? case "$c" in 0) nets=$(printf '%s\n' "$nets" | LC_ALL=C sort | tr '\n' ';') expect_str="cb table $nets" ;; 1) expect_str="cb nil" ;; *) pt_fail "listnets binary ('$listnets_binary') failed with code $c." ;; esac pt_expect_line "$expect_str" <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-network-linux/02-simple-newfmt.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') local function _fmt_t(t) local s = {} for k, v in pairs(t) do for _, x in ipairs(v.ipv4 or {}) do s[#s + 1] = string.format('%s ipv4 %s', k, x) end for _, x in ipairs(v.ipv6 or {}) do s[#s + 1] = string.format('%s ipv6 %s', k, x) end end table.sort(s) return table.concat(s, ';') .. ';' end widget = { plugin = '$PT_BUILD_DIR/plugins/network-linux/plugin-network-linux.so', opts = { new_ip_fmt = true, new_overall_fmt = true, }, cb = function(t) assert(type(t) == 'table', 't is not a table') assert(t.what, 't.what is not present') if t.what == 'error' then f:write('cb error\n') else f:write('cb ok ' .. _fmt_t(t.data) .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd listnets_binary=$PT_BUILD_DIR/tests/listnets c=0; nets=$("$listnets_binary") || c=$? case "$c" in 0) nets=$(printf '%s\n' "$nets" | LC_ALL=C sort | tr '\n' ';') expect_str="cb ok $nets" ;; 1) expect_str="cb error" ;; *) pt_fail "listnets binary ('$listnets_binary') failed with code $c." ;; esac pt_expect_line "$expect_str" <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-network-rate-linux/00-common.lib.bash ================================================ pt_require_tools mktemp main_fifo_file=./tmp-fifo-main netdev_content_1="\ Inter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed lo: 1563691862 518094 0 0 0 0 0 0 1563691862 518094 0 0 0 0 0 0 enp3s0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 wlan0: 14599861312 19393562 0 706 0 0 0 0 14123982636 19982115 0 23 0 0 0 0 tun0: 1482 19 0 0 0 0 0 0 251702 1316 0 0 0 0 0 0 " netdev_content_2="\ Inter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed lo: 1563691862 518094 0 0 0 0 0 0 1563691862 518094 0 0 0 0 0 0 enp3s0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 wlan0: 14601405091 19395437 0 706 0 0 0 0 14125578402 19984370 0 23 0 0 0 0 tun0: 1482 19 0 0 0 0 0 0 251702 1316 0 0 0 0 0 0 " res_wlan0_div1='R:1543779.0,S:1595766.0' res_wlan0_div2='R:771889.5,S:797883.0' network_rate_linux_testcase() { local extra_lua_opts=$1 local expect_str=$2 pt_testcase_begin pt_add_fifo "$main_fifo_file" local proc_dir; proc_dir=$(mktemp -d) || pt_fail "'mktemp -d' failed" pt_check mkdir "$proc_dir"/net pt_add_dirs_to_remove_inorder "$proc_dir"/net "$proc_dir" local netdev_file=$proc_dir/net/dev printf '%s' "$netdev_content_1" > "$netdev_file" || pt_fail 'cannot write netdev_file' pt_add_file_to_remove "$netdev_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') function my_event_func() end x = dofile('$PT_SOURCE_DIR/plugins/network-rate-linux/network-rate-linux.lua') function stringify_results(tbl) if type(next(tbl)) ~= "number" then local new_tbl = {} for P, Q in pairs(tbl) do table.insert(new_tbl, {P, Q}) end table.sort(new_tbl, function(PQ_a, PQ_b) return PQ_a[1] < PQ_b[1] end) tbl = new_tbl end local chunks = {} for _, PQ in ipairs(tbl) do table.insert(chunks, string.format("%s=>{R:%.1f,S:%.1f}", PQ[1], PQ[2].R, PQ[2].S)) end return string.format("[%s]", table.concat(chunks, ",")) end widget = x.widget{ _procpath = '$proc_dir', cb = function(t) f:write('cb ' .. stringify_results(t) .. '\n') end, event = my_event_func, $extra_lua_opts } widget.plugin = ('$PT_BUILD_DIR/plugins/{}/plugin-{}.so'):gsub('{}', widget.plugin) assert(widget.event == my_event_func) __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb []' <&$pfd printf '%s' "$netdev_content_2" > "$netdev_file" pt_expect_line "$expect_str" <&$pfd pt_close_fd "$pfd" pt_testcase_end } ================================================ FILE: tests/pt_tests/plugin-network-rate-linux/01-simple.lib.bash ================================================ network_rate_linux_testcase '' "cb [lo=>{R:0.0,S:0.0},tun0=>{R:0.0,S:0.0},wlan0=>{$res_wlan0_div1}]" network_rate_linux_testcase 'period=2' "cb [lo=>{R:0.0,S:0.0},tun0=>{R:0.0,S:0.0},wlan0=>{$res_wlan0_div2}]" ================================================ FILE: tests/pt_tests/plugin-network-rate-linux/02-in-array-form.lib.bash ================================================ network_rate_linux_testcase 'in_array_form=true' "cb [lo=>{R:0.0,S:0.0},wlan0=>{$res_wlan0_div1},tun0=>{R:0.0,S:0.0}]" network_rate_linux_testcase 'in_array_form=true,period=2' "cb [lo=>{R:0.0,S:0.0},wlan0=>{$res_wlan0_div2},tun0=>{R:0.0,S:0.0}]" ================================================ FILE: tests/pt_tests/plugin-network-rate-linux/03-iface-filter.lib.bash ================================================ network_rate_linux_testcase_iface_filter() { local lua_opts=$1 shift local expected_out= local letter for letter in "$@"; do case "$letter" in L) expected_out+='lo=>{R:0.0,S:0.0},' ;; T) expected_out+='tun0=>{R:0.0,S:0.0},' ;; W) expected_out+="wlan0=>{$res_wlan0_div1}," ;; *) pt_fail "unexpected letter '$letter'" ;; esac done expected_out=${expected_out%,} network_rate_linux_testcase "$lua_opts" "cb [$expected_out]" } network_rate_linux_testcase_iface_filter 'iface_only="wlan0"' W network_rate_linux_testcase_iface_filter 'iface_except="lo"' T W network_rate_linux_testcase_iface_filter 'iface_only={"tun0","wlan0"}' T W network_rate_linux_testcase_iface_filter 'iface_except={"lo","tun0"}' W network_rate_linux_testcase_iface_filter 'iface_only={tun0=true,wlan0=true}' T W network_rate_linux_testcase_iface_filter 'iface_except={lo=true,tun0=true}' W network_rate_linux_testcase_iface_filter 'iface_filter=function(s) return string.find(s, "w") == nil end' L T ================================================ FILE: tests/pt_tests/plugin-pipe/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main ================================================ FILE: tests/pt_tests/plugin-pipe/01-simple.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') function my_event_func() end x = dofile('$PT_SOURCE_DIR/plugins/pipe/pipe.lua') widget = x.widget{ command = 'echo one; echo two; echo three', cb = function(line) f:write('cb ' .. line .. '\n') end, on_eof = function() f:write('eof\n') while true do end end, event = my_event_func, } widget.plugin = ('$PT_BUILD_DIR/plugins/{}/plugin-{}.so'):gsub('{}', widget.plugin) assert(widget.event == my_event_func) __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb one' <&$pfd pt_expect_line 'cb two' <&$pfd pt_expect_line 'cb three' <&$pfd pt_expect_line 'eof' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-pipe/02-func-shell-escape.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') x = dofile('$PT_SOURCE_DIR/plugins/pipe/pipe.lua') local function dump(id, arg) local s = x.shell_escape(arg) assert(type(s) == 'string', 'shell_escape() return value is not string') f:write(string.format('%s => %s\n', id, s:gsub('\n', ';'))) end dump('1', 'hello') dump('2', 'hello world') dump('3', 'ichi\t nichi') dump('4', 'one\ntwo') dump('5', "it's fine") dump('6', '') dump('7', {}) dump('8', {'one'}) dump('9', {"one's own"}) dump('10', {'one', 'two', 'three'}) dump('11', {'one two', 'three'}) dump('12', {'one', 'two\n\tthree'}) __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" tab=$'\t' pt_expect_line 'init' <&$pfd pt_expect_line "1 => 'hello'" <&$pfd pt_expect_line "2 => 'hello world'" <&$pfd pt_expect_line "3 => 'ichi${tab} nichi'" <&$pfd pt_expect_line "4 => 'one;two'" <&$pfd pt_expect_line "5 => 'it'\\''s fine'" <&$pfd pt_expect_line "6 => ''" <&$pfd pt_expect_line "7 => " <&$pfd pt_expect_line "8 => 'one'" <&$pfd pt_expect_line "9 => 'one'\\''s own'" <&$pfd pt_expect_line "10 => 'one' 'two' 'three'" <&$pfd pt_expect_line "11 => 'one two' 'three'" <&$pfd pt_expect_line "12 => 'one' 'two;${tab}three'" <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-pipev2/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main ================================================ FILE: tests/pt_tests/plugin-pipev2/01-simple.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/pipev2/plugin-pipev2.so', opts = { argv = {'/bin/sh', '-c', 'echo one; echo two; echo three'}, }, cb = function(t) if t.what == 'line' then f:write('cb line ' .. t.line .. '\n') else f:write('cb ' .. t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb line one' <&$pfd pt_expect_line 'cb line two' <&$pfd pt_expect_line 'cb line three' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-pipev2/02-greet.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/pipev2/plugin-pipev2.so', opts = { argv = {'/bin/sh', '-c', 'echo one; echo two; echo three'}, greet = true, }, cb = function(t) if t.what == 'line' then f:write('cb line ' .. t.line .. '\n') else f:write('cb ' .. t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb hello' <&$pfd pt_expect_line 'cb line one' <&$pfd pt_expect_line 'cb line two' <&$pfd pt_expect_line 'cb line three' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-pipev2/03-bye.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/pipev2/plugin-pipev2.so', opts = { argv = {'/bin/sh', '-c', 'echo one; echo two; echo three'}, bye = true, }, cb = function(t) if t.what == 'line' then f:write('cb line ' .. t.line .. '\n') else f:write('cb ' .. t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb line one' <&$pfd pt_expect_line 'cb line two' <&$pfd pt_expect_line 'cb line three' <&$pfd pt_expect_line 'cb bye' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-pipev2/04-write-to-stdin.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') i = 0 widget = { plugin = '$PT_BUILD_DIR/plugins/pipev2/plugin-pipev2.so', opts = { argv = {'/bin/sh', '-c', 'while read x; do echo "you said <\$x>"; done'}, greet = true, pipe_stdin = true, }, cb = function(t) assert(luastatus.plugin.write_to_stdin(string.format('%d\n', i))) i = i + 1 if t.what == 'line' then f:write('cb line ' .. t.line .. '\n') else f:write('cb ' .. t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb hello' <&$pfd pt_expect_line 'cb line you said <0>' <&$pfd pt_expect_line 'cb line you said <1>' <&$pfd pt_expect_line 'cb line you said <2>' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-pipev2/05-kill-default.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/pipev2/plugin-pipev2.so', opts = { argv = {'/usr/bin/env', 'bash', '-c', 'for (( i=0; ; ++i)); do echo \$i; sleep 1; done'}, greet = true, bye = true, }, cb = function(t) if t.what == 'line' then f:write('cb line ' .. t.line .. '\n') if t.line == '3' then luastatus.plugin.kill() end else f:write('cb ' .. t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb hello' <&$pfd pt_expect_line 'cb line 0' <&$pfd pt_expect_line 'cb line 1' <&$pfd pt_expect_line 'cb line 2' <&$pfd pt_expect_line 'cb line 3' <&$pfd pt_expect_line 'cb bye' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-pipev2/06-kill-by-spelling.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/pipev2/plugin-pipev2.so', opts = { argv = {'/usr/bin/env', 'bash', '-c', 'for (( i=0; ; ++i)); do echo \$i; sleep 1; done'}, greet = true, bye = true, }, cb = function(t) if t.what == 'line' then f:write('cb line ' .. t.line .. '\n') if t.line == '3' then luastatus.plugin.kill('SIGKILL') end else f:write('cb ' .. t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb hello' <&$pfd pt_expect_line 'cb line 0' <&$pfd pt_expect_line 'cb line 1' <&$pfd pt_expect_line 'cb line 2' <&$pfd pt_expect_line 'cb line 3' <&$pfd pt_expect_line 'cb bye' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-pipev2/07-kill-by-signo.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/pipev2/plugin-pipev2.so', opts = { argv = {'/usr/bin/env', 'bash', '-c', 'for (( i=0; ; ++i)); do echo \$i; sleep 1; done'}, greet = true, bye = true, }, cb = function(t) if t.what == 'line' then f:write('cb line ' .. t.line .. '\n') if t.line == '3' then luastatus.plugin.kill(9) end else f:write('cb ' .. t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb hello' <&$pfd pt_expect_line 'cb line 0' <&$pfd pt_expect_line 'cb line 1' <&$pfd pt_expect_line 'cb line 2' <&$pfd pt_expect_line 'cb line 3' <&$pfd pt_expect_line 'cb bye' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-pipev2/08-sigrt-bounds.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/pipev2/plugin-pipev2.so', opts = { argv = {'/bin/sh', '-c', 'exit 0'}, greet = true, }, cb = function(t) if t.what == 'hello' then min, max = luastatus.plugin.get_sigrt_bounds() assert(min < max) print(string.format('sigrt bounds: min=%d, max=%d', min, max)) f:write('ok\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-pulse/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main sink_name='PX7NE64ItQVFBw2' x_pulse_begin() { pt_dbus_daemon_spawn --session pt_pulseaudio_daemon_spawn } x_pulse_end() { pt_pulseaudio_daemon_kill pt_dbus_daemon_kill } ================================================ FILE: tests/pt_tests/plugin-pulse/01-simple.lib.bash ================================================ pt_require_tools pacmd x_pulse_begin pt_testcase_begin pt_spawn_thing_pipe pulsetalker "$PT_SOURCE_DIR"/tests/pulsetalker.sh "$sink_name" pt_expect_line 'ready' <&${PT_SPAWNED_THINGS_FDS_0[pulsetalker]} pt_check pacmd set-sink-mute "$sink_name" false pt_check pacmd set-sink-volume "$sink_name" 65536 pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') local last_line = nil widget = { plugin = '$PT_BUILD_DIR/plugins/pulse/plugin-pulse.so', opts = { sink = '$sink_name', }, cb = function(t) assert(t) local line = string.format('[%s] cb %.0f%%', t.name, t.cur / t.norm * 100) if t.mute then line = line .. ' (mute)' end if line ~= last_line then f:write(line .. '\n') end last_line = line end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line "[$sink_name] cb 100%" <&$pfd pacmd set-sink-volume "$sink_name" 32768 pt_expect_line "[$sink_name] cb 50%" <&$pfd pacmd set-sink-mute "$sink_name" true pt_expect_line "[$sink_name] cb 50% (mute)" <&$pfd pacmd set-sink-volume "$sink_name" 2048 pt_expect_line "[$sink_name] cb 3% (mute)" <&$pfd pt_close_fd "$pfd" pt_testcase_end x_pulse_end ================================================ FILE: tests/pt_tests/plugin-pulse/02-lfuncs.lib.bash ================================================ pt_require_tools pacmd x_pulse_begin pt_testcase_begin pt_spawn_thing_pipe pulsetalker "$PT_SOURCE_DIR"/tests/pulsetalker.sh "$sink_name" pt_expect_line 'ready' <&${PT_SPAWNED_THINGS_FDS_0[pulsetalker]} pt_check pacmd set-sink-mute "$sink_name" false || exit $? pt_check pacmd set-sink-volume "$sink_name" 65536 || exit $? pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') local last_line = nil widget = { plugin = '$PT_BUILD_DIR/plugins/pulse/plugin-pulse.so', opts = { sink = '$sink_name', make_self_pipe = true, }, cb = function(t) if t == nil then f:write('cb nil\n') return end local line = string.format('cb %.0f%%', t.cur / t.norm * 100) if t.mute then line = line .. ' (mute)' end if line ~= last_line then f:write(line .. '\n') end last_line = line luastatus.plugin.wake_up() end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb 100%' <&$pfd pt_expect_line 'cb nil' <&$pfd pacmd set-sink-volume "$sink_name" 32768 pt_expect_line 'cb 50%' <&$pfd pt_expect_line 'cb nil' <&$pfd pacmd set-sink-mute "$sink_name" true pt_expect_line 'cb 50% (mute)' <&$pfd pt_expect_line 'cb nil' <&$pfd pacmd set-sink-volume "$sink_name" 2048 pt_expect_line 'cb 3% (mute)' <&$pfd pt_expect_line 'cb nil' <&$pfd pt_close_fd "$pfd" pt_testcase_end x_pulse_end ================================================ FILE: tests/pt_tests/plugin-temperature-linux/00-common.lib.bash ================================================ pt_require_tools mktemp main_fifo_file=./tmp-fifo-main temperature_linux_add_dir() { local path=$1 mkdir -- "$path" || pt_fail "cannot create directory '$path'" pt_add_dir_to_remove "$path" } temperature_linux_add_file() { local path=$1 local content=$2 printf '%s\n' "$content" > "$path" || pt_fail "cannot write file '$path'" pt_add_file_to_remove "$path" } temperature_linux_create_carcass() { local tmp_root=$1 local carcass=$2 mkdir "$tmp_root"/hwmon || pt_fail "cannot create hwmon dir ($tmp_root/hwmon)" mkdir "$tmp_root"/thermal || pt_fail "cannot create thermal dir ($tmp_root/thermal)" local ftype local path local content local dont_care_1 local dont_care_2 while read ftype path content dont_care_1 dont_care_2; do if [[ $ftype == D ]]; then temperature_linux_add_dir "$tmp_root"/"$path" elif [[ $ftype == F ]]; then temperature_linux_add_file "$tmp_root"/"$path" "$content" else pt_fail "Unexpected ftype '$ftype'" fi done < <(printf '%s\n' "$carcass" | sed '/^$/d') pt_add_dir_to_remove "$tmp_root"/hwmon pt_add_dir_to_remove "$tmp_root"/thermal } temperature_linux_make_awk_program() { local filter_regex=$1 cat <%s, name=>%s, value=>%.0f)", x.kind, x.name, x.value * 1000 )) end f:write('\n') end, event = my_event_func, } widget = x.widget(_opts, _data) widget.plugin = ('$PT_BUILD_DIR/plugins/{}/plugin-{}.so'):gsub('{}', widget.plugin) assert(widget.event == my_event_func) __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line "$expect_str" <&$pfd pt_close_fd "$pfd" pt_testcase_end } ================================================ FILE: tests/pt_tests/plugin-temperature-linux/01-common-carcass.lib.bash ================================================ ### --- Generated with: --- ########################### # ( # set -e # # echo_D() { # local path=$1 # if [ -e "$path" ]; then # echo "D $path _ _ _" # fi # } # # echo_F() { # local path=$1 # local kind=$2 # local name=$3 # if [ -e "$path" ]; then # echo "F $path $(cat < "$path") $kind $name" # fi # } # # cd /sys/class # # for d in thermal/thermal_zone*; do # echo_D "$d" # echo_F "$d"/temp thermal "${d##*/}" # done # # for d in hwmon/*; do # echo_D "$d" # echo_F "$d"/name _ _ # IFS= read -r name < "$d"/name || continue # for f in "$d"/temp*_input; do # echo_F "$f" hwmon "$name" # done # done # ) common_carcass="\ D thermal/thermal_zone0 _ _ _ F thermal/thermal_zone0/temp 43000 thermal thermal_zone0 D thermal/thermal_zone1 _ _ _ F thermal/thermal_zone1/temp 20000 thermal thermal_zone1 D thermal/thermal_zone2 _ _ _ F thermal/thermal_zone2/temp 37050 thermal thermal_zone2 D thermal/thermal_zone3 _ _ _ F thermal/thermal_zone3/temp 42050 thermal thermal_zone3 D thermal/thermal_zone4 _ _ _ F thermal/thermal_zone4/temp 40050 thermal thermal_zone4 D thermal/thermal_zone5 _ _ _ F thermal/thermal_zone5/temp 50 thermal thermal_zone5 D thermal/thermal_zone6 _ _ _ F thermal/thermal_zone6/temp 50 thermal thermal_zone6 D thermal/thermal_zone7 _ _ _ F thermal/thermal_zone7/temp 32650 thermal thermal_zone7 D thermal/thermal_zone8 _ _ _ F thermal/thermal_zone8/temp 43050 thermal thermal_zone8 D thermal/thermal_zone9 _ _ _ F thermal/thermal_zone9/temp 43000 thermal thermal_zone9 D hwmon/hwmon0 _ _ _ F hwmon/hwmon0/name acpitz _ _ F hwmon/hwmon0/temp1_input 43000 hwmon acpitz D hwmon/hwmon1 _ _ _ F hwmon/hwmon1/name BAT0 _ _ D hwmon/hwmon2 _ _ _ F hwmon/hwmon2/name nvme _ _ F hwmon/hwmon2/temp1_input 36850 hwmon nvme D hwmon/hwmon3 _ _ _ F hwmon/hwmon3/name ADP1 _ _ D hwmon/hwmon4 _ _ _ F hwmon/hwmon4/name coretemp _ _ F hwmon/hwmon4/temp1_input 42000 hwmon coretemp F hwmon/hwmon4/temp2_input 39000 hwmon coretemp F hwmon/hwmon4/temp3_input 40000 hwmon coretemp F hwmon/hwmon4/temp4_input 39000 hwmon coretemp F hwmon/hwmon4/temp5_input 39000 hwmon coretemp " ================================================ FILE: tests/pt_tests/plugin-temperature-linux/02-simple.lib.bash ================================================ temperature_linux_testcase "$common_carcass" ================================================ FILE: tests/pt_tests/plugin-temperature-linux/03-filter-by-kind.lib.bash ================================================ temperature_linux_testcase "$common_carcass" \ 'function(kind, name) return kind == "thermal" end' \ '^thermal:.*$' ================================================ FILE: tests/pt_tests/plugin-temperature-linux/04-filter-by-name.lib.bash ================================================ temperature_linux_testcase "$common_carcass" \ 'function(kind, name) return (name:find("t")) ~= nil end' \ '^.*:.*t.*$' ================================================ FILE: tests/pt_tests/plugin-timer/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main wakeup_fifo_file=./tmp-fifo-wakeup ================================================ FILE: tests/pt_tests/plugin-timer/01-default.lib.bash ================================================ pt_testcase_begin using_measure pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/timer/plugin-timer.so', cb = function(t) f:write('cb ' .. t .. '\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd measure_start pt_expect_line 'cb hello' <&$pfd measure_check_ms 0 pt_expect_line 'cb timeout' <&$pfd measure_check_ms 1000 pt_expect_line 'cb timeout' <&$pfd measure_check_ms 1000 pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-timer/02-period.lib.bash ================================================ pt_testcase_begin using_measure pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/timer/plugin-timer.so', opts = { period = 0.1, }, cb = function(t) f:write('cb ' .. t .. '\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb hello' <&$pfd measure_start pt_expect_line 'cb timeout' <&$pfd measure_check_ms 100 pt_expect_line 'cb timeout' <&$pfd measure_check_ms 100 pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-timer/03-wakeup-fifo.lib.bash ================================================ pt_testcase_begin using_measure pt_add_fifo "$main_fifo_file" pt_add_fifo "$wakeup_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/timer/plugin-timer.so', opts = { fifo = '$wakeup_fifo_file', }, cb = function(t) f:write('cb ' .. t .. '\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd measure_start pt_expect_line 'cb hello' <&$pfd measure_check_ms 0 pt_expect_line 'cb timeout' <&$pfd measure_check_ms 1000 touch "$wakeup_fifo_file" pt_expect_line 'cb fifo' <&$pfd measure_check_ms 0 pt_expect_line 'cb timeout' <&$pfd measure_check_ms 1000 pt_expect_line 'cb timeout' <&$pfd measure_check_ms 1000 pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-timer/04-lfuncs.lib.bash ================================================ pt_testcase_begin using_measure pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') n=0 widget = { plugin = '$PT_BUILD_DIR/plugins/timer/plugin-timer.so', opts = { period = 0.1, }, cb = function(t) if t == 'timeout' then n = (n+1)%3 f:write('cb timeout n=' .. n .. '\n') if n == 0 then luastatus.plugin.push_period(0.6) end else f:write('cb ' .. t .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd measure_start pt_expect_line 'cb hello' <&$pfd measure_check_ms 0 pt_expect_line 'cb timeout n=1' <&$pfd measure_check_ms 100 for (( i = 0; i < 3; ++i )); do pt_expect_line 'cb timeout n=2' <&$pfd measure_check_ms 100 pt_expect_line 'cb timeout n=0' <&$pfd measure_check_ms 100 pt_expect_line 'cb timeout n=1' <&$pfd measure_check_ms 600 done pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-timer/05-procalive-lfuncs.lib.bash ================================================ pt_testcase_begin using_measure pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') function check_eq(a, b) if a ~= b then print(a, '!=', b) error('check_eq() failed') end end function array_contains(t, v) for _, x in ipairs(t) do if x == v then return true end end return false end -- this function converts return list with multiple values into a _S_ingle value function S(x) return x end widget = { plugin = '$PT_BUILD_DIR/plugins/timer/plugin-timer.so', opts = { period = 1.0, }, cb = function(t) if t ~= 'hello' then return end check_eq(S(luastatus.plugin.access('.')), true) check_eq(S(luastatus.plugin.access('./gbdc06iiw1kZ4yccnnx9')), false) check_eq(S(luastatus.plugin.stat('.')), 'dir') check_eq(S(luastatus.plugin.stat('/proc/uptime')), 'regular') check_eq(S(luastatus.plugin.stat('./gbdc06iiw1kZ4yccnnx9')), nil) assert(array_contains(S(luastatus.plugin.glob('./*')), './pt.sh')) assert(luastatus.plugin.is_process_alive($$)) assert(luastatus.plugin.is_process_alive("$$")) assert(luastatus.plugin.is_process_alive(1)) assert(luastatus.plugin.is_process_alive("1")) while true do local random_pid = math.random(2, 1073741823) print('trying random_pid=' .. random_pid) if not luastatus.plugin.is_process_alive(random_pid) then break end print('it is alive, retrying') end f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-timer/06-self-pipe.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/timer/plugin-timer.so', opts = { period = 0.1, make_self_pipe = true, }, cb = function(t) f:write('cb ' .. t .. '\n') if t == 'timeout' then luastatus.plugin.wake_up() end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'cb hello' <&$pfd pt_expect_line 'cb timeout' <&$pfd pt_expect_line 'cb self_pipe' <&$pfd pt_expect_line 'cb timeout' <&$pfd pt_expect_line 'cb self_pipe' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-unixsock/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main socket_file=./pt-test-socket unixsock_wait_socket() { echo >&2 "Waiting for socket $socket_file to appear..." while ! [[ -S $socket_file ]]; do true done } unixsock_send_verbatim() { printf '%s' "$1" | "$PT_PARROT" UNIX-CLIENT "$socket_file" } ================================================ FILE: tests/pt_tests/plugin-unixsock/01-simple.lib.bash ================================================ pt_testcase_begin rm -f "$socket_file" pt_add_fifo "$main_fifo_file" pt_add_file_to_remove "$socket_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/unixsock/plugin-unixsock.so', opts = { path = '$socket_file', }, cb = function(t) assert(t.what == 'line') f:write('line ' .. t.line .. '\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd unixsock_wait_socket unixsock_send_verbatim $'one\n' pt_expect_line 'line one' <&$pfd unixsock_send_verbatim $'two\n' pt_expect_line 'line two' <&$pfd unixsock_send_verbatim 'without newline' unixsock_send_verbatim $'with newline\n' pt_expect_line 'line with newline' <&$pfd unixsock_send_verbatim $'aypibmmwxrdxdknh\ngmstvxwxamouhlmw\ncybymucjtfxrauwn' pt_expect_line 'line aypibmmwxrdxdknh' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-unixsock/02-greet.lib.bash ================================================ pt_testcase_begin rm -f "$socket_file" pt_add_fifo "$main_fifo_file" pt_add_file_to_remove "$socket_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/unixsock/plugin-unixsock.so', opts = { path = '$socket_file', greet = true, }, cb = function(t) if t.what == 'line' then f:write('line ' .. t.line .. '\n') else f:write(t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd unixsock_wait_socket pt_expect_line 'hello' <&$pfd unixsock_send_verbatim $'one\n' pt_expect_line 'line one' <&$pfd unixsock_send_verbatim $'two\n' pt_expect_line 'line two' <&$pfd unixsock_send_verbatim $'three\n' pt_expect_line 'line three' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-unixsock/03-do-not-try-unlink-failure.lib.bash ================================================ pt_testcase_begin rm -f "$socket_file" true > "$socket_file" || pt_fail "Cannot create regular file $socket_file." pt_add_file_to_remove "$socket_file" pt_write_widget_file <<__EOF__ widget = { plugin = '$PT_BUILD_DIR/plugins/unixsock/plugin-unixsock.so', opts = { path = '$socket_file', try_unlink = false, }, cb = function(t) end, } __EOF__ pt_spawn_luastatus -e pt_wait_luastatus || pt_fail "luastatus exited with non-zero code $?" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-unixsock/04-do-try-unlink-success.lib.bash ================================================ pt_testcase_begin rm -f "$socket_file" pt_add_fifo "$main_fifo_file" true > "$socket_file" || pt_fail "Cannot create regular file $socket_file." pt_add_file_to_remove "$socket_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/unixsock/plugin-unixsock.so', opts = { path = '$socket_file', }, cb = function(t) assert(t.what == 'line') f:write('line ' .. t.line .. '\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd unixsock_wait_socket unixsock_send_verbatim $'aloha\n' pt_expect_line 'line aloha' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-unixsock/05-timeout.lib.bash ================================================ pt_testcase_begin rm -f "$socket_file" using_measure pt_add_fifo "$main_fifo_file" pt_add_file_to_remove "$socket_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/unixsock/plugin-unixsock.so', opts = { path = '$socket_file', greet = true, timeout = 0.25, }, cb = function(t) if t.what == 'line' then f:write('line ' .. t.line .. '\n') else f:write(t.what .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd unixsock_wait_socket pt_expect_line 'hello' <&$pfd measure_start pt_expect_line 'timeout' <&$pfd measure_check_ms 250 pt_expect_line 'timeout' <&$pfd measure_check_ms 250 unixsock_send_verbatim $'boo\n' pt_expect_line 'line boo' <&$pfd measure_check_ms 0 pt_expect_line 'timeout' <&$pfd measure_check_ms 250 pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-unixsock/06-lfuncs.lib.bash ================================================ pt_testcase_begin rm -f "$socket_file" using_measure pt_add_fifo "$main_fifo_file" pt_add_file_to_remove "$socket_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') n=0 widget = { plugin = '$PT_BUILD_DIR/plugins/unixsock/plugin-unixsock.so', opts = { path = '$socket_file', greet = true, timeout = 0.1, }, cb = function(t) if t.what == 'line' then f:write('line ' .. t.line .. '\n') else n=(n+1)%3 f:write(t.what .. ' n=' .. n .. '\n') if n == 0 then luastatus.plugin.push_timeout(0.6) end end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd unixsock_wait_socket pt_expect_line 'hello n=1' <&$pfd measure_start for (( i = 0; i < 4; ++i )); do unixsock_send_verbatim "foobar$i"$'\n' pt_expect_line "line foobar$i" <&$pfd measure_check_ms 0 pt_expect_line 'timeout n=2' <&$pfd measure_check_ms 100 pt_expect_line 'timeout n=0' <&$pfd measure_check_ms 100 pt_expect_line 'timeout n=1' <&$pfd measure_check_ms 600 done pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-web/00-common.lib.bash ================================================ main_fifo_file=./tmp-fifo-main pt_find_free_tcp_port port=$PT_FOUND_FREE_PORT httpserv_spawn() { pt_spawn_thing_pipe httpserv "$PT_HTTPSERV" --port="$port" "$@" pt_expect_line 'ready' <&${PT_SPAWNED_THINGS_FDS_0[httpserv]} } httpserv_expect() { pt_expect_line "$1" <&${PT_SPAWNED_THINGS_FDS_0[httpserv]} } httpserv_say() { printf '%s\n' "$1" >&${PT_SPAWNED_THINGS_FDS_1[httpserv]} } httpserv_kill() { pt_kill_thing httpserv } httpserv_wait() { pt_wait_thing httpserv || true } ================================================ FILE: tests/pt_tests/plugin-web/01-simple.lib.bash ================================================ pt_testcase_begin httpserv_spawn GET / pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/web/plugin-web.so', opts = { planner = function() while true do coroutine.yield({ action = 'request', params = { url = 'http://127.0.0.1:$port/', }, }) coroutine.yield({action = 'sleep', period = 1.0}) end end, }, cb = function(t) assert(t.what == 'response') if t.error then error('got error: ' .. t.error) end f:write('resp ' .. t.body .. '\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd httpserv_expect '>' httpserv_say 'RESPONSE' pt_expect_line 'resp RESPONSE' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-web/02-post.lib.bash ================================================ pt_testcase_begin httpserv_spawn POST / pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/web/plugin-web.so', opts = { planner = function() while true do coroutine.yield({ action = 'request', params = { url = 'http://127.0.0.1:$port/', post_fields = 'test=TEST', }, }) coroutine.yield({action = 'sleep', period = 1.0}) end end, }, cb = function(t) assert(t.what == 'response') f:write('resp ' .. t.body .. '\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd httpserv_expect '>test=TEST' httpserv_say 'RESPONSE' pt_expect_line 'resp RESPONSE' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-web/03-call-cb.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/web/plugin-web.so', opts = { planner = function() while true do coroutine.yield({action = 'call_cb', what = 'hi there'}) coroutine.yield({action = 'sleep', period = 0.1}) end end, }, cb = function(t) assert(t.what == 'hi there') f:write(t.what .. '\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd for (( i = 0; i < 5; ++i )); do pt_expect_line 'hi there' <&$pfd done pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-web/04-with-headers.lib.bash ================================================ pt_testcase_begin httpserv_spawn GET / pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') local function strip(header) local res, _ = header:gsub('[\r\n]', '') return res end widget = { plugin = '$PT_BUILD_DIR/plugins/web/plugin-web.so', opts = { planner = function() while true do coroutine.yield({ action = 'request', params = { url = 'http://127.0.0.1:$port/', with_headers = true, }, }) coroutine.yield({action = 'sleep', period = 1.0}) end end, }, cb = function(t) assert(t.what == 'response') print('Total # of headers:', #t.headers) for _, header in ipairs(t.headers) do print('Header:', strip(header)) if header:lower() == 'content-type: text/plain\r\n' then f:write('ok\n') return end end f:write('fail\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd httpserv_expect '>' httpserv_say 'RESPONSE' pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-web/05-json-encode.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/web/plugin-web.so', opts = { planner = function() while true do coroutine.yield({action = 'call_cb', what = 'test'}) coroutine.yield({action = 'sleep', period = 1.0}) end end, }, cb = function(t) assert(t.what == 'test') local r r = luastatus.plugin.json_encode_str([[xyz"\\]] .. '\n') assert(r == [[xyz\u0022\u005C\u000A]]) r = luastatus.plugin.json_encode_num(0.5) assert(tonumber(r) == 0.5) f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-web/06-urlencode.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') local function check(src, plus_encoding, expected) local r = luastatus.plugin.urlencode(src, plus_encoding) if r ~= expected then print('Expected:', expected) print('Found:', r) print('plus_encoding:', plus_encoding) error('check() failed') end end widget = { plugin = '$PT_BUILD_DIR/plugins/web/plugin-web.so', opts = { planner = function() while true do coroutine.yield({action = 'call_cb', what = 'test'}) coroutine.yield({action = 'sleep', period = 1.0}) end end, }, cb = function(t) assert(t.what == 'test') check('test!foo?bar', true, 'test%21foo%3Fbar') check('test!foo?bar', false, 'test%21foo%3Fbar') check('check it', true, 'check+it') check('check it', false, 'check%20it') f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-web/07-urldecode.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') local function check(src, expected) local r = luastatus.plugin.urldecode(src) if r ~= expected then print('Expected:', expected) print('Found:', r) error('check() failed') end end widget = { plugin = '$PT_BUILD_DIR/plugins/web/plugin-web.so', opts = { planner = function() while true do coroutine.yield({action = 'call_cb', what = 'test'}) coroutine.yield({action = 'sleep', period = 1.0}) end end, }, cb = function(t) assert(t.what == 'test') check('test', 'test') check('foo+bar', 'foo bar') check('test%20case', 'test case') check('what%3F', 'what?') check('what%3f', 'what?') check('xx%xx', nil) check('foo%', nil) check('foo%1', nil) check('foo%1x', nil) f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-web/08-json-decode.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') local function check_dict_impl(json_str, expected, is_marked) local res, err = luastatus.plugin.json_decode(json_str, is_marked) assert(type(res) == 'table') if expected.foo ~= nil then local res_key = next(res) assert(res_key == 'foo') assert(next(res, res_key) == nil) assert(res.foo == expected.foo) else assert(next(res) == nil) end local mt = getmetatable(res) if is_marked then assert(mt ~= nil) assert(mt.is_dict) assert(not mt.is_array) else assert(mt == nil) end end local function check_dict(json_str, expected) check_dict_impl(json_str, expected, false) check_dict_impl(json_str, expected, true) end local function check_array_impl(json_str, expected, is_marked) local res, err = luastatus.plugin.json_decode(json_str, is_marked) assert(type(res) == 'table') assert(#res == #expected) for i = 1, #res do assert(res[i] == expected[i]) end local mt = getmetatable(res) if is_marked then assert(mt ~= nil) assert(mt.is_array) assert(not mt.is_dict) else assert(mt == nil) end end local function check_array(json_str, expected) check_array_impl(json_str, expected, false) check_array_impl(json_str, expected, true) end widget = { plugin = '$PT_BUILD_DIR/plugins/web/plugin-web.so', opts = { planner = function() while true do coroutine.yield({action = 'call_cb', what = 'test'}) coroutine.yield({action = 'sleep', period = 1.0}) end end, }, cb = function(t) assert(t.what == 'test') check_dict('{"foo":"bar"}', {foo = "bar"}) check_dict('{"foo":42}', {foo = 42}) check_dict('{"foo":true}', {foo = true}) check_dict('{"foo":false}', {foo = false}) check_dict('{"foo":null}', {}) check_array('[]', {}) check_array('["foo"]', {"foo"}) check_array('["foo", "bar"]', {"foo", "bar"}) f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-web/09-json-decode-null.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/web/plugin-web.so', opts = { planner = function() while true do coroutine.yield({action = 'call_cb', what = 'test'}) coroutine.yield({action = 'sleep', period = 1.0}) end end, }, cb = function(t) assert(t.what == 'test') local json_str = '{"foo":null}' local res = assert(luastatus.plugin.json_decode(json_str, false, true)) assert(type(res) == 'table') assert(type(res.foo) == 'userdata') f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-web/10-error.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/web/plugin-web.so', opts = { planner = function() while true do coroutine.yield({ action = 'request', params = { url = 'foobar:////', }, }) coroutine.yield({action = 'sleep', period = 1.0}) end end, }, cb = function(t) assert(t.what == 'response') assert(t.error) local i, _ = t.error:find('libcurl error') assert(i) f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-web/11-timeout.lib.bash ================================================ pt_testcase_begin httpserv_spawn --max-requests=1 --freeze-for=10000 GET / pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/web/plugin-web.so', opts = { planner = function() while true do coroutine.yield({ action = 'request', params = { url = 'http://127.0.0.1:$port/', timeout = 2.0, }, }) coroutine.yield({action = 'sleep', period = 0.1}) end end, }, cb = function(t) assert(t.what == 'response') if t.error then f:write('error\n') else f:write('resp ' .. t.body .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd httpserv_expect '>' httpserv_say 'RESPONSE' pt_expect_line 'resp RESPONSE' <&$pfd pt_expect_line 'error' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-web/12-not-avail.lib.bash ================================================ pt_testcase_begin httpserv_spawn --max-requests=1 GET / pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/web/plugin-web.so', opts = { planner = function() while true do coroutine.yield({ action = 'request', params = { url = 'http://127.0.0.1:$port/', timeout = 2.0, }, }) coroutine.yield({action = 'sleep', period = 0.1}) end end, }, cb = function(t) assert(t.what == 'response') if t.error then f:write('error\n') else f:write('resp ' .. t.body .. '\n') end end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd httpserv_expect '>' httpserv_say 'RESPONSE' pt_expect_line 'resp RESPONSE' <&$pfd pt_expect_line 'error' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-web/13-time-now.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/web/plugin-web.so', opts = { planner = function() while true do coroutine.yield({action = 'call_cb', what = 'test'}) coroutine.yield({action = 'sleep', period = 1.0}) end end, }, cb = function(t) assert(t.what == 'test') local t0 = assert(luastatus.plugin.time_now()) os.execute('sleep 1') local t1 = assert(luastatus.plugin.time_now()) assert(t1 > t0) f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pt_tests/plugin-web/14-get-supported-opts.lib.bash ================================================ pt_testcase_begin pt_add_fifo "$main_fifo_file" pt_write_widget_file <<__EOF__ f = assert(io.open('$main_fifo_file', 'w')) f:setvbuf('line') f:write('init\n') widget = { plugin = '$PT_BUILD_DIR/plugins/web/plugin-web.so', opts = { planner = function() while true do coroutine.yield({action = 'call_cb', what = 'test'}) coroutine.yield({action = 'sleep', period = 1.0}) end end, }, cb = function(t) assert(t.what == 'test') local t = assert(luastatus.plugin.get_supported_opts()) assert(t.url) for k, v in pairs(t) do assert(type(k) == 'string') assert(v == true) end f:write('ok\n') end, } __EOF__ pt_spawn_luastatus exec {pfd}<"$main_fifo_file" pt_expect_line 'init' <&$pfd pt_expect_line 'ok' <&$pfd pt_close_fd "$pfd" pt_testcase_end ================================================ FILE: tests/pulsetalker.sh ================================================ #!/usr/bin/env bash set -e if (( $# == 1 )); then sink_name=$1 pactl_opts=() elif (( $# == 2 )); then sink_name=$1 server_name=$2 pactl_opts=( --server="$server_name" ) else echo >&2 "USAGE: $0 SINK_NAME [SERVER_NAME]" exit 2 fi if ! command -v pactl >/dev/null; then echo >&2 "'pactl' tool was not found." exit 1 fi unset mod_idx trap ' if [[ -n "$mod_idx" ]]; then echo >&2 "[pulsetalker] Unloading module with index $mod_idx..." pactl "${pactl_opts[@]}" unload-module "$mod_idx" fi ' EXIT echo >&2 "[pulsetalker] Creating a null sink with name '$sink_name'..." mod_idx=$(pactl "${pactl_opts[@]}" load-module module-null-sink sink_name="$sink_name") echo ready echo >&2 "[pulsetalker] Done, module index is $mod_idx." echo >&2 "[pulsetalker] Now waiting for signal..." while true; do sleep 10 done ================================================ FILE: tests/stopwatch.c ================================================ /* * Copyright (C) 2021-2025 luastatus developers * * This file is part of luastatus. * * luastatus is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * luastatus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with luastatus. If not, see . */ #include #include #include #include #include #include #include #include static inline struct timespec now_ts(void) { struct timespec ts; int rc; #if (!defined(_POSIX_MONOTONIC_CLOCK)) || (_POSIX_MONOTONIC_CLOCK < 0) // CLOCK_MONOTONIC is not supported at compile-time. rc = clock_gettime(CLOCK_REALTIME, &ts); #elif _POSIX_MONOTONIC_CLOCK > 0 // CLOCK_MONOTONIC is supported both at compile-time and at run-time. rc = clock_gettime(CLOCK_MONOTONIC, &ts); #else // CLOCK_MONOTONIC is supported at compile-time, but might or might not // be supported at run-time. rc = clock_gettime(CLOCK_MONOTONIC, &ts); if (rc < 0) { rc = clock_gettime(CLOCK_REALTIME, &ts); } #endif if (rc < 0) { fprintf(stderr, "stopwatch: clock_gettime() failed.\n"); abort(); } return ts; } static inline uint64_t now_ms(void) { struct timespec ts = now_ts(); return ((uint64_t) ts.tv_sec) * 1000 + (ts.tv_nsec / 1000000); } static uint64_t parse_uint_cstr_or_die(const char *s) { errno = 0; char *endptr; uint64_t r = strtoull(s, &endptr, 10); if (errno || endptr == s || endptr[0] != '\0') { fprintf(stderr, "stopwatch: cannot parse as unsigned integer: '%s'.\n", s); abort(); } return r; } static void trim_newline(char *s) { size_t ns = strlen(s); if (ns && s[ns - 1] == '\n') { s[ns - 1] = '\0'; } } int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "USAGE: stopwatch \n"); return 2; } uint64_t slackness = parse_uint_cstr_or_die(argv[1]); uint64_t start = 0; char *buf = NULL; size_t nbuf = 0; while (getline(&buf, &nbuf, stdin) > 0) { switch (buf[0]) { case 's': start = now_ms(); break; case 'q': printf("%" PRIu64 "\n", now_ms() - start); fflush(stdout); break; case 'r': { uint64_t cur = now_ms(); printf("%" PRIu64 "\n", cur - start); fflush(stdout); start = cur; } break; case 'c': { if (buf[1] != ' ') { fprintf(stderr, "stopwatch: expected space after 'c', found symbol '%c'.\n", buf[1]); abort(); } trim_newline(buf); uint64_t cur = now_ms(); uint64_t delta = cur - start; uint64_t expected = parse_uint_cstr_or_die(buf + 2); if (delta > expected + slackness) { printf("0 delta-is-too-big %" PRIu64 "\n", delta); } else if (expected > slackness && delta < (expected - slackness)) { printf("0 delta-is-too-small %" PRIu64 "\n", delta); } else { printf("1\n"); } fflush(stdout); start = cur; } break; default: fprintf(stderr, "stopwatch: got line with unexpected first symbol '%c'.\n", buf[0]); abort(); } } if (!feof(stdin)) { perror("stopwatch: getline"); abort(); } free(buf); return 0; } ================================================ FILE: tests/torture.sh ================================================ #!/usr/bin/env bash set -e opwd=$PWD cd -- "$(dirname "$(readlink "$0" || printf '%s\n' "$0")")" source ./utils.lib.bash if (( $# != 1 )); then echo >&2 "USAGE: $0 " exit 2 fi BUILD_DIR=$(resolve_relative "$1" "$opwd") LUASTATUS=( "$BUILD_DIR"/luastatus/luastatus ${DEBUG:+-l trace} ) VALGRIND=( valgrind --error-exitcode=42 ) run1() { local n=$1 m=$2 sep_st=$3 event_beg event_end if (( sep_st )); then event_beg='function()' event_end='end' else event_beg='[[' event_end=']]' fi shift 3 "${VALGRIND[@]}" "$@" "${LUASTATUS[@]}" -e -b "$BUILD_DIR"/tests/barlib-mock.so -B gen_events="$m" <(cat <<__EOF__ n = 0 widget = { plugin = '$BUILD_DIR/tests/plugin-mock.so', opts = { make_calls = $n, }, cb = function() n = n + 1 if n % 10000 == 0 then --print('--- n = ' .. n .. ' ---') end end, event = $event_beg m = (m or 0) + 1 if m % 10000 == 0 then --print('--- m = ' .. m .. ' ---') end $event_end, } __EOF__ ) } run2() { run1 "$1" "$2" 0 "${@:3}" run1 "$1" "$2" 1 "${@:3}" } run2 10000 10000 \ --suppressions=dlopen.supp \ --leak-check=full \ --show-leak-kinds=all \ --errors-for-leak-kinds=all \ --track-fds=yes # Interesting options: # --fair-sched=yes run2 100000 100000 \ --tool=helgrind echo >&2 "=== PASSED ===" ================================================ FILE: tests/utils.lib.bash ================================================ resolve_relative() { if [[ $1 == /* ]]; then printf '%s\n' "$1" else printf '%s\n' "$2/$1" fi }